From 09131573993a0c4d32be6edbac604dc3412d331c Mon Sep 17 00:00:00 2001 From: Josh Hall-Bachner Date: Mon, 10 Jun 2013 17:18:17 -0700 Subject: [PATCH 1/4] Added the initial structure for a Symfony Validation handler that is injected into the parsers. --- Formatter/AbstractFormatter.php | 1 + NelmioApiDocBundle.php | 1 + Parser/FormTypeParser.php | 6 + Parser/SymfonyValidationParser.php | 115 ++++++++++++++++ Resources/config/formatters.xml | 5 + Resources/config/services.xml | 3 +- Resources/views/method.html.twig | 4 +- Tests/Fixtures/Model/ValidatorTest.php | 95 +++++++++++++ Tests/Parser/SymfonyValidationHandlerTest.php | 128 ++++++++++++++++++ 9 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 Parser/SymfonyValidationParser.php create mode 100644 Tests/Fixtures/Model/ValidatorTest.php create mode 100644 Tests/Parser/SymfonyValidationHandlerTest.php diff --git a/Formatter/AbstractFormatter.php b/Formatter/AbstractFormatter.php index b5d2fc7..f521a4a 100644 --- a/Formatter/AbstractFormatter.php +++ b/Formatter/AbstractFormatter.php @@ -73,6 +73,7 @@ abstract class AbstractFormatter implements FormatterInterface 'dataType' => $info['dataType'], 'readonly' => $info['readonly'], 'required' => $info['required'], + 'format' => array_key_exists('format', $info) ? $info['format'] : null, 'sinceVersion' => array_key_exists('sinceVersion', $info) ? $info['sinceVersion'] : null, 'untilVersion' => array_key_exists('untilVersion', $info) ? $info['untilVersion'] : null, ); diff --git a/NelmioApiDocBundle.php b/NelmioApiDocBundle.php index c165888..bfce996 100644 --- a/NelmioApiDocBundle.php +++ b/NelmioApiDocBundle.php @@ -7,6 +7,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Nelmio\ApiDocBundle\DependencyInjection\RegisterJmsParserPass; use Nelmio\ApiDocBundle\DependencyInjection\RegisterExtractorParsersPass; use Nelmio\ApiDocBundle\DependencyInjection\ExtractorHandlerCompilerPass; +use Nelmio\ApiDocBundle\DependencyInjection\ParserHandlerCompilerPass; class NelmioApiDocBundle extends Bundle { diff --git a/Parser/FormTypeParser.php b/Parser/FormTypeParser.php index f04a2d9..f55f913 100644 --- a/Parser/FormTypeParser.php +++ b/Parser/FormTypeParser.php @@ -23,6 +23,11 @@ class FormTypeParser implements ParserInterface */ protected $formFactory; + /** + * @var \Symfony\Component\Form\FormRegistry + */ + protected $formRegistry; + /** * @var array */ @@ -82,6 +87,7 @@ class FormTypeParser implements ParserInterface private function parseForm($form, $prefix = null) { + $className = get_class($form); $parameters = array(); foreach ($form as $name => $child) { $config = $child->getConfig(); diff --git a/Parser/SymfonyValidationParser.php b/Parser/SymfonyValidationParser.php new file mode 100644 index 0000000..f1294cf --- /dev/null +++ b/Parser/SymfonyValidationParser.php @@ -0,0 +1,115 @@ +factory = $factory; + } + + /** + * {@inheritdoc} + */ + public function supports(array $input) + { + $className = $input['class']; + + return $this->factory->hasMetadataFor($className); + } + + /** + * {@inheritdoc} + */ + public function parse(array $input) + { + $vparams = array(); + $className = $input['class']; + + $classdata = $this->factory->getMetadataFor($className); + + if($classdata->hasPropertyMetadata($name)) { + $propdata = $classdata->getPropertyMetadata($name); + $propdata = reset($propdata); + $constraints = $propdata->getConstraints(); + + foreach($constraints as $constraint) { + $vparams = $this->parseConstraint($constraint, $vparams); + } + + if(isset($vparams['format'])) { + $vparams['format'] = join(', ', $vparams['format']); + } + } + + return $vparams; + } + + protected function parseConstraint(Constraint $constraint, $vparams) + { + $class = substr(get_class($constraint), strlen('Symfony\\Component\\Validator\\Constraints\\')); + + switch($class) { + case 'NotBlank': + case 'NotNull': + $vparams['required'] = true; + break; + case 'Type': + $vparams['dataType'] = $constraint->type; + break; + case 'Email': + $vparams['format'][] = '{email address}'; + break; + case 'Url': + $vparams['format'][] = '{url}'; + break; + case 'Ip': + $vparams['format'][] = '{ip address}'; + break; + case 'Length': + $messages = array(); + if(isset($constraint->min)) { + $messages[] = "min: {$constraint->min}"; + } + if(isset($constraint->max)) { + $messages[] = "max: {$constraint->max}"; + } + $vparams['format'][] = '{length: ' . join(', ', $messages) . '}'; + break; + case 'Choice': + $format = '[' . join('|', $constraint->choices) . ']'; + if($constraint->multiple) { + $messages = array(); + if(isset($constraint->min)) { + $messages[] = "min: {$constraint->min} "; + } + if(isset($constraint->max)) { + $messages[] = "max: {$constraint->max} "; + } + $vparams['format'][] = '{' . join ('', $messages) . 'choice of ' . $format . '}'; + } else { + $vparams['format'][] = $format; + } + break; + case 'Regex': + if($constraint->match) { + $vparams['format'][] = '{match: ' . $constraint->pattern . '}'; + } else { + $vparams['format'][] = '{not match: ' . $constraint->pattern . '}'; + } + break; + } + + return $vparams; + } +} \ No newline at end of file diff --git a/Resources/config/formatters.xml b/Resources/config/formatters.xml index 53c31be..6e23df1 100644 --- a/Resources/config/formatters.xml +++ b/Resources/config/formatters.xml @@ -5,6 +5,7 @@ Nelmio\ApiDocBundle\Parser\FormTypeParser + Nelmio\ApiDocBundle\Parser\SymfonyValidationParser Nelmio\ApiDocBundle\Formatter\AbstractFormatter Nelmio\ApiDocBundle\Formatter\MarkdownFormatter Nelmio\ApiDocBundle\Formatter\SimpleFormatter @@ -18,6 +19,10 @@ + + + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 2c15d31..cce11c4 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -15,7 +15,7 @@ - + @@ -46,7 +46,6 @@ - diff --git a/Resources/views/method.html.twig b/Resources/views/method.html.twig index 81649a5..ed7c31b 100644 --- a/Resources/views/method.html.twig +++ b/Resources/views/method.html.twig @@ -109,6 +109,7 @@ Parameter Type Required? + Format Description @@ -119,6 +120,7 @@ {{ name }} {{ infos.dataType }} {{ infos.required ? 'true' : 'false' }} + {{ infos.format }} {{ infos.description }} {% endif %} @@ -216,7 +218,7 @@

= - - + -

{% endif %} {% endfor %} diff --git a/Tests/Fixtures/Model/ValidatorTest.php b/Tests/Fixtures/Model/ValidatorTest.php new file mode 100644 index 0000000..46ad6b9 --- /dev/null +++ b/Tests/Fixtures/Model/ValidatorTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Nelmio\ApiDocBundle\Tests\Fixtures\Model; + +use Symfony\Component\Validator\Constraints as Assert; + +class ValidatorTest +{ + /** + * @Assert\Length(min=10); + */ + public $length10; + + /** + * @Assert\Length(min=1, max=10) + */ + public $length1to10; + + /** + * @Assert\NotBlank() + */ + public $notblank; + + /** + * @Assert\NotNull() + */ + public $notnull; + + /** + * @Assert\Type("DateTime"); + */ + public $type; + + /** + * @Assert\Email() + */ + public $email; + + /** + * @Assert\Url() + */ + public $url; + + /** + * @Assert\Ip() + */ + public $ip; + + /** + * @Assert\Choice(choices={"a", "b"}) + */ + public $singlechoice; + + /** + * @Assert\Choice(choices={"x", "y", "z"}, multiple=true) + */ + public $multiplechoice; + + /** + * @Assert\Choice(choices={"foo", "bar", "baz", "qux"}, multiple=true, min=2, max=3) + */ + public $multiplerangechoice; + + /** + * @Assert\Regex(pattern="/^\d{1,4}\w{1,4}$/") + */ + public $regexmatch; + + /** + * @Assert\Regex(pattern="/\d/", match=false) + */ + public $regexnomatch; + + /** + * @Assert\NotNull() + * @Assert\Type("string") + * @Assert\Email() + */ + public $multipleassertions; + + /** + * @Assert\Url() + * @Assert\Length(min=10) + */ + public $multipleformats; +} diff --git a/Tests/Parser/SymfonyValidationHandlerTest.php b/Tests/Parser/SymfonyValidationHandlerTest.php new file mode 100644 index 0000000..4deb76b --- /dev/null +++ b/Tests/Parser/SymfonyValidationHandlerTest.php @@ -0,0 +1,128 @@ +getContainer(); + $factory = $container->get('validator.mapping.class_metadata_factory'); + + $this->handler = new SymfonyValidationHandler($factory); + } + + /** + * @dataProvider dataTestHandler + */ + public function testHandler($property, $expected) + { + $result = $this->handler->handle('Nelmio\ApiDocBundle\Tests\Fixtures\Model\ValidatorTest', $property, array()); + + $this->assertEquals($expected, $result); + } + + + public function dataTestHandler() + { + return array( + array( + 'property' => 'length10', + 'expected' => array( + 'format' => '{length: min: 10}' + ) + ), + array( + 'property' => 'length1to10', + 'expected' => array( + 'format' => '{length: min: 1, max: 10}' + ) + ), + array( + 'property' => 'notblank', + 'expected' => array( + 'required' => true + ) + ), + array( + 'property' => 'notnull', + 'expected' => array( + 'required' => true + ) + ), + array( + 'property' => 'type', + 'expected' => array( + 'dataType' => 'DateTime' + ) + ), + array( + 'property' => 'email', + 'expected' => array( + 'format' => '{email address}' + ) + ), + array( + 'property' => 'url', + 'expected' => array( + 'format' => '{url}' + ) + ), + array( + 'property' => 'ip', + 'expected' => array( + 'format' => '{ip address}' + ) + ), + array( + 'property' => 'singlechoice', + 'expected' => array( + 'format' => '[a|b]' + ) + ), + array( + 'property' => 'multiplechoice', + 'expected' => array( + 'format' => '{choice of [x|y|z]}' + ) + ), + array( + 'property' => 'multiplerangechoice', + 'expected' => array( + 'format' => '{min: 2 max: 3 choice of [foo|bar|baz|qux]}' + ) + ), + array( + 'property' => 'regexmatch', + 'expected' => array( + 'format' => '{match: /^\d{1,4}\w{1,4}$/}' + ) + ), + array( + 'property' => 'regexnomatch', + 'expected' => array( + 'format' => '{not match: /\d/}' + ) + ), + array( + 'property' => 'multipleassertions', + 'expected' => array( + 'required' => true, + 'dataType' => 'string', + 'format' => '{email address}' + ) + ), + array( + 'property' => 'multipleformats', + 'expected' => array( + 'format' => '{url}, {length: min: 10}' + ) + ) + ); + } +} From 5e1549a29db6b45c78c7e2315f20700ba881e29c Mon Sep 17 00:00:00 2001 From: Josh Hall-Bachner Date: Sun, 30 Jun 2013 18:46:00 -0700 Subject: [PATCH 2/4] Built parse-merging into the ApiDocExtractor. Wired up a "post-parse" pass to allow recursive parsing across multiple parsers. --- Extractor/ApiDocExtractor.php | 51 +++++++++++++++++-- Formatter/AbstractFormatter.php | 2 +- Parser/FormTypeParser.php | 3 +- Parser/JmsMetadataParser.php | 4 ++ ...idationParser.php => ValidationParser.php} | 41 +++++++++++---- Resources/config/formatters.xml | 4 +- 6 files changed, 89 insertions(+), 16 deletions(-) rename Parser/{SymfonyValidationParser.php => ValidationParser.php} (72%) diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index 720a593..4ba5a37 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -268,10 +268,17 @@ class ApiDocExtractor $normalizedInput = $this->normalizeClassParameter($input); + $parameters = array(); foreach ($this->parsers as $parser) { if ($parser->supports($normalizedInput)) { - $parameters = $parser->parse($normalizedInput); - break; + $parameters = $this->mergeParameters($parameters, $parser->parse($normalizedInput)); + } + } + + foreach($this->parsers as $parser) { + if($parser->supports($normalizedInput) && method_exists($parser, 'postParse')) { + $mp = $parser->postParse($normalizedInput, $parameters); + $parameters = $this->mergeParameters($parameters, $mp); } } @@ -294,7 +301,6 @@ class ApiDocExtractor foreach ($this->parsers as $parser) { if ($parser->supports($normalizedOutput)) { $response = $parser->parse($normalizedOutput); - break; } } @@ -374,6 +380,45 @@ class ApiDocExtractor return array_merge($defaults, $input); } + protected function mergeParameters($p1, $p2) + { + $params = array(); + + foreach($p2 as $propname => $propvalue) { + if(!isset($p1[$propname])) { + $params[$propname] = $propvalue; + } else { + $v1 = $p1[$propname]; + + foreach($propvalue as $name => $value) { + if(is_array($value)) { + if(isset($v1[$name]) && is_array($v1[$name])) { + $v1[$name] = $this->mergeParameters($v1[$name], $value); + } else { + $v1[$name] = $value; + } + } elseif(!is_null($value)) { + if(in_array($name, array('required', 'readonly'))) { + $v1[$name] = $v1[$name] || $value; + } elseif($name == 'requirement') { + if(isset($v1[$name])) { + $v1[$name] .= ', ' . $value; + } else { + $v1[$name] = $value; + } + } else { + $v1[$name] = $value; + } + } + } + + $params[$propname] = $v1; + } + } + + return $params; + } + /** * Parses annotations for a given method, and adds new information to the given ApiDoc * annotation. Useful to extract information from the FOSRestBundle annotations. diff --git a/Formatter/AbstractFormatter.php b/Formatter/AbstractFormatter.php index f521a4a..ca52fdd 100644 --- a/Formatter/AbstractFormatter.php +++ b/Formatter/AbstractFormatter.php @@ -69,10 +69,10 @@ abstract class AbstractFormatter implements FormatterInterface $newName = $this->getNewName($name, $info, $parentName); $newParams[$newName] = array( - 'description' => $info['description'], 'dataType' => $info['dataType'], 'readonly' => $info['readonly'], 'required' => $info['required'], + 'description' => array_key_exists('description', $info) ? $info['description'] : null, 'format' => array_key_exists('format', $info) ? $info['format'] : null, 'sinceVersion' => array_key_exists('sinceVersion', $info) ? $info['sinceVersion'] : null, 'untilVersion' => array_key_exists('untilVersion', $info) ? $info['untilVersion'] : null, diff --git a/Parser/FormTypeParser.php b/Parser/FormTypeParser.php index f55f913..bf4d4ca 100644 --- a/Parser/FormTypeParser.php +++ b/Parser/FormTypeParser.php @@ -87,7 +87,6 @@ class FormTypeParser implements ParserInterface private function parseForm($form, $prefix = null) { - $className = get_class($form); $parameters = array(); foreach ($form as $name => $child) { $config = $child->getConfig(); @@ -133,6 +132,8 @@ class FormTypeParser implements ParserInterface 'description' => $config->getAttribute('description'), 'readonly' => $config->getDisabled(), ); + } else { + $parameters[$name]['class'] = $type; } continue; diff --git a/Parser/JmsMetadataParser.php b/Parser/JmsMetadataParser.php index 8286e20..1b3d149 100644 --- a/Parser/JmsMetadataParser.php +++ b/Parser/JmsMetadataParser.php @@ -126,6 +126,10 @@ class JmsMetadataParser implements ParserInterface 'untilVersion' => $item->untilVersion, ); + if(!is_null($dataType['class'])) { + $params[$name]['class'] = $dataType['class']; + } + // if class already parsed, continue, to avoid infinite recursion if (in_array($dataType['class'], $visited)) { continue; diff --git a/Parser/SymfonyValidationParser.php b/Parser/ValidationParser.php similarity index 72% rename from Parser/SymfonyValidationParser.php rename to Parser/ValidationParser.php index f1294cf..1385cd4 100644 --- a/Parser/SymfonyValidationParser.php +++ b/Parser/ValidationParser.php @@ -6,7 +6,7 @@ use Nelmio\ApiDocBundle\Parser\ParserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Constraint; -class SymfonyValidationParser implements ParserInterface +class ValidationParser implements ParserInterface { /** * @var \Symfony\Component\Validator\MetadataFactoryInterface @@ -33,26 +33,49 @@ class SymfonyValidationParser implements ParserInterface */ public function parse(array $input) { - $vparams = array(); + $params = array(); $className = $input['class']; $classdata = $this->factory->getMetadataFor($className); + $properties = $classdata->getConstrainedProperties(); - if($classdata->hasPropertyMetadata($name)) { - $propdata = $classdata->getPropertyMetadata($name); - $propdata = reset($propdata); - $constraints = $propdata->getConstraints(); + foreach($properties as $property) { + $vparams = array(); + $pds = $classdata->getPropertyMetadata($property); + foreach($pds as $propdata) { + $constraints = $propdata->getConstraints(); - foreach($constraints as $constraint) { - $vparams = $this->parseConstraint($constraint, $vparams); + foreach($constraints as $constraint) { + $vparams = $this->parseConstraint($constraint, $vparams); + } } if(isset($vparams['format'])) { $vparams['format'] = join(', ', $vparams['format']); } + + foreach(array('dataType', 'readonly', 'required') as $reqprop) { + if(!isset($vparams[$reqprop])) { + $vparams[$reqprop] = null; + } + } + + $params[$property] = $vparams; } - return $vparams; + return $params; + } + + public function postParse(array $input, $parameters) + { + foreach($parameters as $param => $data) { + if(isset($data['class']) && isset($data['children'])) { + $input = array('class' => $data['class']); + $parameters[$param]['children'] = $this->parse($input, $parameters[$param]['children']); + } + } + + return $parameters; } protected function parseConstraint(Constraint $constraint, $vparams) diff --git a/Resources/config/formatters.xml b/Resources/config/formatters.xml index 6e23df1..b9a9610 100644 --- a/Resources/config/formatters.xml +++ b/Resources/config/formatters.xml @@ -5,7 +5,7 @@ Nelmio\ApiDocBundle\Parser\FormTypeParser - Nelmio\ApiDocBundle\Parser\SymfonyValidationParser + Nelmio\ApiDocBundle\Parser\ValidationParser Nelmio\ApiDocBundle\Formatter\AbstractFormatter Nelmio\ApiDocBundle\Formatter\MarkdownFormatter Nelmio\ApiDocBundle\Formatter\SimpleFormatter @@ -19,7 +19,7 @@ - + From 23f64b84f61c553de18b33ecb6eae58f3f00ca58 Mon Sep 17 00:00:00 2001 From: Josh Hall-Bachner Date: Sun, 30 Jun 2013 21:28:40 -0700 Subject: [PATCH 3/4] Fixed multi-level validation nesting. Removed "class" parameters from results after processed. Updated README. --- Extractor/ApiDocExtractor.php | 16 +++++++++++++++- Parser/ValidationParser.php | 7 ++++++- README.md | 5 +++-- ...andlerTest.php => ValidationParserTest.php} | 18 ++++++++++-------- 4 files changed, 34 insertions(+), 12 deletions(-) rename Tests/Parser/{SymfonyValidationHandlerTest.php => ValidationParserTest.php} (86%) diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index 4ba5a37..da7cead 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -282,6 +282,8 @@ class ApiDocExtractor } } + $parameters = $this->clearClasses($parameters); + if ('PUT' === $method) { // All parameters are optional with PUT (update) array_walk($parameters, function($val, $key) use (&$data) { @@ -303,6 +305,7 @@ class ApiDocExtractor $response = $parser->parse($normalizedOutput); } } + $response = $this->clearClasses($response); $annotation->setResponse($response); } @@ -382,7 +385,7 @@ class ApiDocExtractor protected function mergeParameters($p1, $p2) { - $params = array(); + $params = $p1; foreach($p2 as $propname => $propvalue) { if(!isset($p1[$propname])) { @@ -434,4 +437,15 @@ class ApiDocExtractor $handler->handle($annotation, $annots, $route, $method); } } + + protected function clearClasses($array) + { + if(is_array($array)) { + unset($array['class']); + foreach($array as $name => $item) { + $array[$name] = $this->clearClasses($item); + } + } + return $array; + } } diff --git a/Parser/ValidationParser.php b/Parser/ValidationParser.php index 1385cd4..0cb4399 100644 --- a/Parser/ValidationParser.php +++ b/Parser/ValidationParser.php @@ -71,7 +71,12 @@ class ValidationParser implements ParserInterface foreach($parameters as $param => $data) { if(isset($data['class']) && isset($data['children'])) { $input = array('class' => $data['class']); - $parameters[$param]['children'] = $this->parse($input, $parameters[$param]['children']); + $parameters[$param]['children'] = array_merge( + $parameters[$param]['children'], $this->postParse($input, $parameters[$param]['children']) + ); + $parameters[$param]['children'] = array_merge( + $parameters[$param]['children'], $this->parse($input, $parameters[$param]['children']) + ); } } diff --git a/README.md b/README.md index ed38e28..b31a0e8 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,9 @@ The following properties are available: * `filters`: an array of filters; -* `input`: the input type associated to the method, currently this supports Form Types, and classes with JMS Serializer - metadata, useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container). +* `input`: the input type associated to the method (currently this supports Form Types, classes with JMS Serializer + metadata, and classes with Validation component metadata) useful for POST|PUT methods, either as FQCN or as form type + (if it is registered in the form factory in the container). * `output`: the output type associated with the response. Specified and parsed the same way as `input`. diff --git a/Tests/Parser/SymfonyValidationHandlerTest.php b/Tests/Parser/ValidationParserTest.php similarity index 86% rename from Tests/Parser/SymfonyValidationHandlerTest.php rename to Tests/Parser/ValidationParserTest.php index 4deb76b..4e5c256 100644 --- a/Tests/Parser/SymfonyValidationHandlerTest.php +++ b/Tests/Parser/ValidationParserTest.php @@ -2,10 +2,10 @@ namespace NelmioApiDocBundle\Tests\Parser; use Nelmio\ApiDocBundle\Tests\WebTestCase; -use Nelmio\ApiDocBundle\Parser\Handler\SymfonyValidationHandler; +use Nelmio\ApiDocBundle\Parser\ValidationParser; -class SymfonyValidationHandlerTest extends WebTestCase +class ValidationParserTest extends WebTestCase { protected $handler; @@ -14,21 +14,23 @@ class SymfonyValidationHandlerTest extends WebTestCase $container = $this->getContainer(); $factory = $container->get('validator.mapping.class_metadata_factory'); - $this->handler = new SymfonyValidationHandler($factory); + $this->parser = new ValidationParser($factory); } /** - * @dataProvider dataTestHandler + * @dataProvider dataTestParser */ - public function testHandler($property, $expected) + public function testParser($property, $expected) { - $result = $this->handler->handle('Nelmio\ApiDocBundle\Tests\Fixtures\Model\ValidatorTest', $property, array()); + $result = $this->parser->parse(array('class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\ValidatorTest')); - $this->assertEquals($expected, $result); + foreach($expected as $name => $value) { + $this->assertEquals($value, $expected[$name]); + } } - public function dataTestHandler() + public function dataTestParser() { return array( array( From 54a6ad566dbc178aa8ebe863cb9cc0d882c405e9 Mon Sep 17 00:00:00 2001 From: Josh Hall-Bachner Date: Tue, 2 Jul 2013 21:57:09 -0700 Subject: [PATCH 4/4] Updated postParse logic to utilize an interface and to avoid unnecessary "supports" checks. Expanded documentation on new classes and methods. --- Extractor/ApiDocExtractor.php | 27 ++++++++++++++++-- Parser/ParserInterface.php | 2 ++ Parser/PostParserInterface.php | 38 +++++++++++++++++++++++++ Parser/ValidationParser.php | 51 ++++++++++++++++++++++++++++------ 4 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 Parser/PostParserInterface.php diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index da7cead..7277732 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Extractor; use Doctrine\Common\Annotations\Reader; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Parser\ParserInterface; +use Nelmio\ApiDocBundle\Parser\PostParserInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -268,15 +269,17 @@ class ApiDocExtractor $normalizedInput = $this->normalizeClassParameter($input); + $supportedParsers = array(); $parameters = array(); foreach ($this->parsers as $parser) { if ($parser->supports($normalizedInput)) { + $supportedParsers[] = $parser; $parameters = $this->mergeParameters($parameters, $parser->parse($normalizedInput)); } } - foreach($this->parsers as $parser) { - if($parser->supports($normalizedInput) && method_exists($parser, 'postParse')) { + foreach($supportedParsers as $parser) { + if($parser instanceof PostParserInterface) { $mp = $parser->postParse($normalizedInput, $parameters); $parameters = $this->mergeParameters($parameters, $mp); } @@ -383,6 +386,18 @@ class ApiDocExtractor return array_merge($defaults, $input); } + /** + * Merges two parameter arrays together. This logic allows documentation to be built + * from multiple parser passes, with later passes extending previous passes: + * - Boolean values are true if any parser returns true. + * - Requirement parameters are concatenated. + * - Other string values are overridden by later parsers when present. + * - Array parameters are recursively merged. + * + * @param array $p1 The pre-existing parameters array. + * @param array $p2 The newly-returned parameters array. + * @return array The resulting, merged array. + */ protected function mergeParameters($p1, $p2) { $params = $p1; @@ -403,7 +418,7 @@ class ApiDocExtractor } elseif(!is_null($value)) { if(in_array($name, array('required', 'readonly'))) { $v1[$name] = $v1[$name] || $value; - } elseif($name == 'requirement') { + } elseif(in_array($name, array('requirement'))) { if(isset($v1[$name])) { $v1[$name] .= ', ' . $value; } else { @@ -438,6 +453,12 @@ class ApiDocExtractor } } + /** + * Clears the temporary 'class' parameter from the parameters array before it is returned. + * + * @param array $array The source array. + * @return array The cleared array. + */ protected function clearClasses($array) { if(is_array($array)) { diff --git a/Parser/ParserInterface.php b/Parser/ParserInterface.php index 398b230..3710bcf 100644 --- a/Parser/ParserInterface.php +++ b/Parser/ParserInterface.php @@ -33,6 +33,8 @@ interface ParserInterface * - readonly boolean * - children (optional) array of nested property names mapped to arrays * in the format described here + * - class (optional) the fully-qualified class name of the item, if + * it is represented by an object * * @param string $item The string type of input to parse. * @return array diff --git a/Parser/PostParserInterface.php b/Parser/PostParserInterface.php new file mode 100644 index 0000000..51832a7 --- /dev/null +++ b/Parser/PostParserInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Nelmio\ApiDocBundle\Parser; + +/** + * This is the interface parsers must implement in order to register a second parsing pass after the initial structure + * is populated.. + */ +interface PostParserInterface +{ + /** + * Reparses an object for additional documentation details after it has already been parsed once, to allow + * parsers to extend information initially documented by other parsers. + * + * Returns an array of class property metadata where each item is a key (the property name) and + * an array of data with the following keys: + * - dataType string + * - required boolean + * - description string + * - readonly boolean + * - children (optional) array of nested property names mapped to arrays + * in the format described here + * + * @param string $item The string type of input to parse. + * @param array $parameters The previously-parsed parameters array. + * @return array + */ + public function postParse(array $item, array $parameters); +} diff --git a/Parser/ValidationParser.php b/Parser/ValidationParser.php index 0cb4399..363a663 100644 --- a/Parser/ValidationParser.php +++ b/Parser/ValidationParser.php @@ -1,18 +1,34 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Nelmio\ApiDocBundle\Parser; -use Nelmio\ApiDocBundle\Parser\ParserInterface; use Symfony\Component\Validator\MetadataFactoryInterface; use Symfony\Component\Validator\Constraint; -class ValidationParser implements ParserInterface +/** + * Uses the Symfony Validation component to extract information about API objects. + */ +class ValidationParser implements ParserInterface, PostParserInterface { /** * @var \Symfony\Component\Validator\MetadataFactoryInterface */ protected $factory; + /** + * Requires a validation MetadataFactory. + * + * @param MetadataFactoryInterface $factory + */ public function __construct(MetadataFactoryInterface $factory) { $this->factory = $factory; @@ -66,7 +82,10 @@ class ValidationParser implements ParserInterface return $params; } - public function postParse(array $input, $parameters) + /** + * {@inheritDoc} + */ + public function postParse(array $input, array $parameters) { foreach($parameters as $param => $data) { if(isset($data['class']) && isset($data['children'])) { @@ -83,6 +102,22 @@ class ValidationParser implements ParserInterface return $parameters; } + /** + * Create a valid documentation parameter based on an individual validation Constraint. + * Currently supports: + * - NotBlank/NotNull + * - Type + * - Email + * - Url + * - Ip + * - Length (min and max) + * - Choice (single and multiple, min and max) + * - Regex (match and non-match) + * + * @param Constraint $constraint The constraint metadata object. + * @param array $vparams The existing validation parameters. + * @return mixed The parsed list of validation parameters. + */ protected function parseConstraint(Constraint $constraint, $vparams) { $class = substr(get_class($constraint), strlen('Symfony\\Component\\Validator\\Constraints\\')); @@ -130,11 +165,11 @@ class ValidationParser implements ParserInterface } break; case 'Regex': - if($constraint->match) { - $vparams['format'][] = '{match: ' . $constraint->pattern . '}'; - } else { - $vparams['format'][] = '{not match: ' . $constraint->pattern . '}'; - } + if($constraint->match) { + $vparams['format'][] = '{match: ' . $constraint->pattern . '}'; + } else { + $vparams['format'][] = '{not match: ' . $constraint->pattern . '}'; + } break; }