Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feature #18834 [Serializer] Add the possibility to filter attributes …
…(dunglas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Serializer] Add the possibility to filter attributes

| Q | A |
| --- | --- |
| Branch? | master |
| Bug fix? | no |
| New feature? | yes |
| BC breaks? | no |
| Deprecations? | no |
| Tests pass? | yes |
| Fixed tickets | n/a |
| License | MIT |
| Doc PR | todo |

This new features give the possibility to the end user to select select fields to normalize.

Exemple:

``` php
class Foo
{
    public $a = '1';
    public $b = '2';
    public $c = '3';
}

$normalizer = new \Symfony\Component\Serializer\Normalizer\ObjectNormalizer();
$normalizer->normalize(new Foo(), null, ['attributes' => ['a', 'c']]);
// ['a' => '1', 'c' => '3']
```

Denormalization is also supported. This feature works for any normalizer using the `Symfony\Component\Serializer\Normalizer\AbstractNormalizer` class.

The main use case is to permit to API clients to optimize responses length by dropping unnecessary information like in the following example:

```
http://exemple.com/cars?fields=a,c

{'a' => 1, 'c' => 2}
```

Thanks to this PR, support for this feature will be available out of the box in [API Platform](https://api-platform.com).

Commits
-------

b3826fb [Serializer] Add the possibility to filter attributes
  • Loading branch information
fabpot committed Feb 13, 2017
2 parents a0f1e85 + b3826fb commit e43bd57
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 5 deletions.
36 changes: 33 additions & 3 deletions src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
Expand Up @@ -236,7 +236,20 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
*/
protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = array())
{
return !in_array($attribute, $this->ignoredAttributes);
if (in_array($attribute, $this->ignoredAttributes)) {
return false;
}

if (isset($context['attributes'][$attribute])) {
// Nested attributes
return true;
}

if (isset($context['attributes']) && is_array($context['attributes'])) {
return in_array($attribute, $context['attributes'], true);
}

return true;
}

/**
Expand Down Expand Up @@ -324,7 +337,7 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;

$allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes);
$ignored = in_array($paramName, $this->ignoredAttributes);
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) {
if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
if (!is_array($data[$paramName])) {
Expand All @@ -341,7 +354,7 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref
throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $constructorParameter->getClass(), static::class));
}
$parameterClass = $constructorParameter->getClass()->getName();
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $context);
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $paramName));
}
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $key), 0, $e);
Expand Down Expand Up @@ -372,4 +385,21 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref

return new $class();
}

/**
* @param array $parentContext
* @param string $attribute
*
* @return array
*
* @internal
*/
protected function createChildContext(array $parentContext, $attribute)
{
if (isset($parentContext['attributes'][$attribute])) {
$parentContext['attributes'] = $parentContext['attributes'][$attribute];
}

return $parentContext;
}
}
Expand Up @@ -95,7 +95,7 @@ public function normalize($object, $format = null, array $context = array())
throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer', $attribute));
}

$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $context));
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute)));
}

return $data;
Expand Down Expand Up @@ -268,7 +268,7 @@ private function validateAndDenormalize($currentClass, $attribute, $data, $forma
}

if ($this->serializer->supportsDenormalization($data, $class, $format)) {
return $this->serializer->denormalize($data, $class, $format, $context);
return $this->serializer->denormalize($data, $class, $format, $this->createChildContext($context, $attribute));
}
}

Expand Down
Expand Up @@ -643,6 +643,70 @@ public function testExtractAttributesRespectsContext()

$this->assertSame(array('foo' => 'bar', 'bar' => 'foo'), $normalizer->normalize($data, null, array('include_foo_and_bar' => true)));
}

public function testAttributesContextNormalize()
{
$normalizer = new ObjectNormalizer();
$serializer = new Serializer(array($normalizer));

$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectInner->bar = 'innerBar';

$objectDummy = new ObjectDummy();
$objectDummy->setFoo('foo');
$objectDummy->setBaz(true);
$objectDummy->setObject($objectInner);

$context = array('attributes' => array('foo', 'baz', 'object' => array('foo')));
$this->assertEquals(
array(
'foo' => 'foo',
'baz' => true,
'object' => array('foo' => 'innerFoo'),
),
$serializer->normalize($objectDummy, null, $context)
);
}

public function testAttributesContextDenormalize()
{
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$serializer = new Serializer(array($normalizer));

$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';

$objectOuter = new ObjectOuter();
$objectOuter->bar = 'bar';
$objectOuter->setInner($objectInner);

$context = array('attributes' => array('bar', 'inner' => array('foo')));
$this->assertEquals($objectOuter, $serializer->denormalize(
array(
'foo' => 'foo',
'bar' => 'bar',
'date' => '2017-02-03',
'inner' => array('foo' => 'innerFoo', 'bar' => 'innerBar'),
), ObjectOuter::class, null, $context));
}

public function testAttributesContextDenormalizeConstructor()
{
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$serializer = new Serializer(array($normalizer));

$objectInner = new ObjectInner();
$objectInner->bar = 'bar';

$obj = new DummyWithConstructorObjectAndDefaultValue('a', $objectInner);

$context = array('attributes' => array('inner' => array('bar')));
$this->assertEquals($obj, $serializer->denormalize(array(
'foo' => 'b',
'inner' => array('foo' => 'foo', 'bar' => 'bar'),
), DummyWithConstructorObjectAndDefaultValue::class, null, $context));
}
}

class ObjectDummy
Expand Down Expand Up @@ -813,6 +877,8 @@ public function setFoo(array $f)

class ObjectOuter
{
public $foo;
public $bar;
private $inner;
private $date;

Expand Down Expand Up @@ -910,3 +976,25 @@ class JsonNumber
*/
public $number;
}

class DummyWithConstructorObjectAndDefaultValue
{
private $foo;
private $inner;

public function __construct($foo = 'a', ObjectInner $inner)
{
$this->foo = $foo;
$this->inner = $inner;
}

public function getFoo()
{
return $this->foo;
}

public function getInner()
{
return $this->inner;
}
}

0 comments on commit e43bd57

Please sign in to comment.