Skip to content

Commit

Permalink
feature #28744 [Serializer] Add an @ignore annotation (dunglas)
Browse files Browse the repository at this point in the history
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
-------

8526d7c [Serializer] Add an @ignore annotation
  • Loading branch information
fabpot committed Apr 24, 2020
2 parents 260dea0 + 8526d7c commit 734a006
Show file tree
Hide file tree
Showing 19 changed files with 218 additions and 8 deletions.
Expand Up @@ -47,7 +47,8 @@ public function getProperties(string $class, array $context = []): ?array
$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();
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/Symfony/Component/Serializer/Annotation/Ignore.php
@@ -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
{
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
31 changes: 30 additions & 1 deletion src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php
Expand Up @@ -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;
Expand Down Expand Up @@ -113,6 +122,22 @@ public function getSerializedName(): ?string
return $this->serializedName;
}

/**
* {@inheritdoc}
*/
public function setIgnore(bool $ignore)
{
$this->ignore = $ignore;
}

/**
* {@inheritdoc}
*/
public function isIgnored(): bool
{
return $this->ignore;
}

/**
* {@inheritdoc}
*/
Expand All @@ -131,6 +156,10 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
if (null === $this->serializedName) {
$this->serializedName = $attributeMetadata->getSerializedName();
}

if ($ignore = $attributeMetadata->isIgnored()) {
$this->ignore = $ignore;
}
}

/**
Expand All @@ -140,6 +169,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
*/
public function __sleep()
{
return ['name', 'groups', 'maxDepth', 'serializedName'];
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore'];
}
}
Expand Up @@ -61,6 +61,16 @@ public function setSerializedName(string $serializedName = null);
*/
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.
*/
Expand Down
Expand Up @@ -14,6 +14,7 @@
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;
Expand Down Expand Up @@ -71,6 +72,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
$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;
Expand Down Expand Up @@ -116,6 +119,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
}

$attributeMetadata->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof Ignore) {
$attributeMetadata->setIgnore(true);
}

$loaded = true;
Expand Down
Expand Up @@ -70,6 +70,10 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
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'})) {
Expand Down
Expand Up @@ -93,6 +93,14 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)

$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']);
}
}
}

Expand Down
Expand Up @@ -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" />
Expand Down Expand Up @@ -78,6 +78,7 @@
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="ignore" type="xsd:boolean" />
</xsd:complexType>

</xsd:schema>
17 changes: 12 additions & 5 deletions src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
Expand Up @@ -239,22 +239,29 @@ protected function getAllowedAttributes($classOrObject, array $context, bool $at

$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;
}

Expand Down
35 changes: 35 additions & 0 deletions src/Symfony/Component/Serializer/Tests/Fixtures/IgnoreDummy.php
@@ -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
Expand Up @@ -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>
Expand Up @@ -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
Expand Up @@ -57,6 +57,14 @@ public function testSerializedName()
$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');
Expand All @@ -69,11 +77,14 @@ public function testMerge()
$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()
Expand Down
Expand Up @@ -20,6 +20,7 @@
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;

/**
Expand Down Expand Up @@ -105,4 +106,14 @@ public function testLoadClassMetadataAndMerge()

$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());
}
}
Expand Up @@ -19,6 +19,7 @@
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;

/**
Expand Down Expand Up @@ -92,4 +93,14 @@ public function testLoadDiscriminatorMap()

$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());
}
}
Expand Up @@ -12,13 +12,15 @@
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;
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;

/**
Expand Down Expand Up @@ -105,4 +107,22 @@ public function testLoadDiscriminatorMap()

$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));
}
}

0 comments on commit 734a006

Please sign in to comment.