From 06271f824adbd6eaa429f716a4f8c5c8adfcb5d0 Mon Sep 17 00:00:00 2001 From: fieg Date: Mon, 25 Mar 2013 12:28:00 +0100 Subject: [PATCH] added support for JMS Serializer GroupsExclusionStrategy --- Extractor/ApiDocExtractor.php | 33 ++- Parser/FormTypeParser.php | 10 +- Parser/JmsMetadataParser.php | 45 +++- Parser/ParserInterface.php | 7 +- README.md | 23 +- Tests/Fixtures/app/AppKernel.php | 18 +- Tests/Parser/JmsMetadataParserTest.php | 332 +++++++++++++++++++++++-- composer.json | 1 + 8 files changed, 405 insertions(+), 64 deletions(-) diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index b9077e2..481e071 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -266,9 +266,11 @@ class ApiDocExtractor if (null !== $input = $annotation->getInput()) { $parameters = array(); + $normalizedInput = $this->normalizeClassParameter($input); + foreach ($this->parsers as $parser) { - if ($parser->supports($input)) { - $parameters = $parser->parse($input); + if ($parser->supports($normalizedInput)) { + $parameters = $parser->parse($normalizedInput); break; } } @@ -287,9 +289,11 @@ class ApiDocExtractor if (null !== $output = $annotation->getOutput()) { $response = array(); + $normalizedOutput = $this->normalizeClassParameter($output); + foreach ($this->parsers as $parser) { - if ($parser->supports($output)) { - $response = $parser->parse($output); + if ($parser->supports($normalizedOutput)) { + $response = $parser->parse($normalizedOutput); break; } } @@ -350,6 +354,27 @@ class ApiDocExtractor return $annotation; } + protected function normalizeClassParameter($input) + { + $defaults = array( + 'class' => '', + 'groups' => array(), + 'version' => null, + ); + + // normalize strings + if (is_string($input)) { + $input = array('class' => $input); + } + + // normalize groups + if (isset($input['groups']) && is_string($input['groups'])) { + $input['groups'] = array_map('trim', explode(',', $input['groups'])); + } + + return array_merge($defaults, $input); + } + /** * 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/Parser/FormTypeParser.php b/Parser/FormTypeParser.php index e0f0a60..f04a2d9 100644 --- a/Parser/FormTypeParser.php +++ b/Parser/FormTypeParser.php @@ -47,10 +47,12 @@ class FormTypeParser implements ParserInterface /** * {@inheritdoc} */ - public function supports($item) + public function supports(array $item) { + $className = $item['class']; + try { - if ($this->createForm($item)) { + if ($this->createForm($className)) { return true; } } catch (FormException $e) { @@ -65,8 +67,10 @@ class FormTypeParser implements ParserInterface /** * {@inheritdoc} */ - public function parse($type) + public function parse(array $item) { + $type = $item['class']; + if ($this->implementsType($type)) { $type = $this->getTypeInstance($type); } diff --git a/Parser/JmsMetadataParser.php b/Parser/JmsMetadataParser.php index 5ac634a..0882d0e 100644 --- a/Parser/JmsMetadataParser.php +++ b/Parser/JmsMetadataParser.php @@ -11,6 +11,10 @@ namespace Nelmio\ApiDocBundle\Parser; +use JMS\Serializer\Exclusion\GroupsExclusionStrategy; +use JMS\Serializer\GraphNavigator; +use JMS\Serializer\NavigatorContext; +use JMS\Serializer\SerializationContext; use Metadata\MetadataFactoryInterface; use Nelmio\ApiDocBundle\Util\DocCommentExtractor; use JMS\Serializer\Metadata\PropertyMetadata; @@ -22,7 +26,6 @@ use JMS\Serializer\Naming\PropertyNamingStrategyInterface; */ class JmsMetadataParser implements ParserInterface { - /** * @var \Metadata\MetadataFactoryInterface */ @@ -54,10 +57,12 @@ class JmsMetadataParser implements ParserInterface /** * {@inheritdoc} */ - public function supports($input) + public function supports(array $input) { + $className = $input['class']; + try { - if ($meta = $this->factory->getMetadataForClass($input)) { + if ($meta = $this->factory->getMetadataForClass($className)) { return true; } } catch (\ReflectionException $e) { @@ -69,9 +74,12 @@ class JmsMetadataParser implements ParserInterface /** * {@inheritdoc} */ - public function parse($input) + public function parse(array $input) { - return $this->doParse($input); + $className = $input['class']; + $groups = $input['groups']; + + return $this->doParse($className, array(), $groups, $version); } /** @@ -79,10 +87,11 @@ class JmsMetadataParser implements ParserInterface * * @param string $className Class to get all metadata for * @param array $visited Classes we've already visited to prevent infinite recursion. + * @param array $groups Groups to be used in the group exclusion strategy * @return array metadata for given class * @throws \InvalidArgumentException */ - protected function doParse($className, $visited = array()) + protected function doParse($className, $visited = array(), array $groups = array()) { $meta = $this->factory->getMetadataForClass($className); @@ -90,6 +99,9 @@ class JmsMetadataParser implements ParserInterface throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className)); } + $exclusionStrategies = array(); + $exclusionStrategies[] = new GroupsExclusionStrategy($groups); + $params = array(); // iterate over property metadata @@ -99,11 +111,19 @@ class JmsMetadataParser implements ParserInterface $dataType = $this->processDataType($item); + // apply exclusion strategies + foreach ($exclusionStrategies as $strategy) { + if (true === $strategy->shouldSkipProperty($item, SerializationContext::create())) { + continue 2; + } + } + $params[$name] = array( - 'dataType' => $dataType['normalized'], - 'required' => false, //TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this - 'description' => $this->getDescription($className, $item), - 'readonly' => $item->readOnly + 'dataType' => $dataType['normalized'], + 'required' => false, + //TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this + 'description' => $this->getDescription($className, $item), + 'readonly' => $item->readOnly ); // if class already parsed, continue, to avoid infinite recursion @@ -113,8 +133,8 @@ class JmsMetadataParser implements ParserInterface // check for nested classes with JMS metadata if ($dataType['class'] && null !== $this->factory->getMetadataForClass($dataType['class'])) { - $visited[] = $dataType['class']; - $params[$name]['children'] = $this->doParse($dataType['class'], $visited); + $visited[] = $dataType['class']; + $params[$name]['children'] = $this->doParse($dataType['class'], $visited, $groups); } } } @@ -206,5 +226,4 @@ class JmsMetadataParser implements ParserInterface return $extracted; } - } diff --git a/Parser/ParserInterface.php b/Parser/ParserInterface.php index fcdadf2..398b230 100644 --- a/Parser/ParserInterface.php +++ b/Parser/ParserInterface.php @@ -19,10 +19,10 @@ interface ParserInterface /** * Return true/false whether this class supports parsing the given class. * - * @param string $item The string type of input to parse. + * @param array $item containing the following fields: class, groups. Of which groups is optional * @return boolean */ - public function supports($item); + public function supports(array $item); /** * Returns an array of class property metadata where each item is a key (the property name) and @@ -37,6 +37,5 @@ interface ParserInterface * @param string $item The string type of input to parse. * @return array */ - public function parse($item); - + public function parse(array $item); } diff --git a/README.md b/README.md index 897fc2b..4a76bc1 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,28 @@ 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) + metadata, useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container). + When using a class with JMS Serializer metadata, you can + [use specific groups](http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#creating-different-views-of-your-objects) + by using this syntax: + + ``` + input={ + "class"="Acme\Bundle\Entity\User", + "groups"={"update", "public"} + } + ``` + + In this case the groups 'update' and 'public' are used. + + Also supported are versions: + + ``` + input={ + "class"="Acme\Bundle\Entity\User", + "version"="2.3" + } + ``` * `output`: the output type associated with the response. Specified and parsed the same way as `input`. diff --git a/Tests/Fixtures/app/AppKernel.php b/Tests/Fixtures/app/AppKernel.php index 0ebbbb0..adc9506 100644 --- a/Tests/Fixtures/app/AppKernel.php +++ b/Tests/Fixtures/app/AppKernel.php @@ -12,23 +12,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; // get the autoload file -$dir = __DIR__; -$lastDir = null; -while ($dir !== $lastDir) { - $lastDir = $dir; - - if (is_file($dir.'/autoload.php')) { - require_once $dir.'/autoload.php'; - break; - } - - if (is_file($dir.'/autoload.php.dist')) { - require_once $dir.'/autoload.php.dist'; - break; - } - - $dir = dirname($dir); -} +require_once __DIR__.'/../../../vendor/autoload.php'; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\HttpKernel\Kernel; diff --git a/Tests/Parser/JmsMetadataParserTest.php b/Tests/Parser/JmsMetadataParserTest.php index dd30b75..8d4acae 100644 --- a/Tests/Parser/JmsMetadataParserTest.php +++ b/Tests/Parser/JmsMetadataParserTest.php @@ -5,6 +5,7 @@ use Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested; use Nelmio\ApiDocBundle\Parser\JmsMetadataParser; use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Metadata\PropertyMetadata; +use JMS\Serializer\Naming\CamelCaseNamingStrategy; class JmsMetadataParserTest extends \PHPUnit_Framework_TestCase { @@ -61,7 +62,7 @@ class JmsMetadataParserTest extends \PHPUnit_Framework_TestCase ->method('translateName') ->will($this->returnValue('baz')); - $input = new JmsNested(); + $input = 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested'; $metadataFactory->expects($this->once()) ->method('getMetadataForClass') @@ -70,28 +71,315 @@ class JmsMetadataParserTest extends \PHPUnit_Framework_TestCase $jmsMetadataParser = new JmsMetadataParser($metadataFactory, $propertyNamingStrategy, $docCommentExtractor); - $output = $jmsMetadataParser->parse($input); - - $this->assertEquals(array( - 'foo' => array( - 'dataType' => 'DateTime', - 'required' => false, - 'description' => '', - 'readonly' => false - ), - 'bar' => array( - 'dataType' => 'string', - 'required' => false, - 'description' => '', - 'readonly' => false - ), - 'baz' => array( - 'dataType' => 'array of integers', - 'required' => false, - 'description' => '', - 'readonly' => false + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array(), + 'version' => null, ) - ), $output); + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'DateTime', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'bar' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'baz' => array( + 'dataType' => 'array of integers', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ) + ), + $output + ); + } + + public function testParserWithGroups() + { + $metadataFactory = $this->getMock('Metadata\MetadataFactoryInterface'); + $docCommentExtractor = $this->getMockBuilder('Nelmio\ApiDocBundle\Util\DocCommentExtractor') + ->disableOriginalConstructor() + ->getMock(); + + $propertyMetadataFoo = new PropertyMetadata('Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested', 'foo'); + $propertyMetadataFoo->type = array('name' => 'string'); + + $propertyMetadataBar = new PropertyMetadata('Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested', 'bar'); + $propertyMetadataBar->type = array('name' => 'string'); + $propertyMetadataBar->groups = array('Default', 'Special'); + + $propertyMetadataBaz = new PropertyMetadata('Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested', 'baz'); + $propertyMetadataBaz->type = array('name' => 'string'); + $propertyMetadataBaz->groups = array('Special'); + + $input = 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested'; + + $metadata = new ClassMetadata($input); + $metadata->addPropertyMetadata($propertyMetadataFoo); + $metadata->addPropertyMetadata($propertyMetadataBar); + $metadata->addPropertyMetadata($propertyMetadataBaz); + + $metadataFactory->expects($this->any()) + ->method('getMetadataForClass') + ->with($input) + ->will($this->returnValue($metadata)); + + $propertyNamingStrategy = new CamelCaseNamingStrategy(); + + $jmsMetadataParser = new JmsMetadataParser($metadataFactory, $propertyNamingStrategy, $docCommentExtractor); + + // No group specified. + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array(), + 'version' => null, + ) + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'bar' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + ), + $output + ); + + // Default group. + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array('Default'), + 'version' => null, + ) + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'bar' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + ), + $output + ); + + // Special group. + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array('Special'), + 'version' => null, + ) + ); + + $this->assertEquals( + array( + 'bar' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'baz' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + ), + $output + ); + + // Default + Special groups. + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array('Default', 'Special'), + 'version' => null, + ) + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'bar' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'baz' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ) + ), + $output + ); + } + + public function testParserWithVersion() + { + $metadataFactory = $this->getMock('Metadata\MetadataFactoryInterface'); + $docCommentExtractor = $this->getMockBuilder('Nelmio\ApiDocBundle\Util\DocCommentExtractor') + ->disableOriginalConstructor() + ->getMock(); + + $propertyMetadataFoo = new PropertyMetadata('Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested', 'foo'); + $propertyMetadataFoo->type = array('name' => 'string'); + + $propertyMetadataBar = new PropertyMetadata('Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested', 'bar'); + $propertyMetadataBar->type = array('name' => 'string'); + $propertyMetadataBar->sinceVersion = '0.2'; + $propertyMetadataBar->untilVersion = '0.3'; + + $input = 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested'; + + $metadata = new ClassMetadata($input); + $metadata->addPropertyMetadata($propertyMetadataFoo); + $metadata->addPropertyMetadata($propertyMetadataBar); + + $metadataFactory->expects($this->any()) + ->method('getMetadataForClass') + ->with($input) + ->will($this->returnValue($metadata)); + + $propertyNamingStrategy = new CamelCaseNamingStrategy(); + + $jmsMetadataParser = new JmsMetadataParser($metadataFactory, $propertyNamingStrategy, $docCommentExtractor); + + // No version specified. + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array(), + 'version' => null, + ) + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'bar' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + ), + $output + ); + + // 0.1 + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array(), + 'version' => '0.1', + ) + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + ), + $output + ); + + // 0.2 & 0.3 + foreach (array('0.2', '0.3') as $version) { + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array(), + 'version' => $version, + ) + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + 'bar' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + ), + $output + ); + } + + // 0.4 + $output = $jmsMetadataParser->parse( + array( + 'class' => $input, + 'groups' => array(), + 'version' => '0.4', + ) + ); + + $this->assertEquals( + array( + 'foo' => array( + 'dataType' => 'string', + 'required' => false, + 'description' => 'No description.', + 'readonly' => false + ), + ), + $output + ); } public function dataTestParserWithNestedType() diff --git a/composer.json b/composer.json index 7d03918..76b1397 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "dflydev/markdown": "1.0.*" }, "conflict": { + "jms/serializer": "<0.12", "jms/serializer-bundle": "<0.11" }, "require-dev": {