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:
commit
734a0061e5
@ -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();
|
||||
}
|
||||
}
|
||||
|
24
src/Symfony/Component/Serializer/Annotation/Ignore.php
Normal file
24
src/Symfony/Component/Serializer/Annotation/Ignore.php
Normal 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
|
||||
{
|
||||
}
|
@ -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
|
||||
-----
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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'})) {
|
||||
|
@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
'Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy':
|
||||
attributes:
|
||||
ignored1:
|
||||
ignore: foo
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user