minor #30888 [serializer] extract normalizer tests to traits (dbu)

This PR was squashed before being merged into the 4.3-dev branch (closes #30888).

Discussion
----------

[serializer] extract normalizer tests to traits

eufossa

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | Relates to #30818
| License       | MIT
| Doc PR        | -

As discussed with @joelwurtz, extract normalizer functionality tests into traits to ensure consistent behaviour of all normalizers.

* [x] Rebase when #30977, #30950 and #30907 are merged to master **blocker**
* [x] Clean up order of trait inclusion and methods in the tests
* [x] Clean up fixture classes of the traits. I started having one class named the same as the trait, where possible

Stuff that we should do eventually, but can also do in separate pull requests, after this one has been merged:
* [ ] Extract all features that we can (the existing normalizer tests should more or less only have the legacy tests in them, all functionality should be in trait)
* [ ] Run test coverage and increase coverage so that we cover all important features and all relevant error cases.

Commits
-------

2b6ebea73c [serializer] extract normalizer tests to traits
This commit is contained in:
Fabien Potencier 2019-04-27 17:25:46 +01:00
commit 016425eefd
25 changed files with 1901 additions and 1415 deletions

View File

@ -32,20 +32,89 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
use ObjectToPopulateTrait;
use SerializerAwareTrait;
const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
const OBJECT_TO_POPULATE = 'object_to_populate';
const GROUPS = 'groups';
const ATTRIBUTES = 'attributes';
const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
const CALLBACKS = 'callbacks';
const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler';
const IGNORED_ATTRIBUTES = 'ignored_attributes';
/* constants to configure the context */
/**
* How many loops of circular reference to allow while normalizing.
*
* The default value of 1 means that when we encounter the same object a
* second time, we consider that a circular reference.
*
* You can raise this value for special cases, e.g. in combination with the
* max depth setting of the object normalizer.
*/
public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
/**
* Instead of creating a new instance of an object, update the specified object.
*
* If you have a nested structure, child objects will be overwritten with
* new instances unless you set DEEP_OBJECT_TO_POPULATE to true.
*/
public const OBJECT_TO_POPULATE = 'object_to_populate';
/**
* Only (de)normalize attributes that are in the specified groups.
*/
public const GROUPS = 'groups';
/**
* Limit (de)normalize to the specified names.
*
* For nested structures, this list needs to reflect the object tree.
*/
public const ATTRIBUTES = 'attributes';
/**
* If ATTRIBUTES are specified, and the source has fields that are not part of that list,
* either ignore those attributes (true) or throw an ExtraAttributesException (false).
*/
public const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
/**
* Hashmap of default values for constructor arguments.
*
* The names need to match the parameter names in the constructor arguments.
*/
public const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
/**
* Hashmap of field name => callable to normalize this field.
*
* The callable is called if the field is encountered with the arguments:
*
* - mixed $attributeValue value of this field
* - object $object the whole object being normalized
* - string $attributeName name of the attribute being normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const CALLBACKS = 'callbacks';
/**
* Handler to call when a circular reference has been detected.
*
* If you specify no handler, a CircularReferenceException is thrown.
*
* The method will be called with ($object, $format, $context) and its
* return value is returned as the result of the normalize call.
*/
public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler';
/**
* Skip the specified attributes when normalizing an object tree.
*
* This list is applied to each element of nested structures.
*
* Note: The behaviour for nested structures is different from ATTRIBUTES
* for historical reason. Aligning the behaviour would be a BC break.
*/
public const IGNORED_ATTRIBUTES = 'ignored_attributes';
/**
* @internal
*/
const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
protected const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
protected $defaultContext = [
self::ALLOW_EXTRA_ATTRIBUTES => true,

View File

@ -33,13 +33,60 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
{
const ENABLE_MAX_DEPTH = 'enable_max_depth';
const DEPTH_KEY_PATTERN = 'depth_%s::%s';
const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
const SKIP_NULL_VALUES = 'skip_null_values';
const MAX_DEPTH_HANDLER = 'max_depth_handler';
const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key';
const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
/**
* Set to true to respect the max depth metadata on fields.
*/
public const ENABLE_MAX_DEPTH = 'enable_max_depth';
/**
* How to track the current depth in the context.
*/
private const DEPTH_KEY_PATTERN = 'depth_%s::%s';
/**
* While denormalizing, we can verify that types match.
*
* You can disable this by setting this flag to true.
*/
public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
/**
* Flag to control whether fields with the value `null` should be output
* when normalizing or omitted.
*/
public const SKIP_NULL_VALUES = 'skip_null_values';
/**
* Callback to allow to set a value for an attribute when the max depth has
* been reached.
*
* If no callback is given, the attribute is skipped. If a callable is
* given, its return value is used (even if null).
*
* The arguments are:
*
* - mixed $attributeValue value of this field
* - object $object the whole object being normalized
* - string $attributeName name of the attribute being normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const MAX_DEPTH_HANDLER = 'max_depth_handler';
/**
* Specify which context key are not relevant to determine which attributes
* of an object to (de)normalize.
*/
public const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key';
/**
* Flag to tell the denormalizer to also populate existing objects on
* attributes of the main object.
*
* Setting this to true is only useful if you also specify the root object
* in OBJECT_TO_POPULATE.
*/
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
private $propertyTypeExtractor;
private $typesCache = [];

View File

@ -19,4 +19,20 @@ class DeepObjectPopulateChildDummy
public $foo;
public $bar;
// needed to have GetSetNormalizer consider this class as supported
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo)
{
$this->foo = $foo;
}
public function setBar($bar)
{
$this->bar = $bar;
}
}

View File

@ -42,4 +42,9 @@ class MaxDepthDummy
{
return $this->child;
}
public function getFoo()
{
return $this->foo;
}
}

View File

@ -10,7 +10,6 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Tests\Fixtures\AbstractNormalizerDummy;
use Symfony\Component\Serializer\Tests\Fixtures\NullableConstructorArgumentDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ProxyDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorDummy;
use Symfony\Component\Serializer\Tests\Fixtures\StaticConstructorNormalizer;
@ -99,18 +98,6 @@ class AbstractNormalizerTest extends TestCase
$this->assertEquals([$a3, $a4], $result);
}
public function testObjectToPopulateWithProxy()
{
$proxyDummy = new ProxyDummy();
$context = [AbstractNormalizer::OBJECT_TO_POPULATE => $proxyDummy];
$normalizer = new ObjectNormalizer();
$normalizer->denormalize(['foo' => 'bar'], 'Symfony\Component\Serializer\Tests\Fixtures\ToBeProxyfiedDummy', null, $context);
$this->assertSame('bar', $proxyDummy->getFoo());
}
public function testObjectWithStaticConstructor()
{
$normalizer = new StaticConstructorNormalizer();

View File

@ -23,8 +23,6 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateChildDummy;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateParentDummy;
class AbstractObjectNormalizerTest extends TestCase
{
@ -163,58 +161,6 @@ class AbstractObjectNormalizerTest extends TestCase
'allow_extra_attributes' => false,
]);
}
public function testSkipNullValues()
{
$dummy = new Dummy();
$dummy->bar = 'present';
$normalizer = new ObjectNormalizer();
$result = $normalizer->normalize($dummy, null, [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
$this->assertSame(['bar' => 'present'], $result);
}
public function testDeepObjectToPopulate()
{
$child = new DeepObjectPopulateChildDummy();
$child->bar = 'bar-old';
$child->foo = 'foo-old';
$parent = new DeepObjectPopulateParentDummy();
$parent->setChild($child);
$context = [
AbstractObjectNormalizer::OBJECT_TO_POPULATE => $parent,
AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE => true,
];
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
$newChild = new DeepObjectPopulateChildDummy();
$newChild->bar = 'bar-new';
$newChild->foo = 'foo-old';
$serializer = $this->getMockBuilder(__NAMESPACE__.'\ObjectSerializerDenormalizer')->getMock();
$serializer
->method('supportsDenormalization')
->with($this->arrayHasKey('bar'),
$this->equalTo(DeepObjectPopulateChildDummy::class),
$this->isNull(),
$this->contains($child))
->willReturn(true);
$serializer->method('denormalize')->willReturn($newChild);
$normalizer->setSerializer($serializer);
$normalizer->denormalize([
'child' => [
'bar' => 'bar-new',
],
], 'Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateParentDummy', null, $context);
$this->assertSame('bar-new', $parent->getChild()->bar);
$this->assertSame('foo-old', $parent->getChild()->foo);
}
}
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer

View File

@ -0,0 +1,108 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::ATTRIBUTES and AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES.
*/
trait AttributesTestTrait
{
abstract protected function getNormalizerForAttributes(): NormalizerInterface;
abstract protected function getDenormalizerForAttributes(): DenormalizerInterface;
public function testAttributesNormalize()
{
$normalizer = $this->getNormalizerForAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectInner->bar = 'innerBar';
$objectDummy = new ObjectDummy();
$objectDummy->setFoo('foo');
$objectDummy->setBaz(true);
$objectDummy->setObject($objectInner);
$context = ['attributes' => ['foo', 'baz', 'object' => ['foo']]];
$this->assertEquals(
[
'foo' => 'foo',
'baz' => true,
'object' => ['foo' => 'innerFoo'],
],
$normalizer->normalize($objectDummy, null, $context)
);
$context = ['attributes' => ['foo', 'baz', 'object']];
$this->assertEquals(
[
'foo' => 'foo',
'baz' => true,
'object' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
],
$normalizer->normalize($objectDummy, null, $context)
);
}
public function testAttributesContextDenormalize()
{
$normalizer = $this->getDenormalizerForAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectOuter = new ObjectOuter();
$objectOuter->bar = 'bar';
$objectOuter->setInner($objectInner);
$context = ['attributes' => ['bar', 'inner' => ['foo']]];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'bar',
'date' => '2017-02-03',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
public function testAttributesContextDenormalizeIgnoreExtraAttributes()
{
$normalizer = $this->getDenormalizerForAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectOuter = new ObjectOuter();
$objectOuter->setInner($objectInner);
$context = ['attributes' => ['inner' => ['foo']]];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'changed',
'date' => '2017-02-03',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
public function testAttributesContextDenormalizeExceptionExtraAttributes()
{
$normalizer = $this->getDenormalizerForAttributes();
$context = [
'attributes' => ['bar', 'inner' => ['foo']],
'allow_extra_attributes' => false,
];
$this->expectException(ExtraAttributesException::class);
$normalizer->denormalize(
[
'bar' => 'bar',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class CallbacksObject
{
public $bar;
public function __construct($bar = null)
{
$this->bar = $bar;
}
public function getBar()
{
return $this->bar;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::CALLBACKS.
*/
trait CallbacksTestTrait
{
abstract protected function getNormalizerForCallbacks(): NormalizerInterface;
/**
* @dataProvider provideCallbacks
*/
public function testCallbacks($callbacks, $valueBar, $result)
{
$normalizer = $this->getNormalizerForCallbacks();
$obj = new CallbacksObject();
$obj->bar = $valueBar;
$this->assertEquals(
$result,
$normalizer->normalize($obj, 'any', ['callbacks' => $callbacks])
);
}
/**
* @dataProvider provideInvalidCallbacks()
*/
public function testUncallableCallbacks($callbacks)
{
$normalizer = $this->getNormalizerForCallbacks();
$obj = new CallbacksObject();
$this->markTestSkipped('Callback validation for callbacks in the context has been forgotten. See https://github.com/symfony/symfony/pull/30950');
$this->expectException(InvalidArgumentException::class);
$normalizer->normalize($obj, null, ['callbacks' => $callbacks]);
}
public function provideCallbacks()
{
return [
'Change a string' => [
[
'bar' => function ($bar) {
$this->assertEquals('baz', $bar);
return 'baz';
},
],
'baz',
['bar' => 'baz'],
],
'Null an item' => [
[
'bar' => function ($value, $object, $attributeName, $format, $context) {
$this->assertSame('baz', $value);
$this->assertInstanceOf(CallbacksObject::class, $object);
$this->assertSame('bar', $attributeName);
$this->assertSame('any', $format);
$this->assertArrayHasKey('circular_reference_limit_counters', $context);
},
],
'baz',
['bar' => null],
],
'Format a date' => [
[
'bar' => function ($bar) {
$this->assertInstanceOf(\DateTime::class, $bar);
return $bar->format('d-m-Y H:i:s');
},
],
new \DateTime('2011-09-10 06:30:00'),
['bar' => '10-09-2011 06:30:00'],
],
'Collect a property' => [
[
'bar' => function (array $bars) {
$result = '';
foreach ($bars as $bar) {
$result .= $bar->bar;
}
return $result;
},
],
[new CallbacksObject('baz'), new CallbacksObject('quux')],
['bar' => 'bazquux'],
],
'Count a property' => [
[
'bar' => function (array $bars) {
return \count($bars);
},
],
[new CallbacksObject(), new CallbacksObject()],
['bar' => 2],
],
];
}
public function provideInvalidCallbacks()
{
return [
[['bar' => null]],
[['bar' => 'thisisnotavalidfunction']],
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT and AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER.
*/
trait CircularReferenceTestTrait
{
abstract protected function getNormalizerForCircularReference(): NormalizerInterface;
abstract protected function getSelfReferencingModel();
public function testUnableToNormalizeCircularReference()
{
$normalizer = $this->getNormalizerForCircularReference();
$obj = $this->getSelfReferencingModel();
$this->expectException(CircularReferenceException::class);
$normalizer->normalize($obj, null, ['circular_reference_limit' => 2]);
}
public function testCircularReferenceHandler()
{
$normalizer = $this->getNormalizerForCircularReference();
$obj = $this->getSelfReferencingModel();
$expected = ['me' => \get_class($obj)];
$context = [
'circular_reference_handler' => function ($actualObj, string $format, array $context) use ($obj) {
$this->assertInstanceOf(\get_class($obj), $actualObj);
$this->assertSame('test', $format);
$this->assertArrayHasKey('foo', $context);
return \get_class($actualObj);
},
'foo' => 'bar',
];
$this->assertEquals($expected, $normalizer->normalize($obj, 'test', $context));
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ConstructorArgumentsObject
{
private $foo;
private $bar;
private $baz;
public function __construct($foo, $bar, $baz)
{
$this->foo = $foo;
$this->bar = $bar;
$this->baz = $baz;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\NotSerializedConstructorArgumentDummy;
trait ConstructorArgumentsTestTrait
{
abstract protected function getDenormalizerForConstructArguments(): DenormalizerInterface;
public function testDefaultConstructorArguments()
{
$data = [
'foo' => 10,
];
$denormalizer = $this->getDenormalizerForConstructArguments();
$result = $denormalizer->denormalize($data, ConstructorArgumentsObject::class, 'json', [
'default_constructor_arguments' => [
ConstructorArgumentsObject::class => ['foo' => '', 'bar' => '', 'baz' => null],
],
]);
$this->assertEquals(new ConstructorArgumentsObject(10, '', null), $result);
}
public function testMetadataAwareNameConvertorWithNotSerializedConstructorParameter()
{
$denormalizer = $this->getDenormalizerForConstructArguments();
$obj = new NotSerializedConstructorArgumentDummy('buz');
$obj->setBar('xyz');
$this->assertEquals(
$obj,
$denormalizer->denormalize(['bar' => 'xyz'],
NotSerializedConstructorArgumentDummy::class,
null,
['default_constructor_arguments' => [
NotSerializedConstructorArgumentDummy::class => ['foo' => 'buz'],
]]
)
);
}
public function testConstructorWithMissingData()
{
$data = [
'foo' => 10,
];
$normalizer = $this->getDenormalizerForConstructArguments();
$this->expectException(MissingConstructorArgumentsException::class);
$this->expectExceptionMessage('Cannot create an instance of '.ConstructorArgumentsObject::class.' from serialized data because its constructor requires parameter "bar" to be present.');
$normalizer->denormalize($data, ConstructorArgumentsObject::class);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
/**
* Test AbstractNormalizer::GROUPS.
*/
trait GroupsTestTrait
{
abstract protected function getNormalizerForGroups(): NormalizerInterface;
abstract protected function getDenormalizerForGroups(): DenormalizerInterface;
public function testGroupsNormalize()
{
$normalizer = $this->getNormalizerForGroups();
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals([
'bar' => 'bar',
], $normalizer->normalize($obj, null, ['groups' => ['c']]));
$this->assertEquals([
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
'kevin' => 'kevin',
'coopTilleuls' => 'coopTilleuls',
], $normalizer->normalize($obj, null, ['groups' => ['a', 'c']]));
}
public function testGroupsDenormalize()
{
$normalizer = $this->getDenormalizerForGroups();
$obj = new GroupDummy();
$obj->setFoo('foo');
$data = ['foo' => 'foo', 'bar' => 'bar'];
$normalized = $normalizer->denormalize(
$data,
GroupDummy::class,
null,
['groups' => ['a']]
);
$this->assertEquals($obj, $normalized);
$obj->setBar('bar');
$normalized = $normalizer->denormalize(
$data,
GroupDummy::class,
null,
['groups' => ['a', 'b']]
);
$this->assertEquals($obj, $normalized);
}
public function testNormalizeNoPropertyInGroup()
{
$normalizer = $this->getNormalizerForGroups();
$obj = new GroupDummy();
$obj->setFoo('foo');
$this->assertEquals([], $normalizer->normalize($obj, null, ['groups' => ['notExist']]));
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractNormalizer::IGNORED_ATTRIBUTES.
*/
trait IgnoredAttributesTestTrait
{
abstract protected function getNormalizerForIgnoredAttributes(): NormalizerInterface;
abstract protected function getDenormalizerForIgnoredAttributes(): DenormalizerInterface;
public function testIgnoredAttributesNormalize()
{
$normalizer = $this->getNormalizerForIgnoredAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectInner->bar = 'innerBar';
$objectOuter = new ObjectOuter();
$objectOuter->foo = 'foo';
$objectOuter->bar = 'bar';
$objectOuter->setInner($objectInner);
$context = ['ignored_attributes' => ['bar']];
$this->assertEquals(
[
'foo' => 'foo',
'inner' => ['foo' => 'innerFoo'],
'date' => null,
'inners' => null,
],
$normalizer->normalize($objectOuter, null, $context)
);
$this->markTestIncomplete('AbstractObjectNormalizer::getAttributes caches attributes by class instead of by class+context, reusing the normalizer with different config therefore fails. This is being fixed in https://github.com/symfony/symfony/pull/30907');
$context = ['ignored_attributes' => ['foo', 'inner']];
$this->assertEquals(
[
'bar' => 'bar',
'date' => null,
'inners' => null,
],
$normalizer->normalize($objectOuter, null, $context)
);
}
public function testIgnoredAttributesContextDenormalize()
{
$normalizer = $this->getDenormalizerForIgnoredAttributes();
$objectOuter = new ObjectOuter();
$objectOuter->bar = 'bar';
$context = ['ignored_attributes' => ['foo', 'inner']];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'bar',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
public function testIgnoredAttributesContextDenormalizeInherit()
{
$normalizer = $this->getDenormalizerForIgnoredAttributes();
$objectInner = new ObjectInner();
$objectInner->foo = 'innerFoo';
$objectOuter = new ObjectOuter();
$objectOuter->foo = 'foo';
$objectOuter->setInner($objectInner);
$context = ['ignored_attributes' => ['bar']];
$this->assertEquals($objectOuter, $normalizer->denormalize(
[
'foo' => 'foo',
'bar' => 'bar',
'inner' => ['foo' => 'innerFoo', 'bar' => 'innerBar'],
], ObjectOuter::class, null, $context));
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
/**
* Covers AbstractObjectNormalizer::ENABLE_MAX_DEPTH and AbstractObjectNormalizer::MAX_DEPTH_HANDLER.
*/
trait MaxDepthTestTrait
{
abstract protected function getNormalizerForMaxDepth(): NormalizerInterface;
public function testMaxDepth()
{
$normalizer = $this->getNormalizerForMaxDepth();
$level1 = new MaxDepthDummy();
$level1->bar = 'level1';
$level2 = new MaxDepthDummy();
$level2->bar = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->bar = 'level3';
$level2->child = $level3;
$level4 = new MaxDepthDummy();
$level4->bar = 'level4';
$level3->child = $level4;
$result = $normalizer->normalize($level1, null, ['enable_max_depth' => true]);
$expected = [
'bar' => 'level1',
'child' => [
'bar' => 'level2',
'child' => [
'bar' => 'level3',
'child' => [
'child' => null,
],
],
'foo' => null,
],
'foo' => null,
];
$this->assertEquals($expected, $result);
}
public function testMaxDepthHandler()
{
$level1 = new MaxDepthDummy();
$level1->bar = 'level1';
$level2 = new MaxDepthDummy();
$level2->bar = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->bar = 'level3';
$level2->child = $level3;
$level4 = new MaxDepthDummy();
$level4->bar = 'level4';
$level3->child = $level4;
$handlerCalled = false;
$maxDepthHandler = function ($object, $parentObject, $attributeName, $format, $context) use (&$handlerCalled) {
if ('foo' === $attributeName) {
return null;
}
$this->assertSame('level4', $object);
$this->assertInstanceOf(MaxDepthDummy::class, $parentObject);
$this->assertSame('bar', $attributeName);
$this->assertSame('test', $format);
$this->assertArrayHasKey('enable_max_depth', $context);
$handlerCalled = true;
return 'handler';
};
$normalizer = $this->getNormalizerForMaxDepth();
$normalizer->normalize($level1, 'test', [
'enable_max_depth' => true,
'max_depth_handler' => $maxDepthHandler,
]);
$this->assertTrue($handlerCalled);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ObjectDummy
{
protected $foo;
public $bar;
private $baz;
protected $camelCase;
protected $object;
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo)
{
$this->foo = $foo;
}
public function isBaz()
{
return $this->baz;
}
public function setBaz($baz)
{
$this->baz = $baz;
}
public function getFooBar()
{
return $this->foo.$this->bar;
}
public function getCamelCase()
{
return $this->camelCase;
}
public function setCamelCase($camelCase)
{
$this->camelCase = $camelCase;
}
public function otherMethod()
{
throw new \RuntimeException('Dummy::otherMethod() should not be called');
}
public function setObject($object)
{
$this->object = $object;
}
public function getObject()
{
return $this->object;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ObjectInner
{
public $foo;
public $bar;
public function getBar()
{
return $this->bar;
}
public function setBar($bar): void
{
$this->bar = $bar;
}
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo): void
{
$this->foo = $foo;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class ObjectOuter
{
public $foo;
public $bar;
private $inner;
private $date;
/**
* @var ObjectInner[]
*/
private $inners;
public function getFoo()
{
return $this->foo;
}
public function setFoo($foo): void
{
$this->foo = $foo;
}
public function getBar()
{
return $this->bar;
}
public function setBar($bar): void
{
$this->bar = $bar;
}
/**
* @return ObjectInner
*/
public function getInner()
{
return $this->inner;
}
public function setInner(ObjectInner $inner)
{
$this->inner = $inner;
}
public function setDate(\DateTimeInterface $date)
{
$this->date = $date;
}
public function getDate()
{
return $this->date;
}
public function setInners(array $inners)
{
$this->inners = $inners;
}
public function getInners()
{
return $this->inners;
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateChildDummy;
use Symfony\Component\Serializer\Tests\Fixtures\DeepObjectPopulateParentDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ProxyDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ToBeProxyfiedDummy;
trait ObjectToPopulateTestTrait
{
abstract protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface;
public function testObjectToPopulate()
{
$dummy = new ObjectDummy();
$dummy->bar = 'bar';
$denormalizer = $this->getDenormalizerForObjectToPopulate();
$obj = $denormalizer->denormalize(
['foo' => 'foo'],
ObjectDummy::class,
null,
['object_to_populate' => $dummy]
);
$this->assertEquals($dummy, $obj);
$this->assertEquals('foo', $obj->getFoo());
$this->assertEquals('bar', $obj->bar);
}
public function testObjectToPopulateWithProxy()
{
$proxyDummy = new ProxyDummy();
$context = ['object_to_populate' => $proxyDummy];
$denormalizer = $this->getDenormalizerForObjectToPopulate();
$denormalizer->denormalize(['foo' => 'bar'], ToBeProxyfiedDummy::class, null, $context);
$this->assertSame('bar', $proxyDummy->getFoo());
}
public function testObjectToPopulateNoMatch()
{
$this->markTestSkipped('something broken here!');
$denormalizer = $this->getDenormalizerForObjectToPopulate();
$objectToPopulate = new ObjectInner();
$objectToPopulate->foo = 'foo';
$outer = $denormalizer->denormalize([
'foo' => 'foo',
'inner' => [
'bar' => 'bar',
],
], ObjectOuter::class, null, ['object_to_popuplate' => $objectToPopulate]);
$this->assertInstanceOf(ObjectOuter::class, $outer);
$inner = $outer->getInner();
$this->assertInstanceOf(ObjectInner::class, $inner);
$this->assertNotSame($objectToPopulate, $inner);
$this->assertSame('bar', $inner->bar);
$this->assertNull($inner->foo);
}
public function testDeepObjectToPopulate()
{
$child = new DeepObjectPopulateChildDummy();
$child->bar = 'bar-old';
$child->foo = 'foo-old';
$parent = new DeepObjectPopulateParentDummy();
$parent->setChild($child);
$context = [
'object_to_populate' => $parent,
'deep_object_to_populate' => true,
];
$normalizer = $this->getDenormalizerForObjectToPopulate();
$newChild = new DeepObjectPopulateChildDummy();
$newChild->bar = 'bar-new';
$newChild->foo = 'foo-old';
$normalizer->denormalize([
'child' => [
'bar' => 'bar-new',
],
], DeepObjectPopulateParentDummy::class, null, $context);
$this->assertSame('bar-new', $parent->getChild()->bar);
$this->assertSame('foo-old', $parent->getChild()->foo);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Test AbstractObjectNormalizer::SKIP_NULL_VALUES.
*/
trait SkipNullValuesTestTrait
{
abstract protected function getNormalizerForSkipNullValues(): NormalizerInterface;
public function testSkipNullValues()
{
$dummy = new ObjectDummy();
$dummy->bar = 'present';
$normalizer = $this->getNormalizerForSkipNullValues();
$result = $normalizer->normalize($dummy, null, ['skip_null_values' => true]);
$this->assertSame(['fooBar' => 'present', 'bar' => 'present'], $result);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
class TypeEnforcementNumberObject
{
/**
* @var float
*/
public $number;
public function setNumber($number)
{
$this->number = $number;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Test type mismatches with a denormalizer that is aware of types.
* Covers AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT.
*/
trait TypeEnforcementTestTrait
{
abstract protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface;
public function testRejectInvalidType()
{
$denormalizer = $this->getDenormalizerForTypeEnforcement();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('The type of the "date" attribute for class "'.ObjectOuter::class.'" must be one of "DateTimeInterface" ("string" given).');
$denormalizer->denormalize(['date' => 'foo'], ObjectOuter::class);
}
public function testRejectInvalidKey()
{
$denormalizer = $this->getDenormalizerForTypeEnforcement();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('The type of the key "a" must be "int" ("string" given).');
$denormalizer->denormalize(['inners' => ['a' => ['foo' => 1]]], ObjectOuter::class);
}
public function testDoNotRejectInvalidTypeOnDisableTypeEnforcementContextOption()
{
$denormalizer = $this->getDenormalizerForTypeEnforcement();
$this->assertSame('foo', $denormalizer->denormalize(
['number' => 'foo'],
TypeEnforcementNumberObject::class,
null,
[AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true]
)->number);
}
}

View File

@ -13,21 +13,45 @@ namespace Symfony\Component\Serializer\Tests\Normalizer;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectToPopulateTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait;
class GetSetMethodNormalizerTest extends TestCase
{
use CallbacksTestTrait;
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;
use ObjectToPopulateTestTrait;
use TypeEnforcementTestTrait;
/**
* @var GetSetMethodNormalizer
*/
@ -89,7 +113,7 @@ class GetSetMethodNormalizerTest extends TestCase
{
$obj = $this->normalizer->denormalize(
['foo' => 'foo', 'bar' => 'bar', 'baz' => true, 'fooBar' => 'foobar'],
__NAMESPACE__.'\GetSetDummy',
GetSetDummy::class,
'any'
);
$this->assertEquals('foo', $obj->getFoo());
@ -119,14 +143,14 @@ class GetSetMethodNormalizerTest extends TestCase
$data->foo = 'foo';
$data->bar = 'bar';
$data->fooBar = 'foobar';
$obj = $this->normalizer->denormalize($data, __NAMESPACE__.'\GetSetDummy', 'any');
$obj = $this->normalizer->denormalize($data, GetSetDummy::class, 'any');
$this->assertEquals('foo', $obj->getFoo());
$this->assertEquals('bar', $obj->getBar());
}
public function testDenormalizeNull()
{
$this->assertEquals(new GetSetDummy(), $this->normalizer->denormalize(null, __NAMESPACE__.'\GetSetDummy'));
$this->assertEquals(new GetSetDummy(), $this->normalizer->denormalize(null, GetSetDummy::class));
}
public function testConstructorDenormalize()
@ -202,62 +226,90 @@ class GetSetMethodNormalizerTest extends TestCase
$this->assertEquals('bar', $obj->getFoo());
}
public function testGroupsNormalize()
protected function getNormalizerForCallbacks(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals([
'bar' => 'bar',
], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['c']]));
$this->assertEquals([
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
'kevin' => 'kevin',
'coopTilleuls' => 'coopTilleuls',
], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['a', 'c']]));
return new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
}
public function testGroupsDenormalize()
/**
* @dataProvider provideCallbacks
*/
public function testLegacyCallbacks($callbacks, $value, $result)
{
$this->normalizer->setCallbacks($callbacks);
$obj = new CallbacksObject($value);
$this->assertEquals(
$result,
$this->normalizer->normalize($obj, 'any')
);
}
protected function getNormalizerForCircularReference(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
new Serializer([$normalizer]);
return $normalizer;
}
protected function getSelfReferencingModel()
{
return new CircularReferenceDummy();
}
public function testLegacyUnableToNormalizeCircularReference()
{
$this->normalizer->setCircularReferenceLimit(2);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj = new CircularReferenceDummy();
$toNormalize = ['foo' => 'foo', 'bar' => 'bar'];
$this->expectException(CircularReferenceException::class);
$this->normalizer->normalize($obj);
}
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[GetSetMethodNormalizer::GROUPS => ['a']]
);
$this->assertEquals($obj, $normalized);
public function testLegacyCircularReferenceHandler()
{
$handler = function ($obj) {
return \get_class($obj);
};
$obj->setBar('bar');
$this->normalizer->setCircularReferenceHandler($handler);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[GetSetMethodNormalizer::GROUPS => ['a', 'b']]
);
$this->assertEquals($obj, $normalized);
$obj = new CircularReferenceDummy();
$expected = ['me' => CircularReferenceDummy::class];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
}
protected function getDenormalizerForConstructArguments(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$denormalizer = new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
new Serializer([$denormalizer]);
return $denormalizer;
}
protected function getNormalizerForGroups(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
return new GetSetMethodNormalizer($classMetadataFactory);
}
protected function getDenormalizerForGroups(): GetSetMethodNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
return new GetSetMethodNormalizer($classMetadataFactory);
}
public function testGroupsNormalizeWithNameConverter()
@ -302,74 +354,62 @@ class GetSetMethodNormalizerTest extends TestCase
);
}
/**
* @dataProvider provideCallbacks
*/
public function testCallbacks($callbacks, $value, $result, $message)
protected function getNormalizerForMaxDepth(): NormalizerInterface
{
$this->doTestCallbacks($callbacks, $value, $result, $message);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$serializer = new Serializer([$normalizer]);
$normalizer->setSerializer($serializer);
return $normalizer;
}
/**
* @dataProvider provideCallbacks
*/
public function testLegacyCallbacks($callbacks, $value, $result, $message)
protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface
{
$this->doTestCallbacks($callbacks, $value, $result, $message, true);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
return $normalizer;
}
private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false)
protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface
{
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([GetSetMethodNormalizer::CALLBACKS => $callbacks]);
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new GetSetMethodNormalizer(null, null, $extractor);
$serializer = new Serializer([new ArrayDenormalizer(), $normalizer]);
$normalizer->setSerializer($serializer);
$obj = new GetConstructorDummy('', $value, true);
$this->assertEquals(
$result,
$this->normalizer->normalize($obj, 'any'),
$message
);
return $normalizer;
}
/**
* @expectedException \InvalidArgumentException
*/
public function testUncallableCallbacks()
public function testRejectInvalidKey()
{
$this->doTestUncallableCallbacks();
$this->markTestSkipped('This test makes no sense with the GetSetMethodNormalizer');
}
/**
* @expectedException \InvalidArgumentException
*/
public function testLegacyUncallableCallbacks()
protected function getNormalizerForIgnoredAttributes(): GetSetMethodNormalizer
{
$this->doTestUncallableCallbacks(true);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
return $normalizer;
}
private function doTestUncallableCallbacks(bool $legacy = false)
protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer
{
$callbacks = ['bar' => null];
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([GetSetMethodNormalizer::CALLBACKS => $callbacks]);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
$obj = new GetConstructorDummy('baz', 'quux', true);
$this->normalizer->normalize($obj, 'any');
}
public function testIgnoredAttributes()
{
$this->doTestIgnoredAttributes();
return $normalizer;
}
public function testLegacyIgnoredAttributes()
{
$this->doTestIgnoredAttributes(true);
}
private function doTestIgnoredAttributes(bool $legacy = false)
{
$ignoredAttributes = ['foo', 'bar', 'baz', 'camelCase', 'object'];
$legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer([GetSetMethodNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]);
$this->normalizer->setIgnoredAttributes($ignoredAttributes);
$obj = new GetSetDummy();
$obj->setFoo('foo');
@ -382,66 +422,6 @@ class GetSetMethodNormalizerTest extends TestCase
);
}
public function provideCallbacks()
{
return [
[
[
'bar' => function ($bar) {
return 'baz';
},
],
'baz',
['foo' => '', 'bar' => 'baz', 'baz' => true],
'Change a string',
],
[
[
'bar' => function ($bar) {
},
],
'baz',
['foo' => '', 'bar' => null, 'baz' => true],
'Null an item',
],
[
[
'bar' => function ($bar) {
return $bar->format('d-m-Y H:i:s');
},
],
new \DateTime('2011-09-10 06:30:00'),
['foo' => '', 'bar' => '10-09-2011 06:30:00', 'baz' => true],
'Format a date',
],
[
[
'bar' => function ($bars) {
$foos = '';
foreach ($bars as $bar) {
$foos .= $bar->getFoo();
}
return $foos;
},
],
[new GetConstructorDummy('baz', '', false), new GetConstructorDummy('quux', '', false)],
['foo' => '', 'bar' => 'bazquux', 'baz' => true],
'Collect a property',
],
[
[
'bar' => function ($bars) {
return \count($bars);
},
],
[new GetConstructorDummy('baz', '', false), new GetConstructorDummy('quux', '', false)],
['foo' => '', 'bar' => 2, 'baz' => true],
'Count a property',
],
];
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\LogicException
* @expectedExceptionMessage Cannot normalize attribute "object" because the injected serializer is not a normalizer
@ -458,32 +438,6 @@ class GetSetMethodNormalizerTest extends TestCase
$this->normalizer->normalize($obj, 'any');
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testUnableToNormalizeCircularReference()
{
$this->doTestUnableToNormalizeCircularReference();
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testLegacyUnableToNormalizeCircularReference()
{
$this->doTestUnableToNormalizeCircularReference(true);
}
private function doTestUnableToNormalizeCircularReference(bool $legacy = false)
{
$legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer([GetSetMethodNormalizer::CIRCULAR_REFERENCE_LIMIT => 2]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new CircularReferenceDummy();
$this->normalizer->normalize($obj);
}
public function testSiblingReference()
{
$serializer = new Serializer([$this->normalizer]);
@ -499,60 +453,17 @@ class GetSetMethodNormalizerTest extends TestCase
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
}
public function testCircularReferenceHandler()
{
$this->doTestCircularReferenceHandler();
}
public function testLegacyCircularReferenceHandler()
{
$this->doTestCircularReferenceHandler(true);
}
private function doTestCircularReferenceHandler(bool $legacy = false)
{
$handler = function ($obj) {
return \get_class($obj);
};
$legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer([GetSetMethodNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new CircularReferenceDummy();
$expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy'];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
}
public function testObjectToPopulate()
{
$dummy = new GetSetDummy();
$dummy->setFoo('foo');
$obj = $this->normalizer->denormalize(
['bar' => 'bar'],
__NAMESPACE__.'\GetSetDummy',
null,
[GetSetMethodNormalizer::OBJECT_TO_POPULATE => $dummy]
);
$this->assertEquals($dummy, $obj);
$this->assertEquals('foo', $obj->getFoo());
$this->assertEquals('bar', $obj->getBar());
}
public function testDenormalizeNonExistingAttribute()
{
$this->assertEquals(
new GetSetDummy(),
$this->normalizer->denormalize(['non_existing' => true], __NAMESPACE__.'\GetSetDummy')
$this->normalizer->denormalize(['non_existing' => true], GetSetDummy::class)
);
}
public function testDenormalizeShouldNotSetStaticAttribute()
{
$obj = $this->normalizer->denormalize(['staticObject' => true], __NAMESPACE__.'\GetSetDummy');
$obj = $this->normalizer->denormalize(['staticObject' => true], GetSetDummy::class);
$this->assertEquals(new GetSetDummy(), $obj);
$this->assertNull(GetSetDummy::getStaticObject());
@ -590,46 +501,6 @@ class GetSetMethodNormalizerTest extends TestCase
$this->normalizer->normalize($obj, 'any')
);
}
public function testMaxDepth()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$level1 = new MaxDepthDummy();
$level1->bar = 'level1';
$level2 = new MaxDepthDummy();
$level2->bar = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->bar = 'level3';
$level2->child = $level3;
$level4 = new MaxDepthDummy();
$level4->bar = 'level4';
$level3->child = $level4;
$result = $serializer->normalize($level1, null, [GetSetMethodNormalizer::ENABLE_MAX_DEPTH => true]);
$expected = [
'bar' => 'level1',
'child' => [
'bar' => 'level2',
'child' => [
'bar' => 'level3',
'child' => [
'child' => null,
],
],
],
];
$this->assertEquals($expected, $result);
}
}
class GetSetDummy

View File

@ -13,24 +13,50 @@ namespace Symfony\Component\Serializer\Tests\Normalizer;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummyChild;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectToPopulateTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait;
class PropertyNormalizerTest extends TestCase
{
use CallbacksTestTrait;
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;
use ObjectToPopulateTestTrait;
use TypeEnforcementTestTrait;
/**
* @var PropertyNormalizer
*/
private $normalizer;
/**
* @var SerializerInterface
*/
@ -121,146 +147,111 @@ class PropertyNormalizerTest extends TestCase
$this->assertEquals('bar', $obj->getBar());
}
/**
* @dataProvider provideCallbacks
*/
public function testCallbacks($callbacks, $value, $result, $message)
protected function getNormalizerForCallbacks(): PropertyNormalizer
{
$this->doTestCallbacks($callbacks, $value, $result, $message);
return new PropertyNormalizer();
}
/**
* @dataProvider provideCallbacks
*/
public function testLegacyCallbacks($callbacks, $value, $result, $message)
public function testLegacyCallbacks($callbacks, $value, $result)
{
$this->doTestCallbacks($callbacks, $value, $result, $message, true);
}
$this->normalizer->setCallbacks($callbacks);
private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false)
{
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]);
$obj = new PropertyConstructorDummy('', $value);
$obj = new CallbacksObject($value);
$this->assertEquals(
$result,
$this->normalizer->normalize($obj, 'any'),
$message
);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testUncallableCallbacks()
{
$this->doTestUncallableCallbacks();
}
/**
* @expectedException \InvalidArgumentException
*/
public function testLegacyUncallableCallbacks()
{
$this->doTestUncallableCallbacks(true);
}
/**
* @expectedException \InvalidArgumentException
*/
private function doTestUncallableCallbacks(bool $legacy = false)
{
$callbacks = ['bar' => null];
$legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer([PropertyNormalizer::CALLBACKS => $callbacks]);
$obj = new PropertyConstructorDummy('baz', 'quux');
$this->normalizer->normalize($obj, 'any');
}
public function testIgnoredAttributes()
{
$this->doTestIgnoredAttributes();
}
public function testLegacyIgnoredAttributes()
{
$this->doTestIgnoredAttributes(true);
}
private function doTestIgnoredAttributes(bool $legacy = false)
{
$ignoredAttributes = ['foo', 'bar', 'camelCase'];
$legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer([PropertyNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes]);
$obj = new PropertyDummy();
$obj->foo = 'foo';
$obj->setBar('bar');
$this->assertEquals(
[],
$this->normalizer->normalize($obj, 'any')
);
}
public function testGroupsNormalize()
/**
* @dataProvider provideInvalidCallbacks
*/
public function testLegacyUncallableCallbacks($callbacks)
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$this->expectException(InvalidArgumentException::class);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals([
'bar' => 'bar',
], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['c']]));
// The PropertyNormalizer is also able to hydrate properties from parent classes
$this->assertEquals([
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
'kevin' => 'kevin',
'coopTilleuls' => 'coopTilleuls',
], $this->normalizer->normalize($obj, null, [PropertyNormalizer::GROUPS => ['a', 'c']]));
$this->normalizer->setCallbacks($callbacks);
}
public function testGroupsDenormalize()
protected function getNormalizerForCircularReference(): PropertyNormalizer
{
$normalizer = new PropertyNormalizer();
new Serializer([$normalizer]);
return $normalizer;
}
protected function getSelfReferencingModel()
{
return new PropertyCircularReferenceDummy();
}
public function testLegacyUnableToNormalizeCircularReference()
{
$this->normalizer->setCircularReferenceLimit(2);
new Serializer([$this->normalizer]);
$obj = new PropertyCircularReferenceDummy();
$this->expectException(CircularReferenceException::class);
$this->normalizer->normalize($obj);
}
public function testSiblingReference()
{
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$siblingHolder = new PropertySiblingHolder();
$expected = [
'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
];
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
}
public function testLegacyCircularReferenceHandler()
{
$this->normalizer->setCircularReferenceHandler(function ($obj) {
return \get_class($obj);
});
new Serializer([$this->normalizer]);
$obj = new PropertyCircularReferenceDummy();
$expected = ['me' => PropertyCircularReferenceDummy::class];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
}
protected function getDenormalizerForConstructArguments(): PropertyNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$denormalizer = new PropertyNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory));
$serializer = new Serializer([$denormalizer]);
$denormalizer->setSerializer($serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
return $denormalizer;
}
$toNormalize = ['foo' => 'foo', 'bar' => 'bar'];
protected function getNormalizerForGroups(): PropertyNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[PropertyNormalizer::GROUPS => ['a']]
);
$this->assertEquals($obj, $normalized);
return new PropertyNormalizer($classMetadataFactory);
}
$obj->setBar('bar');
protected function getDenormalizerForGroups(): PropertyNormalizer
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
[PropertyNormalizer::GROUPS => ['a', 'b']]
);
$this->assertEquals($obj, $normalized);
return new PropertyNormalizer($classMetadataFactory);
}
public function testGroupsNormalizeWithNameConverter()
@ -305,132 +296,71 @@ class PropertyNormalizerTest extends TestCase
);
}
public function provideCallbacks()
protected function getDenormalizerForIgnoredAttributes(): PropertyNormalizer
{
return [
[
[
'bar' => function ($bar) {
return 'baz';
},
],
'baz',
['foo' => '', 'bar' => 'baz'],
'Change a string',
],
[
[
'bar' => function ($bar) {
},
],
'baz',
['foo' => '', 'bar' => null],
'Null an item',
],
[
[
'bar' => function ($bar) {
return $bar->format('d-m-Y H:i:s');
},
],
new \DateTime('2011-09-10 06:30:00'),
['foo' => '', 'bar' => '10-09-2011 06:30:00'],
'Format a date',
],
[
[
'bar' => function ($bars) {
$foos = '';
foreach ($bars as $bar) {
$foos .= $bar->getFoo();
}
$normalizer = new PropertyNormalizer();
// instantiate a serializer with the normalizer to handle normalizing recursive structures
new Serializer([$normalizer]);
return $foos;
},
],
[new PropertyConstructorDummy('baz', ''), new PropertyConstructorDummy('quux', '')],
['foo' => '', 'bar' => 'bazquux'],
'Collect a property',
],
[
[
'bar' => function ($bars) {
return \count($bars);
},
],
[new PropertyConstructorDummy('baz', ''), new PropertyConstructorDummy('quux', '')],
['foo' => '', 'bar' => 2],
'Count a property',
],
];
return $normalizer;
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testUnableToNormalizeCircularReference()
protected function getNormalizerForIgnoredAttributes(): PropertyNormalizer
{
$this->doTestUnableToNormalizeCircularReference();
$normalizer = new PropertyNormalizer();
// instantiate a serializer with the normalizer to handle normalizing recursive structures
new Serializer([$normalizer]);
return $normalizer;
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
*/
public function testLegacyUnableToNormalizeCircularReference()
public function testIgnoredAttributesContextDenormalizeInherit()
{
$this->doTestUnableToNormalizeCircularReference(true);
$this->markTestSkipped('This has not been tested previously - did not manage to make the test work');
}
private function doTestUnableToNormalizeCircularReference(bool $legacy = false)
public function testLegacyIgnoredAttributes()
{
$legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer([PropertyNormalizer::CIRCULAR_REFERENCE_LIMIT => 2]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$ignoredAttributes = ['foo', 'bar', 'camelCase'];
$this->normalizer->setIgnoredAttributes($ignoredAttributes);
$obj = new PropertyCircularReferenceDummy();
$obj = new PropertyDummy();
$obj->foo = 'foo';
$obj->setBar('bar');
$this->normalizer->normalize($obj);
$this->assertEquals(
[],
$this->normalizer->normalize($obj, 'any')
);
}
public function testSiblingReference()
protected function getNormalizerForMaxDepth(): PropertyNormalizer
{
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new PropertyNormalizer($classMetadataFactory);
$serializer = new Serializer([$normalizer]);
$normalizer->setSerializer($serializer);
$siblingHolder = new PropertySiblingHolder();
$expected = [
'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
];
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
return $normalizer;
}
public function testCircularReferenceHandler()
protected function getDenormalizerForObjectToPopulate(): PropertyNormalizer
{
$this->doTestCircularReferenceHandler();
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new PropertyNormalizer($classMetadataFactory, null, new PhpDocExtractor());
new Serializer([$normalizer]);
return $normalizer;
}
public function testLegacyCircularReferenceHandler()
protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface
{
$this->doTestCircularReferenceHandler(true);
}
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new PropertyNormalizer(null, null, $extractor);
$serializer = new Serializer([new ArrayDenormalizer(), $normalizer]);
$normalizer->setSerializer($serializer);
private function doTestCircularReferenceHandler(bool $legacy = false)
{
$handler = function ($obj) {
return \get_class($obj);
};
$legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer([PropertyNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler]);
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
$obj = new PropertyCircularReferenceDummy();
$expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy'];
$this->assertEquals($expected, $this->normalizer->normalize($obj));
return $normalizer;
}
public function testDenormalizeNonExistingAttribute()
@ -475,42 +405,6 @@ class PropertyNormalizerTest extends TestCase
$this->assertFalse($this->normalizer->supportsNormalization(new StaticPropertyDummy()));
}
public function testMaxDepth()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($serializer);
$level1 = new MaxDepthDummy();
$level1->foo = 'level1';
$level2 = new MaxDepthDummy();
$level2->foo = 'level2';
$level1->child = $level2;
$level3 = new MaxDepthDummy();
$level3->foo = 'level3';
$level2->child = $level3;
$result = $serializer->normalize($level1, null, [PropertyNormalizer::ENABLE_MAX_DEPTH => true]);
$expected = [
'foo' => 'level1',
'child' => [
'foo' => 'level2',
'child' => [
'child' => null,
'bar' => null,
],
'bar' => null,
],
'bar' => null,
];
$this->assertEquals($expected, $result);
}
public function testInheritedPropertiesSupport()
{
$this->assertTrue($this->normalizer->supportsNormalization(new PropertyChildDummy()));