added support for JMS Serializer GroupsExclusionStrategy

This commit is contained in:
fieg 2013-03-25 12:28:00 +01:00 committed by Pierre-Yves LEBECQ
parent 806e009391
commit 06271f824a
8 changed files with 405 additions and 64 deletions

View File

@ -266,9 +266,11 @@ class ApiDocExtractor
if (null !== $input = $annotation->getInput()) { if (null !== $input = $annotation->getInput()) {
$parameters = array(); $parameters = array();
$normalizedInput = $this->normalizeClassParameter($input);
foreach ($this->parsers as $parser) { foreach ($this->parsers as $parser) {
if ($parser->supports($input)) { if ($parser->supports($normalizedInput)) {
$parameters = $parser->parse($input); $parameters = $parser->parse($normalizedInput);
break; break;
} }
} }
@ -287,9 +289,11 @@ class ApiDocExtractor
if (null !== $output = $annotation->getOutput()) { if (null !== $output = $annotation->getOutput()) {
$response = array(); $response = array();
$normalizedOutput = $this->normalizeClassParameter($output);
foreach ($this->parsers as $parser) { foreach ($this->parsers as $parser) {
if ($parser->supports($output)) { if ($parser->supports($normalizedOutput)) {
$response = $parser->parse($output); $response = $parser->parse($normalizedOutput);
break; break;
} }
} }
@ -350,6 +354,27 @@ class ApiDocExtractor
return $annotation; 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 * Parses annotations for a given method, and adds new information to the given ApiDoc
* annotation. Useful to extract information from the FOSRestBundle annotations. * annotation. Useful to extract information from the FOSRestBundle annotations.

View File

@ -47,10 +47,12 @@ class FormTypeParser implements ParserInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function supports($item) public function supports(array $item)
{ {
$className = $item['class'];
try { try {
if ($this->createForm($item)) { if ($this->createForm($className)) {
return true; return true;
} }
} catch (FormException $e) { } catch (FormException $e) {
@ -65,8 +67,10 @@ class FormTypeParser implements ParserInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function parse($type) public function parse(array $item)
{ {
$type = $item['class'];
if ($this->implementsType($type)) { if ($this->implementsType($type)) {
$type = $this->getTypeInstance($type); $type = $this->getTypeInstance($type);
} }

View File

@ -11,6 +11,10 @@
namespace Nelmio\ApiDocBundle\Parser; 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 Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor; use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Metadata\PropertyMetadata;
@ -22,7 +26,6 @@ use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
*/ */
class JmsMetadataParser implements ParserInterface class JmsMetadataParser implements ParserInterface
{ {
/** /**
* @var \Metadata\MetadataFactoryInterface * @var \Metadata\MetadataFactoryInterface
*/ */
@ -54,10 +57,12 @@ class JmsMetadataParser implements ParserInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function supports($input) public function supports(array $input)
{ {
$className = $input['class'];
try { try {
if ($meta = $this->factory->getMetadataForClass($input)) { if ($meta = $this->factory->getMetadataForClass($className)) {
return true; return true;
} }
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
@ -69,9 +74,12 @@ class JmsMetadataParser implements ParserInterface
/** /**
* {@inheritdoc} * {@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 string $className Class to get all metadata for
* @param array $visited Classes we've already visited to prevent infinite recursion. * @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 * @return array metadata for given class
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
protected function doParse($className, $visited = array()) protected function doParse($className, $visited = array(), array $groups = array())
{ {
$meta = $this->factory->getMetadataForClass($className); $meta = $this->factory->getMetadataForClass($className);
@ -90,6 +99,9 @@ class JmsMetadataParser implements ParserInterface
throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className)); throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className));
} }
$exclusionStrategies = array();
$exclusionStrategies[] = new GroupsExclusionStrategy($groups);
$params = array(); $params = array();
// iterate over property metadata // iterate over property metadata
@ -99,11 +111,19 @@ class JmsMetadataParser implements ParserInterface
$dataType = $this->processDataType($item); $dataType = $this->processDataType($item);
// apply exclusion strategies
foreach ($exclusionStrategies as $strategy) {
if (true === $strategy->shouldSkipProperty($item, SerializationContext::create())) {
continue 2;
}
}
$params[$name] = array( $params[$name] = array(
'dataType' => $dataType['normalized'], '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 'required' => false,
'description' => $this->getDescription($className, $item), //TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
'readonly' => $item->readOnly 'description' => $this->getDescription($className, $item),
'readonly' => $item->readOnly
); );
// if class already parsed, continue, to avoid infinite recursion // if class already parsed, continue, to avoid infinite recursion
@ -113,8 +133,8 @@ class JmsMetadataParser implements ParserInterface
// check for nested classes with JMS metadata // check for nested classes with JMS metadata
if ($dataType['class'] && null !== $this->factory->getMetadataForClass($dataType['class'])) { if ($dataType['class'] && null !== $this->factory->getMetadataForClass($dataType['class'])) {
$visited[] = $dataType['class']; $visited[] = $dataType['class'];
$params[$name]['children'] = $this->doParse($dataType['class'], $visited); $params[$name]['children'] = $this->doParse($dataType['class'], $visited, $groups);
} }
} }
} }
@ -206,5 +226,4 @@ class JmsMetadataParser implements ParserInterface
return $extracted; return $extracted;
} }
} }

View File

@ -19,10 +19,10 @@ interface ParserInterface
/** /**
* Return true/false whether this class supports parsing the given class. * 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 * @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 * 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. * @param string $item The string type of input to parse.
* @return array * @return array
*/ */
public function parse($item); public function parse(array $item);
} }

View File

@ -105,7 +105,28 @@ The following properties are available:
* `filters`: an array of filters; * `filters`: an array of filters;
* `input`: the input type associated to the method, currently this supports Form Types, and classes with JMS Serializer * `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`. * `output`: the output type associated with the response. Specified and parsed the same way as `input`.

View File

@ -12,23 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
// get the autoload file // get the autoload file
$dir = __DIR__; require_once __DIR__.'/../../../vendor/autoload.php';
$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);
}
use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Kernel;

View File

@ -5,6 +5,7 @@ use Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested;
use Nelmio\ApiDocBundle\Parser\JmsMetadataParser; use Nelmio\ApiDocBundle\Parser\JmsMetadataParser;
use JMS\Serializer\Metadata\ClassMetadata; use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Naming\CamelCaseNamingStrategy;
class JmsMetadataParserTest extends \PHPUnit_Framework_TestCase class JmsMetadataParserTest extends \PHPUnit_Framework_TestCase
{ {
@ -61,7 +62,7 @@ class JmsMetadataParserTest extends \PHPUnit_Framework_TestCase
->method('translateName') ->method('translateName')
->will($this->returnValue('baz')); ->will($this->returnValue('baz'));
$input = new JmsNested(); $input = 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested';
$metadataFactory->expects($this->once()) $metadataFactory->expects($this->once())
->method('getMetadataForClass') ->method('getMetadataForClass')
@ -70,28 +71,315 @@ class JmsMetadataParserTest extends \PHPUnit_Framework_TestCase
$jmsMetadataParser = new JmsMetadataParser($metadataFactory, $propertyNamingStrategy, $docCommentExtractor); $jmsMetadataParser = new JmsMetadataParser($metadataFactory, $propertyNamingStrategy, $docCommentExtractor);
$output = $jmsMetadataParser->parse($input); $output = $jmsMetadataParser->parse(
array(
$this->assertEquals(array( 'class' => $input,
'foo' => array( 'groups' => array(),
'dataType' => 'DateTime', 'version' => null,
'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); );
$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() public function dataTestParserWithNestedType()

View File

@ -21,6 +21,7 @@
"dflydev/markdown": "1.0.*" "dflydev/markdown": "1.0.*"
}, },
"conflict": { "conflict": {
"jms/serializer": "<0.12",
"jms/serializer-bundle": "<0.11" "jms/serializer-bundle": "<0.11"
}, },
"require-dev": { "require-dev": {