Skip to content

[PropertyInfo][Serializer] Add support of list and trigger deprecation when filling a list-typed property with an array #49056

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPGRADE-6.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ DependencyInjection

* Deprecate `PhpDumper` options `inline_factories_parameter` and `inline_class_loader_parameter`, use `inline_factories` and `inline_class_loader` instead
* Deprecate undefined and numeric keys with `service_locator` config, use string aliases instead
* Deprecate denormalizing an array that is not a list into a `list` typed property with the `Serializer` component

FrameworkBundle
---------------
Expand Down
11 changes: 11 additions & 0 deletions UPGRADE-7.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,15 @@ Symfony 6.4 and Symfony 7.0 will be released simultaneously at the end of Novemb
release process, both versions will have the same features, but Symfony 7.0 won't include any deprecated features.
To upgrade, make sure to resolve all deprecation notices.

Serializer
----------

* Values being denormalized into `list` typed properties must be list themselves, otherwise the property must be typed with `array`

Workflow
--------

* The first argument of `WorkflowDumpCommand` must be a `ServiceLocator` of all
workflows indexed by names

This file will be updated on the branch 7.0 for each deprecated feature that is removed.
5 changes: 5 additions & 0 deletions src/Symfony/Component/PropertyInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

6.3
---

* Add support of `list` typed properties

6.1
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static function typesProvider()
['emptyVar', null, 'This should not be removed.', null],
['arrayWithKeys', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_STRING))], null, null],
['arrayOfMixed', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), null)], null, null],
['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], null, null],
['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING), true)], null, null],
['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], null, null],
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static function typesProvider()
['emptyVar', null],
['arrayWithKeys', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_STRING))]],
['arrayOfMixed', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), null)]],
['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]],
['listOfStrings', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING), true)]],
['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]],
['rootDummyItems', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, RootDummyItem::class))]],
['rootDummyItem', [new Type(Type::BUILTIN_TYPE_OBJECT, false, RootDummyItem::class)]],
Expand Down Expand Up @@ -411,7 +411,7 @@ public static function pseudoTypesProvider(): array
['positiveInt', [new Type(Type::BUILTIN_TYPE_INT, false, null)]],
['negativeInt', [new Type(Type::BUILTIN_TYPE_INT, false, null)]],
['nonEmptyArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]],
['nonEmptyList', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))]],
['nonEmptyList', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), list: true)]],
['scalar', [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT), new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_BOOL)]],
['number', [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT)]],
['numeric', [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT), new Type(Type::BUILTIN_TYPE_STRING)]],
Expand Down
7 changes: 7 additions & 0 deletions src/Symfony/Component/PropertyInfo/Tests/TypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public function testConstruct()
$this->assertIsArray($collectionValueTypes);
$this->assertContainsOnlyInstancesOf('Symfony\Component\PropertyInfo\Type', $collectionValueTypes);
$this->assertEquals(Type::BUILTIN_TYPE_STRING, $collectionValueTypes[0]->getBuiltinType());
$this->assertFalse($type->isList());
}

public function testIterable()
Expand All @@ -45,6 +46,12 @@ public function testIterable()
$this->assertSame('iterable', $type->getBuiltinType());
}

public function testList()
{
$type = new Type('array', list: true);
$this->assertTrue($type->isList());
}

public function testInvalidType()
{
$this->expectException(\InvalidArgumentException::class);
Expand Down
9 changes: 8 additions & 1 deletion src/Symfony/Component/PropertyInfo/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ class Type
private $collection;
private $collectionKeyType;
private $collectionValueType;
private $list;

/**
* @param Type[]|Type|null $collectionKeyType
* @param Type[]|Type|null $collectionValueType
*
* @throws \InvalidArgumentException
*/
public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, array|Type $collectionKeyType = null, array|Type $collectionValueType = null)
public function __construct(string $builtinType, bool $nullable = false, string $class = null, bool $collection = false, array|Type $collectionKeyType = null, array|Type $collectionValueType = null, bool $list = false)
{
if (!\in_array($builtinType, self::$builtinTypes)) {
throw new \InvalidArgumentException(sprintf('"%s" is not a valid PHP type.', $builtinType));
Expand All @@ -88,6 +89,7 @@ public function __construct(string $builtinType, bool $nullable = false, string
$this->collection = $collection;
$this->collectionKeyType = $this->validateCollectionArgument($collectionKeyType, 5, '$collectionKeyType') ?? [];
$this->collectionValueType = $this->validateCollectionArgument($collectionValueType, 6, '$collectionValueType') ?? [];
$this->list = $list;
}

private function validateCollectionArgument(array|Type|null $collectionArgument, int $argumentIndex, string $argumentName): ?array
Expand Down Expand Up @@ -162,4 +164,9 @@ public function getCollectionValueTypes(): array
{
return $this->collectionValueType;
}

public function isList(): bool
{
return $this->list;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ private function createType(DocType $type, bool $nullable, string $docType = nul
$fqsen = $type->getFqsen();
if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) {
// Workaround for phpdocumentor/type-resolver < 1.6
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $this->getTypes($type->getValueType()));
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $this->getTypes($type->getValueType()), list: true);
}

[$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen);
[$phpType, $class] = $this->getPhpTypeAndClass((string) $type->getFqsen());

$key = $this->getTypes($type->getKeyType());
$value = $this->getTypes($type->getValueType());
Expand All @@ -141,7 +141,7 @@ private function createType(DocType $type, bool $nullable, string $docType = nul
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType);
}

if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) {
if ((($list = str_starts_with($docType, 'list<')) || str_starts_with($docType, 'array<')) && $type instanceof Array_) {
// array<value> is converted to x[] which is handled above
// so it's only necessary to handle array<key, value> here
$collectionKeyType = $this->getTypes($type->getKeyType())[0];
Expand All @@ -154,7 +154,7 @@ private function createType(DocType $type, bool $nullable, string $docType = nul
$collectionValueType = $collectionValueTypes[0];
}

return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType);
return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType, $list);
}

if ($type instanceof PseudoType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
}
}

return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), true, $collectionKeyTypes, $collectionKeyValues)];
return [new Type($mainType->getBuiltinType(), $mainType->isNullable(), $mainType->getClassName(), true, $collectionKeyTypes, $collectionKeyValues, list: 'list' === $node->type->name)];
}
if ($node instanceof ArrayShapeNode) {
return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)];
Expand Down Expand Up @@ -175,7 +175,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
'negative-int' => [new Type(Type::BUILTIN_TYPE_INT)],
'double' => [new Type(Type::BUILTIN_TYPE_FLOAT)],
'list',
'non-empty-list' => [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))],
'non-empty-list' => [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), list: true)],
'non-empty-array' => [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)],
'mixed' => [], // mixed seems to be ignored in all other extractors
'parent' => [new Type(Type::BUILTIN_TYPE_OBJECT, false, $node->name)],
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Add `XmlEncoder::SAVE_OPTIONS` context option
* Deprecate `MissingConstructorArgumentsException` in favor of `MissingConstructorArgumentException`
* Deprecate denormalizing an array that is not a list into a `list` typed property

6.2
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,11 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
return $data;
}

if ($type->isList() && false === array_is_list($data)) {
// In 7.0, an UnexpectedValueException should be raised instead
trigger_deprecation('symfony/serializer', '6.3', 'Denormalizing an array that is not a list into a list-typed property is deprecated.');
}

if (('is_'.$builtinType)($data)) {
return $data;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
Expand Down Expand Up @@ -47,6 +48,8 @@

class AbstractObjectNormalizerTest extends TestCase
{
use ExpectDeprecationTrait;

public function testDenormalize()
{
$normalizer = new AbstractObjectNormalizerDummy();
Expand Down Expand Up @@ -542,6 +545,60 @@ public function testDenormalizeUsesContextAttributeForPropertiesInConstructorWit

$this->assertSame($obj->propertyWithSerializedName->format('Y-m-d'), $obj->propertyWithoutSerializedName->format('Y-m-d'));
}

public function testDenormalizeList()
{
$denormalizer = $this->getDenormalizerForListCollection();

/** @var ListCollection $object */
$object = $denormalizer->denormalize([
'list' => [
1,
2,
3,
],
], ListCollection::class);

$this->assertTrue(array_is_list($object->list));
$this->assertCount(3, $object->list);
}

/**
* @group legacy
*/
public function testDenormalizeArrayInListTypedProperty()
{
$denormalizer = $this->getDenormalizerForListCollection();

$this->expectDeprecation('Since symfony/serializer 6.3: Denormalizing an array that is not a list into a list-typed property is deprecated.');

/** @var ListCollection $object */
$object = $denormalizer->denormalize([
'list' => [
1 => 1,
4 => 2,
'foo' => 3,
],
], ListCollection::class);

$this->assertFalse(array_is_list($object->list));
$this->assertCount(3, $object->list);
}

private function getDenormalizerForListCollection()
{
$extractor = $this->createMock(PhpDocExtractor::class);
$extractor->method('getTypes')
->willReturn(
[new Type('array', false, null, true, new Type('int'), new Type('int'), true)]
);

$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
$serializer = new SerializerCollectionDummy([$denormalizer]);
$denormalizer->setSerializer($serializer);

return $denormalizer;
}
}

class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
Expand Down Expand Up @@ -710,6 +767,12 @@ class StringCollection
public $children;
}

class ListCollection
{
/** @var list<int> */
public $list;
}

class DummyCollection
{
/** @var DummyChild[] */
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Serializer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"symfony/http-kernel": "^5.4|^6.0",
"symfony/mime": "^5.4|^6.0",
"symfony/property-access": "^5.4|^6.0",
"symfony/property-info": "^5.4|^6.0",
"symfony/property-info": "^6.3",
"symfony/uid": "^5.4|^6.0",
"symfony/validator": "^5.4|^6.0",
"symfony/var-dumper": "^5.4|^6.0",
Expand Down