diff --git a/UPGRADE-6.3.md b/UPGRADE-6.3.md index 85db3e4b6cb3c..4627502232a3f 100644 --- a/UPGRADE-6.3.md +++ b/UPGRADE-6.3.md @@ -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 --------------- diff --git a/UPGRADE-7.0.md b/UPGRADE-7.0.md index 751ef30711d33..6b424c02f50a0 100644 --- a/UPGRADE-7.0.md +++ b/UPGRADE-7.0.md @@ -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. diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 3595f75017c6d..1b5705ea6a1fc 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.3 +--- + + * Add support of `list` typed properties + 6.1 --- diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index 6985e2d55e939..6e940922154e5 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -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], ]; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 99a5e3d0a4dc4..62350ecc3972d 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -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)]], @@ -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)]], diff --git a/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php index e871ed49f7b2a..3eb831ed1c5ea 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php @@ -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() @@ -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); diff --git a/src/Symfony/Component/PropertyInfo/Type.php b/src/Symfony/Component/PropertyInfo/Type.php index 2a33b14467276..3288dd1537761 100644 --- a/src/Symfony/Component/PropertyInfo/Type.php +++ b/src/Symfony/Component/PropertyInfo/Type.php @@ -69,6 +69,7 @@ class Type private $collection; private $collectionKeyType; private $collectionValueType; + private $list; /** * @param Type[]|Type|null $collectionKeyType @@ -76,7 +77,7 @@ class Type * * @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)); @@ -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 @@ -162,4 +164,9 @@ public function getCollectionValueTypes(): array { return $this->collectionValueType; } + + public function isList(): bool + { + return $this->list; + } } diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index 6cf083bb4ee86..a84a493bcddb0 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -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()); @@ -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 is converted to x[] which is handled above // so it's only necessary to handle array here $collectionKeyType = $this->getTypes($type->getKeyType())[0]; @@ -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) { diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php index 0a02071ec70b7..a2f1e829f226b 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php @@ -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)]; @@ -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)], diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 19eaa6a9cb8a4..431d865759eb6 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -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 --- diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index a02a46b9415a6..8b89fb9909cce 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -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; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 4cc6686586e89..39980e4c44b6e 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -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; @@ -47,6 +48,8 @@ class AbstractObjectNormalizerTest extends TestCase { + use ExpectDeprecationTrait; + public function testDenormalize() { $normalizer = new AbstractObjectNormalizerDummy(); @@ -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 @@ -710,6 +767,12 @@ class StringCollection public $children; } +class ListCollection +{ + /** @var list */ + public $list; +} + class DummyCollection { /** @var DummyChild[] */ diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index dfb0a928dadb8..9092b8f56873f 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -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",