[Serializer] Construct annotations using named arguments

This commit is contained in:
Alexander M. Turek 2021-04-05 16:40:33 +02:00
parent 3ca3de50c3
commit c11666264d
14 changed files with 300 additions and 53 deletions

View File

@ -92,7 +92,7 @@ PropertyInfo
Routing
-------
* Deprecated creating instances of the `Route` annotation class by passing an array of parameters, use named arguments instead
* Deprecate creating instances of the `Route` annotation class by passing an array of parameters, use named arguments instead
Security
--------
@ -209,7 +209,8 @@ SecurityBundle
Serializer
----------
* Deprecated `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead
* Deprecate `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead
* Deprecate creating instances of the annotation classes by passing an array of parameters, use named arguments instead
Uid
---

View File

@ -305,6 +305,7 @@ Serializer
* Removed `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead.
* `ArrayDenormalizer` does not implement `SerializerAwareInterface` anymore.
* The annotation classes cannot be constructed by passing an array of parameters as first argument anymore, use named arguments instead
TwigBundle
----------

View File

@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
* Annotation class for @Context().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
@ -30,19 +31,26 @@ final class Context
private $groups;
/**
* @param string|string[] $groups
*
* @throws InvalidArgumentException
*/
public function __construct(array $options = [], array $context = [], array $normalizationContext = [], array $denormalizationContext = [], array $groups = [])
public function __construct(array $options = [], array $context = [], array $normalizationContext = [], array $denormalizationContext = [], $groups = [])
{
if (!$context) {
if (!array_intersect((array_keys($options)), ['normalizationContext', 'groups', 'context', 'value', 'denormalizationContext'])) {
// gracefully supports context as first, unnamed attribute argument if it cannot be confused with Doctrine-style options
$context = $options;
} else {
trigger_deprecation('symfony/serializer', '5.3', 'Passing an array of properties as first argument to "%s" is deprecated. Use named arguments instead.', __METHOD__);
// If at least one of the options match, it's likely to be Doctrine-style options. Search for the context inside:
$context = $options['value'] ?? $options['context'] ?? [];
}
}
if (!\is_string($groups) && !\is_array($groups)) {
throw new \TypeError(sprintf('"%s": Expected parameter $groups to be a string or an array of strings, got "%s".', __METHOD__, get_debug_type($groups)));
}
$normalizationContext = $options['normalizationContext'] ?? $normalizationContext;
$denormalizationContext = $options['denormalizationContext'] ?? $denormalizationContext;

View File

@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
* Annotation class for @DiscriminatorMap().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"CLASS"})
*
* @author Samuel Roze <samuel.roze@gmail.com>
@ -35,13 +36,15 @@ class DiscriminatorMap
private $mapping;
/**
* @param string|array $typeProperty
* @param string $typeProperty
*
* @throws InvalidArgumentException
*/
public function __construct($typeProperty, array $mapping = null)
{
if (\is_array($typeProperty)) {
trigger_deprecation('symfony/serializer', '5.3', 'Passing an array as first argument to "%s" is deprecated. Use named arguments instead.', __METHOD__);
$mapping = $typeProperty['mapping'] ?? null;
$typeProperty = $typeProperty['typeProperty'] ?? null;
} elseif (!\is_string($typeProperty)) {

View File

@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
* Annotation class for @Groups().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Kévin Dunglas <dunglas@gmail.com>
@ -30,11 +31,19 @@ class Groups
private $groups;
/**
* @param string|string[] $groups
*
* @throws InvalidArgumentException
*/
public function __construct(array $groups)
public function __construct($groups)
{
if (isset($groups['value'])) {
if (\is_string($groups)) {
$groups = (array) $groups;
} elseif (!\is_array($groups)) {
throw new \TypeError(sprintf('"%s": Parameter $groups is expected to be a string or an array of strings, got "%s".', __METHOD__, get_debug_type($groups)));
} elseif (isset($groups['value'])) {
trigger_deprecation('symfony/serializer', '5.3', 'Passing an array of properties as first argument to "%s" is deprecated. Use named arguments instead.', __METHOD__);
$groups = (array) $groups['value'];
}
if (empty($groups)) {

View File

@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
* Annotation class for @MaxDepth().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Kévin Dunglas <dunglas@gmail.com>
@ -30,11 +31,13 @@ class MaxDepth
private $maxDepth;
/**
* @param int|array $maxDepth
* @param int $maxDepth
*/
public function __construct($maxDepth)
{
if (\is_array($maxDepth)) {
trigger_deprecation('symfony/serializer', '5.3', 'Passing an array as first argument to "%s" is deprecated. Use named arguments instead.', __METHOD__);
if (!isset($maxDepth['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" should be set.', static::class));
}

View File

@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
* Annotation class for @SerializedName().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
@ -30,11 +31,13 @@ final class SerializedName
private $serializedName;
/**
* @param string|array $serializedName
* @param string $serializedName
*/
public function __construct($serializedName)
{
if (\is_array($serializedName)) {
trigger_deprecation('symfony/serializer', '5.3', 'Passing an array as first argument to "%s" is deprecated. Use named arguments instead.', __METHOD__);
if (!isset($serializedName['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" should be set.', static::class));
}

View File

@ -8,6 +8,7 @@ CHANGELOG
* Deprecate `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead
* Add normalization formats to `UidNormalizer`
* Add `CsvEncoder::END_OF_LINE` context option
* Deprecate creating instances of the annotation classes by passing an array of parameters, use named arguments instead
5.2.0
-----

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\VarDumper\Dumper\CliDumper;
@ -22,6 +23,7 @@ use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
*/
class ContextTest extends TestCase
{
use ExpectDeprecationTrait;
use VarDumperTestTrait;
protected function setUp(): void
@ -29,10 +31,19 @@ class ContextTest extends TestCase
$this->setUpVarDumper([], CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_TRAILING_COMMA);
}
public function testThrowsOnEmptyContext()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('At least one of the "context", "normalizationContext", or "denormalizationContext" options of annotation "Symfony\Component\Serializer\Annotation\Context" must be provided as a non-empty array.');
new Context();
}
/**
* @dataProvider provideTestThrowsOnEmptyContextData
* @group legacy
* @dataProvider provideTestThrowsOnEmptyContextLegacyData
*/
public function testThrowsOnEmptyContext(callable $factory)
public function testThrowsOnEmptyContextLegacy(callable $factory)
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('At least one of the "context", "normalizationContext", or "denormalizationContext" options of annotation "Symfony\Component\Serializer\Annotation\Context" must be provided as a non-empty array.');
@ -40,22 +51,15 @@ class ContextTest extends TestCase
$factory();
}
public function provideTestThrowsOnEmptyContextData(): iterable
public function provideTestThrowsOnEmptyContextLegacyData(): iterable
{
yield 'constructor: empty args' => [function () { new Context([]); }];
yield 'doctrine-style: value option as empty array' => [function () { new Context(['value' => []]); }];
yield 'doctrine-style: context option as empty array' => [function () { new Context(['context' => []]); }];
yield 'doctrine-style: context option not provided' => [function () { new Context(['groups' => ['group_1']]); }];
if (\PHP_VERSION_ID >= 80000) {
yield 'named args: empty context' => [function () {
eval('return new Symfony\Component\Serializer\Annotation\Context(context: []);');
}];
}
}
/**
* @group legacy
* @dataProvider provideTestThrowsOnNonArrayContextData
*/
public function testThrowsOnNonArrayContext(array $options)
@ -73,11 +77,25 @@ class ContextTest extends TestCase
yield 'non-array denormalization context' => [['normalizationContext' => 'not_an_array']];
}
/**
* @requires PHP 8
*/
public function testInvalidGroupOption()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Parameter "groups" of annotation "%s" must be a string or an array of strings. Got "stdClass"', Context::class));
new Context(...['context' => ['foo' => 'bar'], 'groups' => ['fine', new \stdClass()]]);
}
/**
* @group legacy
*/
public function testInvalidGroupOptionLegacy()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Parameter "groups" of annotation "%s" must be a string or an array of strings. Got "stdClass"', Context::class));
new Context(['context' => ['foo' => 'bar'], 'groups' => ['fine', new \stdClass()]]);
}
@ -110,18 +128,19 @@ class ContextTest extends TestCase
}
/**
* @requires PHP 8
* @dataProvider provideValidInputs
*/
public function testValidInputs(callable $factory, string $expectedDump)
{
self::assertDumpEquals($expectedDump, $factory());
$this->assertDumpEquals($expectedDump, $factory());
}
public function provideValidInputs(): iterable
{
yield 'doctrine-style: with context option' => [
function () { return new Context(['context' => ['foo' => 'bar']]); },
$expected = <<<DUMP
yield 'named arguments: with context option' => [
function () { return new Context(...['context' => ['foo' => 'bar']]); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
@ -133,14 +152,9 @@ Symfony\Component\Serializer\Annotation\Context {
DUMP
];
yield 'constructor: with context arg' => [
function () { return new Context([], ['foo' => 'bar']); },
$expected,
];
yield 'doctrine-style: with normalization context option' => [
function () { return new Context(['normalizationContext' => ['foo' => 'bar']]); },
$expected = <<<DUMP
yield 'named arguments: with normalization context option' => [
function () { return new Context(...['normalizationContext' => ['foo' => 'bar']]); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: []
-normalizationContext: [
@ -152,14 +166,9 @@ Symfony\Component\Serializer\Annotation\Context {
DUMP
];
yield 'constructor: with normalization context arg' => [
function () { return new Context([], [], ['foo' => 'bar']); },
$expected,
];
yield 'doctrine-style: with denormalization context option' => [
function () { return new Context(['denormalizationContext' => ['foo' => 'bar']]); },
$expected = <<<DUMP
yield 'named arguments: with denormalization context option' => [
function () { return new Context(...['denormalizationContext' => ['foo' => 'bar']]); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: []
-normalizationContext: []
@ -171,9 +180,92 @@ Symfony\Component\Serializer\Annotation\Context {
DUMP
];
yield 'constructor: with denormalization context arg' => [
function () { return new Context([], [], [], ['foo' => 'bar']); },
$expected,
yield 'named arguments: with groups option as string' => [
function () { return new Context(...['context' => ['foo' => 'bar'], 'groups' => 'a']); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
]
-normalizationContext: []
-denormalizationContext: []
-groups: [
"a",
]
}
DUMP
];
yield 'named arguemnts: with groups option as array' => [
function () { return new Context(...['context' => ['foo' => 'bar'], 'groups' => ['a', 'b']]); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
]
-normalizationContext: []
-denormalizationContext: []
-groups: [
"a",
"b",
]
}
DUMP
];
}
/**
* @group legacy
* @dataProvider provideValidLegacyInputs
*/
public function testValidLegacyInputs(callable $factory, string $expectedDump)
{
$this->expectDeprecation('Since symfony/serializer 5.3: Passing an array of properties as first argument to "Symfony\Component\Serializer\Annotation\Context::__construct" is deprecated. Use named arguments instead.');
$this->assertDumpEquals($expectedDump, $factory());
}
public function provideValidLegacyInputs(): iterable
{
yield 'doctrine-style: with context option' => [
function () { return new Context(['context' => ['foo' => 'bar']]); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
]
-normalizationContext: []
-denormalizationContext: []
-groups: []
}
DUMP
];
yield 'doctrine-style: with normalization context option' => [
function () { return new Context(['normalizationContext' => ['foo' => 'bar']]); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: []
-normalizationContext: [
"foo" => "bar",
]
-denormalizationContext: []
-groups: []
}
DUMP
];
yield 'doctrine-style: with denormalization context option' => [
function () { return new Context(['denormalizationContext' => ['foo' => 'bar']]); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: []
-normalizationContext: []
-denormalizationContext: [
"foo" => "bar",
]
-groups: []
}
DUMP
];
yield 'doctrine-style: with groups option as string' => [
@ -194,7 +286,7 @@ DUMP
yield 'doctrine-style: with groups option as array' => [
function () { return new Context(['context' => ['foo' => 'bar'], 'groups' => ['a', 'b']]); },
$expected = <<<DUMP
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
@ -208,10 +300,5 @@ Symfony\Component\Serializer\Annotation\Context {
}
DUMP
];
yield 'constructor: with groups arg' => [
function () { return new Context([], ['foo' => 'bar'], [], [], ['a', 'b']); },
$expected,
];
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
@ -20,8 +21,31 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
*/
class DiscriminatorMapTest extends TestCase
{
use ExpectDeprecationTrait;
/**
* @requires PHP 8
*/
public function testGetTypePropertyAndMapping()
{
$annotation = new DiscriminatorMap(...['typeProperty' => 'type', 'mapping' => [
'foo' => 'FooClass',
'bar' => 'BarClass',
]]);
$this->assertEquals('type', $annotation->getTypeProperty());
$this->assertEquals([
'foo' => 'FooClass',
'bar' => 'BarClass',
], $annotation->getMapping());
}
/**
* @group legacy
*/
public function testGetTypePropertyAndMappingLegacy()
{
$this->expectDeprecation('Since symfony/serializer 5.3: Passing an array as first argument to "Symfony\Component\Serializer\Annotation\DiscriminatorMap::__construct" is deprecated. Use named arguments instead.');
$annotation = new DiscriminatorMap(['typeProperty' => 'type', 'mapping' => [
'foo' => 'FooClass',
'bar' => 'BarClass',
@ -34,25 +58,64 @@ class DiscriminatorMapTest extends TestCase
], $annotation->getMapping());
}
/**
* @group legacy
*/
public function testExceptionWithoutTypeProperty()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(['mapping' => ['foo' => 'FooClass']]);
}
/**
* @requires PHP 8
*/
public function testExceptionWithEmptyTypeProperty()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(...['typeProperty' => '', 'mapping' => ['foo' => 'FooClass']]);
}
/**
* @group legacy
*/
public function testExceptionWithEmptyTypePropertyLegacy()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(['typeProperty' => '', 'mapping' => ['foo' => 'FooClass']]);
}
/**
* @requires PHP 8
*/
public function testExceptionWithoutMappingProperty()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(...['typeProperty' => 'type']);
}
/**
* @group legacy
*/
public function testExceptionWithoutMappingPropertyLegacy()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(['typeProperty' => 'type']);
}
/**
* @requires PHP 8
*/
public function testExceptionWitEmptyMappingProperty()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(...['typeProperty' => 'type', 'mapping' => []]);
}
/**
* @group legacy
*/
public function testExceptionWitEmptyMappingPropertyLegacy()
{
$this->expectException(InvalidArgumentException::class);
new DiscriminatorMap(['typeProperty' => 'type', 'mapping' => []]);

View File

@ -21,11 +21,23 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
class GroupsTest extends TestCase
{
public function testEmptyGroupsParameter()
{
$this->expectException(InvalidArgumentException::class);
new Groups([]);
}
/**
* @group legacy
*/
public function testEmptyGroupsParameterLegacy()
{
$this->expectException(InvalidArgumentException::class);
new Groups(['value' => []]);
}
/**
* @group legacy
*/
public function testNotAnArrayGroupsParameter()
{
$this->expectException(InvalidArgumentException::class);
@ -35,18 +47,38 @@ class GroupsTest extends TestCase
public function testInvalidGroupsParameter()
{
$this->expectException(InvalidArgumentException::class);
new Groups(['value' => ['a', 1, new \stdClass()]]);
new Groups(['a', 1, new \stdClass()]);
}
public function testGroupsParameters()
{
$validData = ['a', 'b'];
$groups = new Groups($validData);
$this->assertEquals($validData, $groups->getGroups());
}
/**
* @group legacy
*/
public function testGroupsParametersLegacy()
{
$validData = ['a', 'b'];
$groups = new Groups(['value' => $validData]);
$this->assertEquals($validData, $groups->getGroups());
}
public function testSingleGroup()
{
$groups = new Groups('a');
$this->assertEquals(['a'], $groups->getGroups());
}
/**
* @group legacy
*/
public function testSingleGroupLegacy()
{
$groups = new Groups(['value' => 'a']);
$this->assertEquals(['a'], $groups->getGroups());

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
@ -20,6 +21,11 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
*/
class MaxDepthTest extends TestCase
{
use ExpectDeprecationTrait;
/**
* @group legacy
*/
public function testNotSetMaxDepthParameter()
{
$this->expectException(InvalidArgumentException::class);
@ -44,11 +50,22 @@ class MaxDepthTest extends TestCase
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Parameter of annotation "Symfony\Component\Serializer\Annotation\MaxDepth" must be a positive integer.');
new MaxDepth(['value' => $value]);
new MaxDepth($value);
}
public function testMaxDepthParameters()
{
$maxDepth = new MaxDepth(3);
$this->assertEquals(3, $maxDepth->getMaxDepth());
}
/**
* @group legacy
*/
public function testMaxDepthParametersLegacy()
{
$this->expectDeprecation('Since symfony/serializer 5.3: Passing an array as first argument to "Symfony\Component\Serializer\Annotation\MaxDepth::__construct" is deprecated. Use named arguments instead.');
$maxDepth = new MaxDepth(['value' => 3]);
$this->assertEquals(3, $maxDepth->getMaxDepth());
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
@ -20,6 +21,11 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
*/
class SerializedNameTest extends TestCase
{
use ExpectDeprecationTrait;
/**
* @group legacy
*/
public function testNotSetSerializedNameParameter()
{
$this->expectException(InvalidArgumentException::class);
@ -27,7 +33,7 @@ class SerializedNameTest extends TestCase
new SerializedName([]);
}
public function provideInvalidValues()
public function provideInvalidValues(): array
{
return [
[''],
@ -42,7 +48,8 @@ class SerializedNameTest extends TestCase
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Parameter of annotation "Symfony\Component\Serializer\Annotation\SerializedName" must be a non-empty string.');
new SerializedName(['value' => $value]);
new SerializedName($value);
}
public function testSerializedNameParameters()
@ -50,4 +57,15 @@ class SerializedNameTest extends TestCase
$maxDepth = new SerializedName(['value' => 'foo']);
$this->assertEquals('foo', $maxDepth->getSerializedName());
}
/**
* @group legacy
*/
public function testSerializedNameParametersLegacy()
{
$this->expectDeprecation('Since symfony/serializer 5.3: Passing an array as first argument to "Symfony\Component\Serializer\Annotation\SerializedName::__construct" is deprecated. Use named arguments instead.');
$maxDepth = new SerializedName(['value' => 'foo']);
$this->assertEquals('foo', $maxDepth->getSerializedName());
}
}

View File

@ -22,7 +22,7 @@
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"doctrine/annotations": "^1.10.4",
"doctrine/annotations": "^1.12",
"doctrine/cache": "~1.0",
"phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0",
"symfony/cache": "^4.4|^5.0",
@ -43,6 +43,7 @@
"symfony/yaml": "^4.4|^5.0"
},
"conflict": {
"doctrine/annotations": "<1.12",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/dependency-injection": "<4.4",