diff --git a/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php index 401f94e94f..eb89242823 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/SerializerExtractor.php @@ -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(); } } diff --git a/src/Symfony/Component/Serializer/Annotation/Ignore.php b/src/Symfony/Component/Serializer/Annotation/Ignore.php new file mode 100644 index 0000000000..313c7f7947 --- /dev/null +++ b/src/Symfony/Component/Serializer/Annotation/Ignore.php @@ -0,0 +1,24 @@ + + * + * 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 + */ +final class Ignore +{ +} diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index cd22413e22..e871eb5fea 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php index 3466f3c835..732e0bd590 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php @@ -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']; } } diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php index 9f53f142b2..9e78cf0d31 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php @@ -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. */ diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index bd9fab1c27..978fe659bb 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -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; diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php index cd329e91c6..696007afb8 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php @@ -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'})) { diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index 8833394109..ff50e622ee 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -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']); + } } } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd index 5dfe1e3730..b427a36e36 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd +++ b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd @@ -48,7 +48,7 @@ - + @@ -78,6 +78,7 @@ + diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 5d30cb7beb..dfa778cfba 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -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; } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/IgnoreDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/IgnoreDummy.php new file mode 100644 index 0000000000..a272b4c6fe --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/IgnoreDummy.php @@ -0,0 +1,35 @@ + + * + * 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 + */ +class IgnoreDummy +{ + public $notIgnored; + /** + * @Ignore() + */ + public $ignored1; + private $ignored2; + + /** + * @Ignore() + */ + public function getIgnored2() + { + return $this->ignored2; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/invalid-ignore.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/invalid-ignore.yml new file mode 100644 index 0000000000..d245021d8c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/invalid-ignore.yml @@ -0,0 +1,4 @@ +'Symfony\Component\Serializer\Tests\Fixtures\IgnoreDummy': + attributes: + ignored1: + ignore: foo diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml index 984c2eab80..257d838b49 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml @@ -34,4 +34,9 @@ + + + + + diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml index dfde403a64..4d98c73b04 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml @@ -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 diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php index 9102374fd3..923d8fc39d 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php @@ -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() diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php index 80bfe3d0c5..b2356c5d78 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -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()); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php index 4fc4032f96..4d30c8e2cb 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -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()); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php index 83e68f73e9..6d2ff5c0bd 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -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)); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php index cc84452cbe..2a029b6db5 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php @@ -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)); + } }