feature #28744 [Serializer] Add an @Ignore annotation (dunglas)

This PR was squashed before being merged into the 5.1-dev branch.

Discussion
----------

[Serializer] Add an @Ignore annotation

| Q             | A
| ------------- | ---
| Branch?       | master
 Bug fix?      | no
| New feature?  | yes <!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no     <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass?   | yes    <!-- please add some, will be required by reviewers -->
| Fixed tickets | #24071
| License       | MIT
| Doc PR        | n/a

Add an `@Ignore` annotation to configure [ignored attributes](https://symfony.com/doc/current/components/serializer.html#ignoring-attributes) in a convenient way, as well as the related XML and YAML loaders.

TODO:

* [x] Add tests

Commits
-------

8526d7c050 [Serializer] Add an @Ignore annotation
This commit is contained in:
Fabien Potencier 2020-04-24 10:37:03 +02:00
commit 734a0061e5
19 changed files with 218 additions and 8 deletions

View File

@ -47,7 +47,8 @@ class SerializerExtractor implements PropertyListExtractorInterface
$serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class);
foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) {
if (array_intersect($context['serializer_groups'], $serializerAttributeMetadata->getGroups())) {
$ignored = method_exists($serializerClassMetadata, 'isIgnored') && $serializerAttributeMetadata->isIgnored();
if (!$ignored && array_intersect($context['serializer_groups'], $serializerAttributeMetadata->getGroups())) {
$properties[] = $serializerAttributeMetadata->getName();
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Annotation;
/**
* Annotation class for @Ignore().
*
* @Annotation
* @Target({"PROPERTY", "METHOD"})
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class Ignore
{
}

View File

@ -6,6 +6,7 @@ CHANGELOG
* added support for scalar values denormalization
* added support for `\stdClass` to `ObjectNormalizer`
* added the ability to ignore properties using metadata (e.g. `@Symfony\Component\Serializer\Annotation\Ignore`)
5.0.0
-----

View File

@ -50,6 +50,15 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public $serializedName;
/**
* @var bool
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link isIgnored()} instead.
*/
public $ignore = false;
public function __construct(string $name)
{
$this->name = $name;
@ -113,6 +122,22 @@ class AttributeMetadata implements AttributeMetadataInterface
return $this->serializedName;
}
/**
* {@inheritdoc}
*/
public function setIgnore(bool $ignore)
{
$this->ignore = $ignore;
}
/**
* {@inheritdoc}
*/
public function isIgnored(): bool
{
return $this->ignore;
}
/**
* {@inheritdoc}
*/
@ -131,6 +156,10 @@ class AttributeMetadata implements AttributeMetadataInterface
if (null === $this->serializedName) {
$this->serializedName = $attributeMetadata->getSerializedName();
}
if ($ignore = $attributeMetadata->isIgnored()) {
$this->ignore = $ignore;
}
}
/**
@ -140,6 +169,6 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public function __sleep()
{
return ['name', 'groups', 'maxDepth', 'serializedName'];
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore'];
}
}

View File

@ -61,6 +61,16 @@ interface AttributeMetadataInterface
*/
public function getSerializedName(): ?string;
/**
* Sets if this attribute must be ignored or not.
*/
public function setIgnore(bool $ignore);
/**
* Gets if this attribute is ignored or not.
*/
public function isIgnored(): bool;
/**
* Merges an {@see AttributeMetadataInterface} with in the current one.
*/

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Serializer\Mapping\Loader;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Exception\MappingException;
@ -71,6 +72,8 @@ class AnnotationLoader implements LoaderInterface
$attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth());
} elseif ($annotation instanceof SerializedName) {
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof Ignore) {
$attributesMetadata[$property->name]->setIgnore(true);
}
$loaded = true;
@ -116,6 +119,8 @@ class AnnotationLoader implements LoaderInterface
}
$attributeMetadata->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof Ignore) {
$attributeMetadata->setIgnore(true);
}
$loaded = true;

View File

@ -70,6 +70,10 @@ class XmlFileLoader extends FileLoader
if (isset($attribute['serialized-name'])) {
$attributeMetadata->setSerializedName((string) $attribute['serialized-name']);
}
if (isset($attribute['ignore'])) {
$attributeMetadata->setIgnore((bool) $attribute['ignore']);
}
}
if (isset($xml->{'discriminator-map'})) {

View File

@ -93,6 +93,14 @@ class YamlFileLoader extends FileLoader
$attributeMetadata->setSerializedName($data['serialized_name']);
}
if (isset($data['ignore'])) {
if (!\is_bool($data['ignore'])) {
throw new MappingException(sprintf('The "ignore" value must be a boolean in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()));
}
$attributeMetadata->setIgnore($data['ignore']);
}
}
}

View File

@ -48,7 +48,7 @@
</xsd:choice>
<xsd:attribute name="type-property" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="discriminator-map-mapping">
<xsd:attribute name="type" type="xsd:string" use="required" />
<xsd:attribute name="class" type="xsd:string" use="required" />
@ -78,6 +78,7 @@
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="ignore" type="xsd:boolean" />
</xsd:complexType>
</xsd:schema>

View File

@ -239,22 +239,29 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
$tmpGroups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? null;
$groups = (\is_array($tmpGroups) || is_scalar($tmpGroups)) ? (array) $tmpGroups : false;
if (false === $groups && $allowExtraAttributes) {
return false;
}
$allowedAttributes = [];
$ignoreUsed = false;
foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
$name = $attributeMetadata->getName();
if ($ignore = $attributeMetadata->isIgnored()) {
$ignoreUsed = true;
}
// If you update this check, update accordingly the one in Symfony\Component\PropertyInfo\Extractor\SerializerExtractor::getProperties()
if (
!$ignore &&
(false === $groups || array_intersect($attributeMetadata->getGroups(), $groups)) &&
$this->isAllowedAttribute($classOrObject, $name, null, $context)
$this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context)
) {
$allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
}
}
if (!$ignoreUsed && false === $groups && $allowExtraAttributes) {
// Backward Compatibility with the code using this method written before the introduction of @Ignore
return false;
}
return $allowedAttributes;
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Ignore;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class IgnoreDummy
{
public $notIgnored;
/**
* @Ignore()
*/
public $ignored1;
private $ignored2;
/**
* @Ignore()
*/
public function getIgnored2()
{
return $this->ignored2;
}
}

View File

@ -0,0 +1,4 @@
'Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy':
attributes:
ignored1:
ignore: foo

View File

@ -34,4 +34,9 @@
<attribute name="foo" />
</class>
<class name="Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy">
<attribute name="ignored1" ignore="true" />
<attribute name="ignored2" ignore="true" />
</class>
</serializer>

View File

@ -24,3 +24,9 @@
second: 'Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild'
attributes:
foo: ~
'Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy':
attributes:
ignored1:
ignore: true
ignored2:
ignore: true

View File

@ -57,6 +57,14 @@ class AttributeMetadataTest extends TestCase
$this->assertEquals('serialized_name', $attributeMetadata->getSerializedName());
}
public function testIgnore()
{
$attributeMetadata = new AttributeMetadata('ignored');
$this->assertFalse($attributeMetadata->isIgnored());
$attributeMetadata->setIgnore(true);
$this->assertTrue($attributeMetadata->isIgnored());
}
public function testMerge()
{
$attributeMetadata1 = new AttributeMetadata('a1');
@ -69,11 +77,14 @@ class AttributeMetadataTest extends TestCase
$attributeMetadata2->setMaxDepth(2);
$attributeMetadata2->setSerializedName('a3');
$attributeMetadata2->setIgnore(true);
$attributeMetadata1->merge($attributeMetadata2);
$this->assertEquals(['a', 'b', 'c'], $attributeMetadata1->getGroups());
$this->assertEquals(2, $attributeMetadata1->getMaxDepth());
$this->assertEquals('a3', $attributeMetadata1->getSerializedName());
$this->assertTrue($attributeMetadata1->isIgnored());
}
public function testSerialize()

View File

@ -20,6 +20,7 @@ use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
@ -105,4 +106,14 @@ class AnnotationLoaderTest extends TestCase
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(true), $classMetadata);
}
public function testLoadIgnore()
{
$classMetadata = new ClassMetadata(IgnoreDummy::class);
$this->loader->loadClassMetadata($classMetadata);
$attributesMetadata = $classMetadata->getAttributesMetadata();
$this->assertTrue($attributesMetadata['ignored1']->isIgnored());
$this->assertTrue($attributesMetadata['ignored2']->isIgnored());
}
}

View File

@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
@ -92,4 +93,14 @@ class XmlFileLoaderTest extends TestCase
$this->assertEquals($expected, $classMetadata);
}
public function testLoadIgnore()
{
$classMetadata = new ClassMetadata(IgnoreDummy::class);
$this->loader->loadClassMetadata($classMetadata);
$attributesMetadata = $classMetadata->getAttributesMetadata();
$this->assertTrue($attributesMetadata['ignored1']->isIgnored());
$this->assertTrue($attributesMetadata['ignored2']->isIgnored());
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Tests\Mapping\Loader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
@ -19,6 +20,7 @@ use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
@ -105,4 +107,22 @@ class YamlFileLoaderTest extends TestCase
$this->assertEquals($expected, $classMetadata);
}
public function testLoadIgnore()
{
$classMetadata = new ClassMetadata(IgnoreDummy::class);
$this->loader->loadClassMetadata($classMetadata);
$attributesMetadata = $classMetadata->getAttributesMetadata();
$this->assertTrue($attributesMetadata['ignored1']->isIgnored());
$this->assertTrue($attributesMetadata['ignored2']->isIgnored());
}
public function testLoadInvalidIgnore()
{
$this->expectException(MappingException::class);
$this->expectExceptionMessage('The "ignore" value must be a boolean');
(new YamlFileLoader(__DIR__.'/../../Fixtures/invalid-ignore.yml'))->loadClassMetadata(new ClassMetadata(IgnoreDummy::class));
}
}

View File

@ -13,6 +13,7 @@ use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractNormalizerDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy;
use Symfony\Component\Serializer\Tests\Fixtures\NullableConstructorArgumentDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorNormalizer;
@ -134,4 +135,20 @@ class AbstractNormalizerTest extends TestCase
$this->assertInstanceOf(Dummy::class, $foo);
}
}
public function testIgnore()
{
$classMetadata = new ClassMetadata(IgnoreDummy::class);
$attributeMetadata = new AttributeMetadata('ignored1');
$attributeMetadata->setIgnore(true);
$classMetadata->addAttributeMetadata($attributeMetadata);
$this->classMetadata->method('getMetadataFor')->willReturn($classMetadata);
$dummy = new IgnoreDummy();
$dummy->ignored1 = 'hello';
$normalizer = new PropertyNormalizer($this->classMetadata);
$this->assertSame([], $normalizer->normalize($dummy));
}
}