diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md index 9313b5e02f197..46c22b66e3861 100644 --- a/UPGRADE-7.1.md +++ b/UPGRADE-7.1.md @@ -11,6 +11,11 @@ Cache * Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` instead +DoctrineBridge +-------------- + + * The `DoctrineExtractor::getTypes()` method is deprecated, use `DoctrineExtractor::getType()` instead + ExpressionLanguage ------------------ @@ -22,6 +27,11 @@ FrameworkBundle * Mark classes `ConfigBuilderCacheWarmer`, `Router`, `SerializerCacheWarmer`, `TranslationsCacheWarmer`, `Translator` and `ValidatorCacheWarmer` as `final` +PropertyInfo +------------ + + * The `PropertyTypeExtractorInterface::getTypes()` method is deprecated, use `PropertyTypeExtractorInterface::getType()` instead + SecurityBundle -------------- diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 754e6938da402..d50f2a2821717 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Deprecate the `DoctrineExtractor::getTypes()` method, use `DoctrineExtractor::getType()` instead + 7.0 --- @@ -17,7 +22,7 @@ CHANGELOG 6.4 --- - * [BC BREAK] Add argument `$buildDir` to `ProxyCacheWarmer::warmUp()` + * [BC BREAK] Add argument `$buildDir` to `ProxyCacheWarmer::warmUp()` * [BC BREAK] Add return type-hints to `EntityFactory` * Deprecate `DbalLogger`, use a middleware instead * Deprecate not constructing `DoctrineDataCollector` with an instance of `DebugDataHolder` diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index 14d691f485a3b..7af2d5059abf1 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -24,7 +24,9 @@ use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Extracts data using Doctrine ORM and ODM metadata. @@ -55,8 +57,110 @@ public function getProperties(string $class, array $context = []): ?array return $properties; } + public function getType(string $class, string $property, array $context = []): ?Type + { + if (null === $metadata = $this->getMetadata($class)) { + return null; + } + + if ($metadata->hasAssociation($property)) { + $class = $metadata->getAssociationTargetClass($property); + + if ($metadata->isSingleValuedAssociation($property)) { + if ($metadata instanceof ClassMetadata) { + $associationMapping = $metadata->getAssociationMapping($property); + $nullable = $this->isAssociationNullable($associationMapping); + } else { + $nullable = false; + } + + return $nullable ? Type::nullable(Type::object($class)) : Type::object($class); + } + + $collectionKeyType = TypeIdentifier::INT; + + if ($metadata instanceof ClassMetadata) { + $associationMapping = $metadata->getAssociationMapping($property); + + if (self::getMappingValue($associationMapping, 'indexBy')) { + $subMetadata = $this->entityManager->getClassMetadata(self::getMappingValue($associationMapping, 'targetEntity')); + + // Check if indexBy value is a property + $fieldName = self::getMappingValue($associationMapping, 'indexBy'); + if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { + $fieldName = $subMetadata->getFieldForColumn(self::getMappingValue($associationMapping, 'indexBy')); + // Not a property, maybe a column name? + if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { + // Maybe the column name is the association join column? + $associationMapping = $subMetadata->getAssociationMapping($fieldName); + + $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName); + $subMetadata = $this->entityManager->getClassMetadata(self::getMappingValue($associationMapping, 'targetEntity')); + + // Not a property, maybe a column name? + if (null === ($typeOfField = $subMetadata->getTypeOfField($indexProperty))) { + $fieldName = $subMetadata->getFieldForColumn($indexProperty); + $typeOfField = $subMetadata->getTypeOfField($fieldName); + } + } + } + + if (!$collectionKeyType = $this->getTypeIdentifier($typeOfField)) { + return null; + } + } + } + + return Type::collection(Type::object(Collection::class), Type::object($class), Type::builtin($collectionKeyType)); + } + + if ($metadata instanceof ClassMetadata && isset($metadata->embeddedClasses[$property])) { + return Type::object(self::getMappingValue($metadata->embeddedClasses[$property], 'class')); + } + + if (!$metadata->hasField($property)) { + return null; + } + + $typeOfField = $metadata->getTypeOfField($property); + + if (!$typeIdentifier = $this->getTypeIdentifier($typeOfField)) { + return null; + } + + $nullable = $metadata instanceof ClassMetadata && $metadata->isNullable($property); + $enumType = null; + + if (null !== $enumClass = self::getMappingValue($metadata->getFieldMapping($property), 'enumType') ?? null) { + $enumType = $nullable ? Type::nullable(Type::enum($enumClass)) : Type::enum($enumClass); + } + + $builtinType = $nullable ? Type::nullable(Type::builtin($typeIdentifier)) : Type::builtin($typeIdentifier); + + return match ($typeIdentifier) { + TypeIdentifier::OBJECT => match ($typeOfField) { + Types::DATE_MUTABLE, Types::DATETIME_MUTABLE, Types::DATETIMETZ_MUTABLE, 'vardatetime', Types::TIME_MUTABLE => $nullable ? Type::nullable(Type::object(\DateTime::class)) : Type::object(\DateTime::class), + Types::DATE_IMMUTABLE, Types::DATETIME_IMMUTABLE, Types::DATETIMETZ_IMMUTABLE, Types::TIME_IMMUTABLE => $nullable ? Type::nullable(Type::object(\DateTimeImmutable::class)) : Type::object(\DateTimeImmutable::class), + Types::DATEINTERVAL => $nullable ? Type::nullable(Type::object(\DateInterval::class)) : Type::object(\DateInterval::class), + default => $builtinType, + }, + TypeIdentifier::ARRAY => match ($typeOfField) { + 'array', 'json_array' => $enumType ? null : ($nullable ? Type::nullable(Type::array()) : Type::array()), + Types::SIMPLE_ARRAY => $nullable ? Type::nullable(Type::list($enumType ?? Type::string())) : Type::list($enumType ?? Type::string()), + default => $builtinType, + }, + TypeIdentifier::INT, TypeIdentifier::STRING => $enumType ? $enumType : $builtinType, + default => $builtinType, + }; + } + + /** + * @deprecated since Symfony 7.1, use "getType" instead + */ public function getTypes(string $class, string $property, array $context = []): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + if (null === $metadata = $this->getMetadata($class)) { return null; } @@ -73,10 +177,10 @@ public function getTypes(string $class, string $property, array $context = []): $nullable = false; } - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $class)]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, $class)]; } - $collectionKeyType = Type::BUILTIN_TYPE_INT; + $collectionKeyType = LegacyType::BUILTIN_TYPE_INT; if ($metadata instanceof ClassMetadata) { $associationMapping = $metadata->getAssociationMapping($property); @@ -104,61 +208,61 @@ public function getTypes(string $class, string $property, array $context = []): } } - if (!$collectionKeyType = $this->getPhpType($typeOfField)) { + if (!$collectionKeyType = $this->getTypeIdentifierLegacy($typeOfField)) { return null; } } } - return [new Type( - Type::BUILTIN_TYPE_OBJECT, + return [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type($collectionKeyType), - new Type(Type::BUILTIN_TYPE_OBJECT, false, $class) + new LegacyType($collectionKeyType), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, $class) )]; } if ($metadata instanceof ClassMetadata && isset($metadata->embeddedClasses[$property])) { - return [new Type(Type::BUILTIN_TYPE_OBJECT, false, self::getMappingValue($metadata->embeddedClasses[$property], 'class'))]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, self::getMappingValue($metadata->embeddedClasses[$property], 'class'))]; } if ($metadata->hasField($property)) { $typeOfField = $metadata->getTypeOfField($property); - if (!$builtinType = $this->getPhpType($typeOfField)) { + if (!$builtinType = $this->getTypeIdentifierLegacy($typeOfField)) { return null; } $nullable = $metadata instanceof ClassMetadata && $metadata->isNullable($property); $enumType = null; if (null !== $enumClass = self::getMappingValue($metadata->getFieldMapping($property), 'enumType') ?? null) { - $enumType = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); + $enumType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); } switch ($builtinType) { - case Type::BUILTIN_TYPE_OBJECT: + case LegacyType::BUILTIN_TYPE_OBJECT: switch ($typeOfField) { case Types::DATE_MUTABLE: case Types::DATETIME_MUTABLE: case Types::DATETIMETZ_MUTABLE: case 'vardatetime': case Types::TIME_MUTABLE: - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; case Types::DATE_IMMUTABLE: case Types::DATETIME_IMMUTABLE: case Types::DATETIMETZ_IMMUTABLE: case Types::TIME_IMMUTABLE: - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; case Types::DATEINTERVAL: - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; } break; - case Type::BUILTIN_TYPE_ARRAY: + case LegacyType::BUILTIN_TYPE_ARRAY: switch ($typeOfField) { case 'array': // DBAL < 4 case 'json_array': // DBAL < 3 @@ -167,21 +271,21 @@ public function getTypes(string $class, string $property, array $context = []): return null; } - return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; case Types::SIMPLE_ARRAY: - return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $enumType ?? new Type(Type::BUILTIN_TYPE_STRING))]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $enumType ?? new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]; } break; - case Type::BUILTIN_TYPE_INT: - case Type::BUILTIN_TYPE_STRING: + case LegacyType::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_STRING: if ($enumType) { return [$enumType]; } break; } - return [new Type($builtinType, $nullable)]; + return [new LegacyType($builtinType, $nullable)]; } return null; @@ -244,20 +348,52 @@ private function isAssociationNullable(array|AssociationMapping $associationMapp /** * Gets the corresponding built-in PHP type. */ - private function getPhpType(string $doctrineType): ?string + private function getTypeIdentifier(string $doctrineType): ?TypeIdentifier + { + return match ($doctrineType) { + Types::SMALLINT, + Types::INTEGER => TypeIdentifier::INT, + Types::FLOAT => TypeIdentifier::FLOAT, + Types::BIGINT, + Types::STRING, + Types::TEXT, + Types::GUID, + Types::DECIMAL => TypeIdentifier::STRING, + Types::BOOLEAN => TypeIdentifier::BOOL, + Types::BLOB, + Types::BINARY => TypeIdentifier::RESOURCE, + 'object', // DBAL < 4 + Types::DATE_MUTABLE, + Types::DATETIME_MUTABLE, + Types::DATETIMETZ_MUTABLE, + 'vardatetime', + Types::TIME_MUTABLE, + Types::DATE_IMMUTABLE, + Types::DATETIME_IMMUTABLE, + Types::DATETIMETZ_IMMUTABLE, + Types::TIME_IMMUTABLE, + Types::DATEINTERVAL => TypeIdentifier::OBJECT, + 'array', // DBAL < 4 + 'json_array', // DBAL < 3 + Types::SIMPLE_ARRAY => TypeIdentifier::ARRAY, + default => null, + }; + } + + private function getTypeIdentifierLegacy(string $doctrineType): ?string { return match ($doctrineType) { Types::SMALLINT, - Types::INTEGER => Type::BUILTIN_TYPE_INT, - Types::FLOAT => Type::BUILTIN_TYPE_FLOAT, + Types::INTEGER => LegacyType::BUILTIN_TYPE_INT, + Types::FLOAT => LegacyType::BUILTIN_TYPE_FLOAT, Types::BIGINT, Types::STRING, Types::TEXT, Types::GUID, - Types::DECIMAL => Type::BUILTIN_TYPE_STRING, - Types::BOOLEAN => Type::BUILTIN_TYPE_BOOL, + Types::DECIMAL => LegacyType::BUILTIN_TYPE_STRING, + Types::BOOLEAN => LegacyType::BUILTIN_TYPE_BOOL, Types::BLOB, - Types::BINARY => Type::BUILTIN_TYPE_RESOURCE, + Types::BINARY => LegacyType::BUILTIN_TYPE_RESOURCE, 'object', // DBAL < 4 Types::DATE_MUTABLE, Types::DATETIME_MUTABLE, @@ -268,10 +404,10 @@ private function getPhpType(string $doctrineType): ?string Types::DATETIME_IMMUTABLE, Types::DATETIMETZ_IMMUTABLE, Types::TIME_IMMUTABLE, - Types::DATEINTERVAL => Type::BUILTIN_TYPE_OBJECT, + Types::DATEINTERVAL => LegacyType::BUILTIN_TYPE_OBJECT, 'array', // DBAL < 4 'json_array', // DBAL < 3 - Types::SIMPLE_ARRAY => Type::BUILTIN_TYPE_ARRAY, + Types::SIMPLE_ARRAY => LegacyType::BUILTIN_TYPE_ARRAY, default => null, }; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index 4589f01488d56..35919529be459 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -29,7 +29,8 @@ use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineWithEmbedded; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -106,17 +107,22 @@ public function testTestGetPropertiesWithEmbedded() } /** - * @dataProvider typesProvider + * @group legacy + * + * @dataProvider legacyTypesProvider */ - public function testExtract(string $property, ?array $type = null) + public function testExtractLegacy(string $property, ?array $type = null) { $this->assertEquals($type, $this->createExtractor()->getTypes(DoctrineDummy::class, $property, [])); } - public function testExtractWithEmbedded() + /** + * @group legacy + */ + public function testExtractWithEmbeddedLegacy() { - $expectedTypes = [new Type( - Type::BUILTIN_TYPE_OBJECT, + $expectedTypes = [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineEmbeddable::class )]; @@ -130,97 +136,103 @@ public function testExtractWithEmbedded() $this->assertEquals($expectedTypes, $actualTypes); } - public function testExtractEnum() + /** + * @group legacy + */ + public function testExtractEnumLegacy() { - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString', [])); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt', [])); $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumStringArray', [])); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class))], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumIntArray', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumInt::class))], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumIntArray', [])); $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom', [])); } - public static function typesProvider(): array + /** + * @group legacy + */ + public static function legacyTypesProvider(): array { return [ - ['id', [new Type(Type::BUILTIN_TYPE_INT)]], - ['guid', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['bigint', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['time', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], - ['timeImmutable', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], - ['dateInterval', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateInterval')]], - ['float', [new Type(Type::BUILTIN_TYPE_FLOAT)]], - ['decimal', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['bool', [new Type(Type::BUILTIN_TYPE_BOOL)]], - ['binary', [new Type(Type::BUILTIN_TYPE_RESOURCE)]], - ['jsonArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], - ['foo', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation')]], - ['bar', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['id', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['guid', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['bigint', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['time', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTime')]], + ['timeImmutable', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], + ['dateInterval', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateInterval')]], + ['float', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['decimal', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['bool', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], + ['binary', [new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE)]], + ['jsonArray', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true)]], + ['foo', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation')]], + ['bar', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedRguid', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedRguid', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedBar', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedBar', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedFoo', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedFoo', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], - ['indexedBaz', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedBaz', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], - ['simpleArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]], + ['simpleArray', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], ['customFoo', null], ['notMapped', null], - ['indexedByDt', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedByDt', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_OBJECT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], ['indexedByCustomType', null], - ['indexedBuz', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['indexedBuz', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], - ['dummyGeneratedValueList', [new Type( - Type::BUILTIN_TYPE_OBJECT, + ['dummyGeneratedValueList', [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Doctrine\Common\Collections\Collection', true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) )]], ['json', null], ]; @@ -231,7 +243,10 @@ public function testGetPropertiesCatchException() $this->assertNull($this->createExtractor()->getProperties('Not\Exist')); } - public function testGetTypesCatchException() + /** + * @group legacy + */ + public function testGetTypesCatchExceptionLegacy() { $this->assertNull($this->createExtractor()->getTypes('Not\Exist', 'baz')); } @@ -244,4 +259,66 @@ public function testGeneratedValueNotWritable() $this->assertNull($extractor->isWritable(DoctrineGeneratedValue::class, 'foo')); $this->assertNull($extractor->isReadable(DoctrineGeneratedValue::class, 'foo')); } + + public function testExtractWithEmbedded() + { + $this->assertEquals( + Type::object(DoctrineEmbeddable::class), + $this->createExtractor()->getType(DoctrineWithEmbedded::class, 'embedded'), + ); + } + + public function testExtractEnum() + { + $this->assertEquals(Type::enum(EnumString::class), $this->createExtractor()->getType(DoctrineEnum::class, 'enumString')); + $this->assertEquals(Type::enum(EnumInt::class), $this->createExtractor()->getType(DoctrineEnum::class, 'enumInt')); + $this->assertNull($this->createExtractor()->getType(DoctrineEnum::class, 'enumStringArray')); + $this->assertEquals(Type::list(Type::enum(EnumInt::class)), $this->createExtractor()->getType(DoctrineEnum::class, 'enumIntArray')); + $this->assertNull($this->createExtractor()->getType(DoctrineEnum::class, 'enumCustom')); + } + + /** + * @dataProvider typeProvider + */ + public function testExtract(string $property, ?Type $type) + { + $this->assertEquals($type, $this->createExtractor()->getType(DoctrineDummy::class, $property, [])); + } + + /** + * @return iterable + */ + public static function typeProvider(): iterable + { + yield ['id', Type::int()]; + yield ['guid', Type::string()]; + yield ['bigint', Type::string()]; + yield ['time', Type::object(\DateTime::class)]; + yield ['timeImmutable', Type::object(\DateTimeImmutable::class)]; + yield ['dateInterval', Type::object(\DateInterval::class)]; + yield ['float', Type::float()]; + yield ['decimal', Type::string()]; + yield ['bool', Type::bool()]; + yield ['binary', Type::resource()]; + yield ['jsonArray', Type::array()]; + yield ['foo', Type::nullable(Type::object(DoctrineRelation::class))]; + yield ['bar', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['indexedRguid', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['indexedBar', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['indexedFoo', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['indexedBaz', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['simpleArray', Type::list(Type::string())]; + yield ['customFoo', null]; + yield ['notMapped', null]; + yield ['indexedByDt', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::object())]; + yield ['indexedByCustomType', null]; + yield ['indexedBuz', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::string())]; + yield ['dummyGeneratedValueList', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['json', null]; + } + + public function testGetTypeCatchException() + { + $this->assertNull($this->createExtractor()->getType('Not\Exist', 'baz')); + } } diff --git a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php index 15916dc596166..93413c4f8e6d8 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php +++ b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php @@ -17,7 +17,6 @@ use Doctrine\ORM\Mapping\MappingException as OrmMappingException; use Doctrine\Persistence\Mapping\MappingException; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Valid; use Symfony\Component\Validator\Mapping\AutoMappingStrategy; diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index be35a0a335994..00cc394d114be 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -19,6 +19,7 @@ "php": ">=8.2", "doctrine/event-manager": "^2", "doctrine/persistence": "^3.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3" @@ -38,6 +39,7 @@ "symfony/security-core": "^6.4|^7.0", "symfony/stopwatch": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", + "symfony/type-info": "^7.1", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php index c61955d37bc20..18cd61b08519c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php @@ -11,7 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; class PropertyInfoTest extends AbstractWebTestCase { @@ -19,7 +20,29 @@ public function testPhpDocPriority() { static::bootKernel(['test_case' => 'Serializer']); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT))], static::getContainer()->get('property_info')->getTypes('Symfony\Bundle\FrameworkBundle\Tests\Functional\Dummy', 'codes')); + $propertyInfo = static::getContainer()->get('property_info'); + + if (!method_exists($propertyInfo, 'getType')) { + $this->markTestSkipped(); + } + + $this->assertEquals(Type::list(Type::int()), $propertyInfo->getType(Dummy::class, 'codes')); + } + + /** + * @group legacy + */ + public function testPhpDocPriorityLegacy() + { + static::bootKernel(['test_case' => 'Serializer']); + + $propertyInfo = static::getContainer()->get('property_info'); + + if (!method_exists($propertyInfo, 'getTypes')) { + $this->markTestSkipped(); + } + + $this->assertEquals([new LegacyType('array', false, null, true, new LegacyType('int'), new LegacyType('int'))], $propertyInfo->getTypes(Dummy::class, 'codes')); } } diff --git a/src/Symfony/Component/PropertyInfo/CHANGELOG.md b/src/Symfony/Component/PropertyInfo/CHANGELOG.md index 298c64357cfab..1e5642be469ed 100644 --- a/src/Symfony/Component/PropertyInfo/CHANGELOG.md +++ b/src/Symfony/Component/PropertyInfo/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG * Introduce `PropertyDocBlockExtractorInterface` to extract a property's doc block * Restrict access to `PhpStanExtractor` based on visibility + * Deprecate the `Type` class, use `Symfony\Component\TypeInfo\Type` class of `symfony/type-info` component instead + * Deprecate the `PropertyTypeExtractorInterface::getTypes()` method, use `PropertyTypeExtractorInterface::getType()` instead 6.4 --- diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php index cbde902e98015..17ec478c39555 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorArgumentTypeExtractorInterface.php @@ -11,7 +11,8 @@ namespace Symfony\Component\PropertyInfo\Extractor; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * Infers the constructor argument type. @@ -25,9 +26,20 @@ interface ConstructorArgumentTypeExtractorInterface /** * Gets types of an argument from constructor. * - * @return Type[]|null + * @return LegacyType[]|null + * + * @deprecated since Symfony 7.1, use "getTypeFromConstructor" instead * * @internal */ public function getTypesFromConstructor(string $class, string $property): ?array; + + /** + * Gets type of an argument from constructor. + * + * @param class-string $class + * + * @internal + */ + public function getTypeFromConstructor(string $class, string $property): ?Type; } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php index 18e563a71883d..ee2ce36a5149c 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ConstructorExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyInfo\Extractor; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\TypeInfo\Type; /** * Extracts the constructor argument type using ConstructorArgumentTypeExtractorInterface implementations. @@ -28,8 +29,24 @@ public function __construct( ) { } + public function getType(string $class, string $property, array $context = []): ?Type + { + foreach ($this->extractors as $extractor) { + if (null !== $value = $extractor->getTypeFromConstructor($class, $property)) { + return $value; + } + } + + return null; + } + + /** + * @deprecated since Symfony 7.1, use "getType" instead + */ public function getTypes(string $class, string $property, array $context = []): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + foreach ($this->extractors as $extractor) { $value = $extractor->getTypesFromConstructor($class, $property); if (null !== $value) { diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index 938d4e2ac96c1..f24760a5951de 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -20,8 +20,12 @@ use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDocBlockExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper; +use Symfony\Component\TypeInfo\Exception\LogicException; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; /** * Extracts data using a PHPDoc parser. @@ -48,6 +52,7 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property private DocBlockFactoryInterface $docBlockFactory; private ContextFactory $contextFactory; + private TypeContextFactory $typeContextFactory; private PhpDocTypeHelper $phpDocTypeHelper; private array $mutatorPrefixes; private array $accessorPrefixes; @@ -66,6 +71,7 @@ public function __construct(?DocBlockFactoryInterface $docBlockFactory = null, ? $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance(); $this->contextFactory = new ContextFactory(); + $this->typeContextFactory = new TypeContextFactory(); $this->phpDocTypeHelper = new PhpDocTypeHelper(); $this->mutatorPrefixes = $mutatorPrefixes ?? ReflectionExtractor::$defaultMutatorPrefixes; $this->accessorPrefixes = $accessorPrefixes ?? ReflectionExtractor::$defaultAccessorPrefixes; @@ -112,8 +118,13 @@ public function getLongDescription(string $class, string $property, array $conte return '' === $contents ? null : $contents; } + /** + * @deprecated since Symfony 7.1, use "getType" instead + */ public function getTypes(string $class, string $property, array $context = []): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + /** @var $docBlock DocBlock */ [$docBlock, $source, $prefix] = $this->findDocBlock($class, $property); if (!$docBlock) { @@ -149,7 +160,7 @@ public function getTypes(string $class, string $property, array $context = []): continue 2; } - $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); + $types[] = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); } } } @@ -162,11 +173,16 @@ public function getTypes(string $class, string $property, array $context = []): return $types; } - return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $types[0])]; } + /** + * @deprecated since Symfony 7.1, use "getTypeFromConstructor" instead + */ public function getTypesFromConstructor(string $class, string $property): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getTypeFromConstructor()" instead.', __METHOD__, self::class); + $docBlock = $this->getDocBlockFromConstructor($class, $property); if (!$docBlock) { @@ -188,6 +204,84 @@ public function getTypesFromConstructor(string $class, string $property): ?array return array_merge([], ...$types); } + public function getType(string $class, string $property, array $context = []): ?Type + { + /** @var $docBlock DocBlock */ + [$docBlock, $source, $prefix] = $this->findDocBlock($class, $property); + if (!$docBlock) { + return null; + } + + $tag = match ($source) { + self::PROPERTY => 'var', + self::ACCESSOR => 'return', + self::MUTATOR => 'param', + }; + + $types = []; + $typeContext = $this->typeContextFactory->createFromClassName($class); + + /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */ + foreach ($docBlock->getTagsByName($tag) as $tag) { + if ($tag instanceof InvalidTag || !$tagType = $tag->getType()) { + continue; + } + + $type = $this->phpDocTypeHelper->getType($tagType); + + if (!$type instanceof ObjectType) { + $types[] = $type; + + continue; + } + + $normalizedClassName = match ($type->getClassName()) { + 'self' => $typeContext->getDeclaringClass(), + 'static' => $typeContext->getCalledClass(), + default => $type->getClassName(), + }; + + if ('parent' === $normalizedClassName) { + try { + $normalizedClassName = $typeContext->getParentClass(); + } catch (LogicException) { + // if there is no parent for the current class, we keep the "parent" raw string + } + } + + $types[] = $type->isNullable() ? Type::nullable(Type::object($normalizedClassName)) : Type::object($normalizedClassName); + } + + if (null === $type = $types[0] ?? null) { + return null; + } + + if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) { + return $type; + } + + return Type::list($type); + } + + public function getTypeFromConstructor(string $class, string $property): ?Type + { + if (!$docBlock = $this->getDocBlockFromConstructor($class, $property)) { + return null; + } + + $types = []; + /** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */ + foreach ($docBlock->getTagsByName('param') as $tag) { + if ($tag instanceof InvalidTag || !$tagType = $tag->getType()) { + continue; + } + + $types[] = $this->phpDocTypeHelper->getType($tagType); + } + + return $types[0] ?? null; + } + public function getDocBlock(string $class, string $property): ?DocBlock { $output = $this->findDocBlock($class, $property); diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php index 119ee08cd1d1d..39d997c78cc47 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php @@ -23,8 +23,12 @@ use PHPStan\PhpDocParser\Parser\TypeParser; use Symfony\Component\PropertyInfo\PhpStan\NameScopeFactory; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\PropertyInfo\Util\PhpStanTypeHelper; +use Symfony\Component\TypeInfo\Exception\UnsupportedException; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; +use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; /** * Extracts data using PHPStan parser. @@ -41,6 +45,9 @@ final class PhpStanExtractor implements PropertyTypeExtractorInterface, Construc private Lexer $lexer; private NameScopeFactory $nameScopeFactory; + private StringTypeResolver $stringTypeResolver; + private TypeContextFactory $typeContextFactory; + /** @var array */ private array $docBlocks = []; private PhpStanTypeHelper $phpStanTypeHelper; @@ -71,10 +78,17 @@ public function __construct(?array $mutatorPrefixes = null, ?array $accessorPref $this->phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); $this->lexer = new Lexer(); $this->nameScopeFactory = new NameScopeFactory(); + $this->stringTypeResolver = new StringTypeResolver(); + $this->typeContextFactory = new TypeContextFactory($this->stringTypeResolver); } + /** + * @deprecated since Symfony 7.1, use "getType" instead + */ public function getTypes(string $class, string $property, array $context = []): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + /** @var PhpDocNode|null $docNode */ [$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property); $nameScope = $this->nameScopeFactory->create($class, $declaringClass); @@ -129,7 +143,7 @@ public function getTypes(string $class, string $property, array $context = []): continue 2; } - $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); + $types[] = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $type->isNullable(), $resolvedClass, $type->isCollection(), $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); } } @@ -141,11 +155,18 @@ public function getTypes(string $class, string $property, array $context = []): return $types; } - return [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $types[0])]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $types[0])]; } + /** + * @deprecated since Symfony 7.1, use "getTypeFromConstructor" instead + * + * @return LegacyType[]|null + */ public function getTypesFromConstructor(string $class, string $property): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getTypeFromConstructor()" instead.', __METHOD__, self::class); + if (null === $tagDocNode = $this->getDocBlockFromConstructor($class, $property)) { return null; } @@ -162,6 +183,63 @@ public function getTypesFromConstructor(string $class, string $property): ?array return $types; } + public function getType(string $class, string $property, array $context = []): ?Type + { + /** @var PhpDocNode|null $docNode */ + [$docNode, $source, $prefix, $declaringClass] = $this->getDocBlock($class, $property); + + if (null === $docNode) { + return null; + } + + $typeContext = $this->typeContextFactory->createFromClassName($class, $declaringClass); + + $tag = match ($source) { + self::PROPERTY => '@var', + self::ACCESSOR => '@return', + self::MUTATOR => '@param', + default => 'invalid', + }; + + $types = []; + + foreach ($docNode->getTagsByName($tag) as $tagDocNode) { + if ($tagDocNode->value instanceof InvalidTagValueNode) { + continue; + } + + if ($tagDocNode->value instanceof ParamTagValueNode && null === $prefix && $tagDocNode->value->parameterName !== '$'.$property) { + continue; + } + + try { + $types[] = $this->stringTypeResolver->resolve((string) $tagDocNode->value->type, $typeContext); + } catch (UnsupportedException) { + } + } + + if (!$type = $types[0] ?? null) { + return null; + } + + if (!\in_array($prefix, $this->arrayMutatorPrefixes, true)) { + return $type; + } + + return Type::list($type); + } + + public function getTypeFromConstructor(string $class, string $property): ?Type + { + if (!$tagDocNode = $this->getDocBlockFromConstructor($class, $property)) { + return null; + } + + $typeContext = $this->typeContextFactory->createFromClassName($class); + + return $this->stringTypeResolver->resolve((string) $tagDocNode->type, $typeContext); + } + private function getDocBlockFromConstructor(string $class, string $property): ?ParamTagValueNode { try { diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index 0a357ebfd36e1..7e7087221812e 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -19,9 +19,15 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\PropertyWriteInfo; use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\String\Inflector\EnglishInflector; use Symfony\Component\String\Inflector\InflectorInterface; +use Symfony\Component\TypeInfo\Exception\UnsupportedException; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\TypeIdentifier; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface; /** * Extracts data using the reflection API. @@ -61,9 +67,9 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp public const ALLOW_MAGIC_CALL = 1 << 2; private const MAP_TYPES = [ - 'integer' => Type::BUILTIN_TYPE_INT, - 'boolean' => Type::BUILTIN_TYPE_BOOL, - 'double' => Type::BUILTIN_TYPE_FLOAT, + 'integer' => TypeIdentifier::INT->value, + 'boolean' => TypeIdentifier::BOOL->value, + 'double' => TypeIdentifier::FLOAT->value, ]; private array $mutatorPrefixes; @@ -76,6 +82,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp private InflectorInterface $inflector; private array $arrayMutatorPrefixesFirst; private array $arrayMutatorPrefixesLast; + private TypeResolverInterface $typeResolver; /** * @param string[]|null $mutatorPrefixes @@ -92,6 +99,7 @@ public function __construct(?array $mutatorPrefixes = null, ?array $accessorPref $this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags); $this->magicMethodsFlags = $magicMethodsFlags; $this->inflector = $inflector ?? new EnglishInflector(); + $this->typeResolver = TypeResolver::create(); $this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes)); $this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst); @@ -132,8 +140,13 @@ public function getProperties(string $class, array $context = []): ?array return $properties ? array_values($properties) : null; } + /** + * @deprecated since Symfony 7.1, use "getType" instead + */ public function getTypes(string $class, string $property, array $context = []): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + if ($fromMutator = $this->extractFromMutator($class, $property)) { return $fromMutator; } @@ -156,8 +169,15 @@ public function getTypes(string $class, string $property, array $context = []): return null; } + /** + * @deprecated since Symfony 7.1, use "getTypeFromConstructor" instead + * + * @return LegacyType[]|null + */ public function getTypesFromConstructor(string $class, string $property): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getTypeFromConstructor()" instead.', __METHOD__, self::class); + try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException) { @@ -179,6 +199,89 @@ public function getTypesFromConstructor(string $class, string $property): ?array return $types; } + public function getType(string $class, string $property, array $context = []): ?Type + { + [$mutatorReflection, $prefix] = $this->getMutatorMethod($class, $property); + + if ($mutatorReflection) { + try { + $type = $this->typeResolver->resolve($mutatorReflection->getParameters()[0]); + + if (!$type instanceof CollectionType && \in_array($prefix, $this->arrayMutatorPrefixes, true)) { + $type = Type::list($type); + } + + return $type; + } catch (UnsupportedException) { + } + } + + [$accessorReflection, $prefix] = $this->getAccessorMethod($class, $property); + if ($accessorReflection) { + try { + return $this->typeResolver->resolve($accessorReflection); + } catch (UnsupportedException) { + } + } + + if ($context['enable_constructor_extraction'] ?? $this->enableConstructorExtraction) { + try { + $reflectionClass = new \ReflectionClass($class); + if ($type = $this->extractTypeFromConstructor($reflectionClass, $property)) { + return $type; + } + } catch (\ReflectionException) { + } + } + + try { + $reflectionClass = new \ReflectionClass($class); + $reflectionProperty = $reflectionClass->getProperty($property); + } catch (\ReflectionException) { + return null; + } + + try { + return $this->typeResolver->resolve($reflectionProperty); + } catch (UnsupportedException) { + } + + if (null === $defaultValue = ($reflectionClass->getDefaultProperties()[$property] ?? null)) { + return null; + } + + $typeIdentifier = TypeIdentifier::from(static::MAP_TYPES[\gettype($defaultValue)] ?? \gettype($defaultValue)); + $type = 'array' === $typeIdentifier->value ? Type::array() : Type::builtin($typeIdentifier); + + if ($this->isNullableProperty($class, $property)) { + $type = Type::nullable($type); + } + + return $type; + } + + public function getTypeFromConstructor(string $class, string $property): ?Type + { + try { + $reflection = new \ReflectionClass($class); + } catch (\ReflectionException) { + return null; + } + + if (!$reflectionConstructor = $reflection->getConstructor()) { + return null; + } + if (!$reflectionParameter = $this->getReflectionParameterFromConstructor($property, $reflectionConstructor)) { + return null; + } + + try { + return $this->typeResolver->resolve($reflectionParameter); + } catch (UnsupportedException) { + return null; + } + } + private function getReflectionParameterFromConstructor(string $property, \ReflectionMethod $reflectionConstructor): ?\ReflectionParameter { foreach ($reflectionConstructor->getParameters() as $reflectionParameter) { @@ -405,7 +508,7 @@ public function getWriteInfo(string $class, string $property, array $context = [ } /** - * @return Type[]|null + * @return LegacyType[]|null */ private function extractFromMutator(string $class, string $property): ?array { @@ -423,7 +526,7 @@ private function extractFromMutator(string $class, string $property): ?array $type = $this->extractFromReflectionType($reflectionType, $reflectionMethod->getDeclaringClass()); if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes, true)) { - $type = [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])]; + $type = [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $type[0])]; } return $type; @@ -432,7 +535,7 @@ private function extractFromMutator(string $class, string $property): ?array /** * Tries to extract type information from accessors. * - * @return Type[]|null + * @return LegacyType[]|null */ private function extractFromAccessor(string $class, string $property): ?array { @@ -446,7 +549,7 @@ private function extractFromAccessor(string $class, string $property): ?array } if (\in_array($prefix, ['is', 'can', 'has'])) { - return [new Type(Type::BUILTIN_TYPE_BOOL)]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]; } return null; @@ -455,7 +558,7 @@ private function extractFromAccessor(string $class, string $property): ?array /** * Tries to extract type information from constructor. * - * @return Type[]|null + * @return LegacyType[]|null */ private function extractFromConstructor(string $class, string $property): ?array { @@ -511,7 +614,31 @@ private function extractFromPropertyDeclaration(string $class, string $property) $type = \gettype($defaultValue); $type = static::MAP_TYPES[$type] ?? $type; - return [new Type($type, $this->isNullableProperty($class, $property), null, Type::BUILTIN_TYPE_ARRAY === $type)]; + return [new LegacyType($type, $this->isNullableProperty($class, $property), null, LegacyType::BUILTIN_TYPE_ARRAY === $type)]; + } + + private function extractTypeFromConstructor(\ReflectionClass $reflectionClass, string $property): ?Type + { + if (!$constructor = $reflectionClass->getConstructor()) { + return null; + } + + foreach ($constructor->getParameters() as $parameter) { + if ($property !== $parameter->name) { + continue; + } + + try { + return $this->typeResolver->resolve($parameter); + } catch (UnsupportedException) { + } + } + + if ($parentClass = $reflectionClass->getParentClass()) { + return $this->extractTypeFromConstructor($parentClass, $property); + } + + return null; } private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionClass $declaringClass): array @@ -530,14 +657,14 @@ private function extractFromReflectionType(\ReflectionType $reflectionType, \Ref continue; } - if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) { - $types[] = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true); + if (LegacyType::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) { + $types[] = new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true); } elseif ('void' === $phpTypeOrClass) { - $types[] = new Type(Type::BUILTIN_TYPE_NULL, $nullable); + $types[] = new LegacyType(LegacyType::BUILTIN_TYPE_NULL, $nullable); } elseif ($type->isBuiltin()) { - $types[] = new Type($phpTypeOrClass, $nullable); + $types[] = new LegacyType($phpTypeOrClass, $nullable); } else { - $types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $declaringClass)); + $types[] = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $declaringClass)); } } diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php index b4543eace7d6e..9cf487eb7fad0 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\PropertyInfo; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\TypeInfo\Type; /** * Adds a PSR-6 cache layer on top of an extractor. @@ -55,8 +56,18 @@ public function getProperties(string $class, array $context = []): ?array return $this->extract('getProperties', [$class, $context]); } + public function getType(string $class, string $property, array $context = []): ?Type + { + return $this->extract('getType', [$class, $property, $context]); + } + + /** + * @deprecated since Symfony 7.1, use "getType" instead + */ public function getTypes(string $class, string $property, array $context = []): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + return $this->extract('getTypes', [$class, $property, $context]); } diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php index 7416849a0a843..cc24382d0e683 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoExtractor.php @@ -11,6 +11,8 @@ namespace Symfony\Component\PropertyInfo; +use Symfony\Component\TypeInfo\Type; + /** * Default {@see PropertyInfoExtractorInterface} implementation. * @@ -51,8 +53,18 @@ public function getLongDescription(string $class, string $property, array $conte return $this->extract($this->descriptionExtractors, 'getLongDescription', [$class, $property, $context]); } + public function getType(string $class, string $property, array $context = []): ?Type + { + return $this->extract($this->typeExtractors, 'getType', [$class, $property, $context]); + } + + /** + * @deprecated since Symfony 7.1, use "getType" instead + */ public function getTypes(string $class, string $property, array $context = []): ?array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + return $this->extract($this->typeExtractors, 'getTypes', [$class, $property, $context]); } diff --git a/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php index 16e9765b1d5b9..d5762370bccd1 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php +++ b/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php @@ -11,17 +11,24 @@ namespace Symfony\Component\PropertyInfo; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; + /** * Type Extractor Interface. * * @author Kévin Dunglas + * + * @method Type|null getType(string $class, string $property, array $context = []) */ interface PropertyTypeExtractorInterface { /** * Gets types of a property. * - * @return Type[]|null + * @deprecated since Symfony 7.1, use "getType" instead + * + * @return LegacyType[]|null */ public function getTypes(string $class, string $property, array $context = []): ?array; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php index d53172ef1504f..6f5c67131124e 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php @@ -20,7 +20,8 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\NullExtractor; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -54,9 +55,17 @@ public function testGetLongDescription() $this->assertSame('long', $this->propertyInfo->getLongDescription('Foo', 'bar', [])); } + public function testGetType() + { + $this->assertEquals(Type::int(), $this->propertyInfo->getType('Foo', 'bar', [])); + } + + /** + * @group legacy + */ public function testGetTypes() { - $this->assertEquals([new Type(Type::BUILTIN_TYPE_INT)], $this->propertyInfo->getTypes('Foo', 'bar', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_INT)], $this->propertyInfo->getTypes('Foo', 'bar', [])); } public function testIsReadable() diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php index 6bd8318ed6229..fcfa7bb86a078 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ConstructorExtractorTest.php @@ -14,7 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Dmitrii Poddubnyi @@ -33,11 +34,28 @@ public function testInstanceOf() $this->assertInstanceOf(\Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface::class, $this->extractor); } + public function testGetType() + { + $this->assertEquals(Type::string(), $this->extractor->getType('Foo', 'bar', [])); + } + + public function testGetTypeIfNoExtractors() + { + $extractor = new ConstructorExtractor([]); + $this->assertNull($extractor->getType('Foo', 'bar', [])); + } + + /** + * @group legacy + */ public function testGetTypes() { - $this->assertEquals([new Type(Type::BUILTIN_TYPE_STRING)], $this->extractor->getTypes('Foo', 'bar', [])); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], $this->extractor->getTypes('Foo', 'bar', [])); } + /** + * @group legacy + */ public function testGetTypesIfNoExtractors() { $extractor = new ConstructorExtractor([]); diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index e2e2870dceb1d..085a7b8a1cfc8 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -14,13 +14,18 @@ use phpDocumentor\Reflection\DocBlock; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypeDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypesDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait; use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -35,9 +40,11 @@ protected function setUp(): void } /** - * @dataProvider typesProvider + * @group legacy + * + * @dataProvider provideLegacyTypes */ - public function testExtract($property, ?array $type, $shortDescription, $longDescription) + public function testExtractLegacy($property, ?array $type, $shortDescription, $longDescription) { $this->assertEquals($type, $this->extractor->getTypes(Dummy::class, $property)); $this->assertSame($shortDescription, $this->extractor->getShortDescription(Dummy::class, $property)); @@ -57,12 +64,15 @@ public function testGetDocBlock() $this->assertNull($docBlock); } - public function testParamTagTypeIsOmitted() + /** + * @group legacy + */ + public function testParamTagTypeIsOmittedLegacy() { $this->assertNull($this->extractor->getTypes(OmittedParamTagTypeDocBlock::class, 'omittedType')); } - public static function invalidTypesProvider() + public static function provideLegacyInvalidTypes() { return [ 'pub' => ['pub', null, null], @@ -72,9 +82,11 @@ public static function invalidTypesProvider() } /** - * @dataProvider invalidTypesProvider + * @group legacy + * + * @dataProvider provideLegacyInvalidTypes */ - public function testInvalid($property, $shortDescription, $longDescription) + public function testInvalidLegacy($property, $shortDescription, $longDescription) { $this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property)); $this->assertSame($shortDescription, $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property)); @@ -84,7 +96,7 @@ public function testInvalid($property, $shortDescription, $longDescription) /** * @group legacy */ - public function testEmptyParamAnnotation() + public function testEmptyParamAnnotationLegacy() { $this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo')); $this->assertSame('Foo.', $this->extractor->getShortDescription('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', 'foo')); @@ -92,119 +104,123 @@ public function testEmptyParamAnnotation() } /** - * @dataProvider typesWithNoPrefixesProvider + * @group legacy + * + * @dataProvider provideLegacyTypesWithNoPrefixes */ - public function testExtractTypesWithNoPrefixes($property, ?array $type = null) + public function testExtractTypesWithNoPrefixesLegacy($property, ?array $type = null) { $noPrefixExtractor = new PhpDocExtractor(null, [], [], []); $this->assertEquals($type, $noPrefixExtractor->getTypes(Dummy::class, $property)); } - public static function typesProvider() + public static function provideLegacyTypes() { return [ ['foo', null, 'Short description.', 'Long description.'], - ['bar', [new Type(Type::BUILTIN_TYPE_STRING)], 'This is bar', null], - ['baz', [new Type(Type::BUILTIN_TYPE_INT)], 'Should be used.', null], - ['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)], null, null], - ['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)], null, null], - ['foo4', [new Type(Type::BUILTIN_TYPE_NULL)], null, null], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], 'This is bar', null], + ['baz', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], 'Should be used.', null], + ['foo2', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)], null, null], + ['foo3', [new LegacyType(LegacyType::BUILTIN_TYPE_CALLABLE)], null, null], + ['foo4', [new LegacyType(LegacyType::BUILTIN_TYPE_NULL)], null, null], ['foo5', null, null, null], [ 'files', [ - new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), - new Type(Type::BUILTIN_TYPE_RESOURCE), + new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), + new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE), ], null, null, ], - ['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null], - ['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null], - ['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], - ['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))], null, null], - ['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null], - ['a', [new Type(Type::BUILTIN_TYPE_INT)], 'A.', null], - ['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], 'B.', null], - ['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)], null, null], - ['ct', [new Type(Type::BUILTIN_TYPE_TRUE, true)], null, null], - ['cf', [new Type(Type::BUILTIN_TYPE_FALSE, true)], null, null], - ['d', [new Type(Type::BUILTIN_TYPE_BOOL)], null, null], - ['dt', [new Type(Type::BUILTIN_TYPE_TRUE)], null, null], - ['df', [new Type(Type::BUILTIN_TYPE_FALSE)], null, null], - ['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))], null, null], - ['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], - ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null], - ['h', [new Type(Type::BUILTIN_TYPE_STRING, true)], null, null], - ['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null], - ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null], - ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null], + ['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null], + ['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], + ['nestedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)))], null, null], + ['mixedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null], + ['a', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], 'A.', null], + ['b', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], 'B.', null], + ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL, true)], null, null], + ['ct', [new LegacyType(LegacyType::BUILTIN_TYPE_TRUE, true)], null, null], + ['cf', [new LegacyType(LegacyType::BUILTIN_TYPE_FALSE, true)], null, null], + ['d', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)], null, null], + ['dt', [new LegacyType(LegacyType::BUILTIN_TYPE_TRUE)], null, null], + ['df', [new LegacyType(LegacyType::BUILTIN_TYPE_FALSE)], null, null], + ['e', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE))], null, null], + ['f', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], + ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null], + ['h', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true)], null, null], + ['i', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true), new LegacyType(LegacyType::BUILTIN_TYPE_INT, true)], null, null], + ['j', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null], + ['nullableCollectionOfNonNullableElements', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_INT, false))], null, null], ['donotexist', null, null, null], ['staticGetter', null, null, null], ['staticSetter', null, null, null], ['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], - ['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], null, null], + ['arrayWithKeys', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))], null, null], + ['arrayOfMixed', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_STRING), null)], null, null], + ['listOfStrings', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))], null, null], + ['self', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], null, null], ]; } /** - * @dataProvider provideCollectionTypes + * @group legacy + * + * @dataProvider provideLegacyCollectionTypes */ - public function testExtractCollection($property, ?array $type, $shortDescription, $longDescription) + public function testExtractCollectionLegacy($property, ?array $type, $shortDescription, $longDescription) { - $this->testExtract($property, $type, $shortDescription, $longDescription); + $this->testExtractLegacy($property, $type, $shortDescription, $longDescription); } - public static function provideCollectionTypes() + public static function provideLegacyCollectionTypes() { return [ - ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], new Type(Type::BUILTIN_TYPE_STRING))], null, null], - ['iteratorCollectionWithKey', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], null, null], + ['iteratorCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)], new LegacyType(LegacyType::BUILTIN_TYPE_STRING))], null, null], + ['iteratorCollectionWithKey', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))], null, null], [ 'nestedIterators', - [new Type( - Type::BUILTIN_TYPE_OBJECT, + [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING)) )], null, null, ], [ 'arrayWithKeys', - [new Type( - Type::BUILTIN_TYPE_ARRAY, + [new LegacyType( + LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_STRING) + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_STRING) )], null, null, ], [ 'arrayWithKeysAndComplexValue', - [new Type( - Type::BUILTIN_TYPE_ARRAY, + [new LegacyType( + LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type( - Type::BUILTIN_TYPE_ARRAY, + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType( + LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_STRING, true) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true) ) )], null, @@ -214,93 +230,95 @@ public static function provideCollectionTypes() } /** - * @dataProvider typesWithCustomPrefixesProvider + * @group legacy + * + * @dataProvider provideLegacyTypesWithCustomPrefixes */ - public function testExtractTypesWithCustomPrefixes($property, ?array $type = null) + public function testExtractTypesWithCustomPrefixesLegacy($property, ?array $type = null) { $customExtractor = new PhpDocExtractor(null, ['add', 'remove'], ['is', 'can']); $this->assertEquals($type, $customExtractor->getTypes(Dummy::class, $property)); } - public static function typesWithCustomPrefixesProvider() + public static function provideLegacyTypesWithCustomPrefixes() { return [ ['foo', null, 'Short description.', 'Long description.'], - ['bar', [new Type(Type::BUILTIN_TYPE_STRING)], 'This is bar', null], - ['baz', [new Type(Type::BUILTIN_TYPE_INT)], 'Should be used.', null], - ['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)], null, null], - ['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)], null, null], - ['foo4', [new Type(Type::BUILTIN_TYPE_NULL)], null, null], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], 'This is bar', null], + ['baz', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], 'Should be used.', null], + ['foo2', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)], null, null], + ['foo3', [new LegacyType(LegacyType::BUILTIN_TYPE_CALLABLE)], null, null], + ['foo4', [new LegacyType(LegacyType::BUILTIN_TYPE_NULL)], null, null], ['foo5', null, null, null], [ 'files', [ - new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), - new Type(Type::BUILTIN_TYPE_RESOURCE), + new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), + new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE), ], null, null, ], - ['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null], - ['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null], - ['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], - ['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))], null, null], - ['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null], + ['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null], + ['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], + ['nestedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)))], null, null], + ['mixedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null], ['a', null, 'A.', null], ['b', null, 'B.', null], - ['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)], null, null], - ['d', [new Type(Type::BUILTIN_TYPE_BOOL)], null, null], - ['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))], null, null], - ['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], - ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null], - ['h', [new Type(Type::BUILTIN_TYPE_STRING, true)], null, null], - ['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null], - ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null], - ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null], - ['nonNullableCollectionOfNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, true))], null, null], - ['nullableCollectionOfMultipleNonNullableElementTypes', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)])], null, null], + ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL, true)], null, null], + ['d', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)], null, null], + ['e', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE))], null, null], + ['f', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], + ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null], + ['h', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true)], null, null], + ['i', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true), new LegacyType(LegacyType::BUILTIN_TYPE_INT, true)], null, null], + ['j', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null], + ['nullableCollectionOfNonNullableElements', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_INT, false))], null, null], + ['nonNullableCollectionOfNullableElements', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_INT, true))], null, null], + ['nullableCollectionOfMultipleNonNullableElementTypes', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])], null, null], ['donotexist', null, null, null], ['staticGetter', null, null, null], ['staticSetter', null, null, null], ]; } - public static function typesWithNoPrefixesProvider() + public static function provideLegacyTypesWithNoPrefixes() { return [ ['foo', null, 'Short description.', 'Long description.'], - ['bar', [new Type(Type::BUILTIN_TYPE_STRING)], 'This is bar', null], - ['baz', [new Type(Type::BUILTIN_TYPE_INT)], 'Should be used.', null], - ['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)], null, null], - ['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)], null, null], - ['foo4', [new Type(Type::BUILTIN_TYPE_NULL)], null, null], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], 'This is bar', null], + ['baz', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], 'Should be used.', null], + ['foo2', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)], null, null], + ['foo3', [new LegacyType(LegacyType::BUILTIN_TYPE_CALLABLE)], null, null], + ['foo4', [new LegacyType(LegacyType::BUILTIN_TYPE_NULL)], null, null], ['foo5', null, null, null], [ 'files', [ - new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), - new Type(Type::BUILTIN_TYPE_RESOURCE), + new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), + new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE), ], null, null, ], - ['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null], - ['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null], - ['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], - ['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))], null, null], - ['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null], + ['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')], null, null], + ['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], null, null], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))], null, null], + ['nestedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)))], null, null], + ['mixedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, null, null)], null, null], ['a', null, 'A.', null], ['b', null, 'B.', null], ['c', null, null, null], ['d', null, null, null], ['e', null, null, null], ['f', null, null, null], - ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null], - ['h', [new Type(Type::BUILTIN_TYPE_STRING, true)], null, null], - ['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null], - ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null], - ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null], + ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null], + ['h', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true)], null, null], + ['i', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true), new LegacyType(LegacyType::BUILTIN_TYPE_INT, true)], null, null], + ['j', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')], null, null], + ['nullableCollectionOfNonNullableElements', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_INT, false))], null, null], ['donotexist', null, null, null], ['staticGetter', null, null, null], ['staticSetter', null, null, null], @@ -312,120 +330,135 @@ public function testReturnNullOnEmptyDocBlock() $this->assertNull($this->extractor->getShortDescription(EmptyDocBlock::class, 'foo')); } - public static function dockBlockFallbackTypesProvider() + public static function provideLegacyDockBlockFallbackTypes() { return [ 'pub' => [ - 'pub', [new Type(Type::BUILTIN_TYPE_STRING)], + 'pub', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], ], 'protAcc' => [ - 'protAcc', [new Type(Type::BUILTIN_TYPE_INT)], + 'protAcc', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], ], 'protMut' => [ - 'protMut', [new Type(Type::BUILTIN_TYPE_BOOL)], + 'protMut', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)], ], ]; } /** - * @dataProvider dockBlockFallbackTypesProvider + * @group legacy + * + * @dataProvider provideLegacyDockBlockFallbackTypes */ - public function testDocBlockFallback($property, $types) + public function testDocBlockFallbackLegacy($property, $types) { $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property)); } /** - * @dataProvider propertiesDefinedByTraitsProvider + * @group legacy + * + * @dataProvider provideLegacyPropertiesDefinedByTraits */ - public function testPropertiesDefinedByTraits(string $property, Type $type) + public function testPropertiesDefinedByTraitsLegacy(string $property, LegacyType $type) { $this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property)); } - public static function propertiesDefinedByTraitsProvider(): array + public static function provideLegacyPropertiesDefinedByTraits(): array { return [ - ['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], - ['propertyInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], - ['propertyInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], - ['propertyInExternalTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], - ['propertyInExternalTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], - ['propertyInExternalTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], + ['propertyInTraitPrimitiveType', new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], + ['propertyInTraitObjectSameNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], + ['propertyInTraitObjectDifferentNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], + ['propertyInExternalTraitPrimitiveType', new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], + ['propertyInExternalTraitObjectSameNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], + ['propertyInExternalTraitObjectDifferentNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], ]; } /** - * @dataProvider methodsDefinedByTraitsProvider + * @group legacy + * + * @dataProvider provideLegacyMethodsDefinedByTraits */ - public function testMethodsDefinedByTraits(string $property, Type $type) + public function testMethodsDefinedByTraitsLegacy(string $property, LegacyType $type) { $this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property)); } - public static function methodsDefinedByTraitsProvider(): array + public static function provideLegacyMethodsDefinedByTraits(): array { return [ - ['methodInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], - ['methodInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], - ['methodInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], - ['methodInExternalTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], - ['methodInExternalTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], - ['methodInExternalTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], + ['methodInTraitPrimitiveType', new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], + ['methodInTraitObjectSameNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], + ['methodInTraitObjectDifferentNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], + ['methodInExternalTraitPrimitiveType', new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], + ['methodInExternalTraitObjectSameNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], + ['methodInExternalTraitObjectDifferentNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], ]; } /** - * @dataProvider propertiesStaticTypeProvider + * @group legacy + * + * @dataProvider provideLegacyPropertiesStaticType */ - public function testPropertiesStaticType(string $class, string $property, Type $type) + public function testPropertiesStaticTypeLegacy(string $class, string $property, LegacyType $type) { $this->assertEquals([$type], $this->extractor->getTypes($class, $property)); } - public static function propertiesStaticTypeProvider(): array + public static function provideLegacyPropertiesStaticType(): array { return [ - [ParentDummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)], - [Dummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], + [ParentDummy::class, 'propertyTypeStatic', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)], + [Dummy::class, 'propertyTypeStatic', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], ]; } /** - * @dataProvider propertiesParentTypeProvider + * @group legacy + * + * @dataProvider provideLegacyPropertiesParentType */ - public function testPropertiesParentType(string $class, string $property, ?array $types) + public function testPropertiesParentTypeLegacy(string $class, string $property, ?array $types) { $this->assertEquals($types, $this->extractor->getTypes($class, $property)); } - public static function propertiesParentTypeProvider(): array + public static function provideLegacyPropertiesParentType(): array { return [ - [ParentDummy::class, 'parentAnnotationNoParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'parent')]], - [Dummy::class, 'parentAnnotation', [new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], + [ParentDummy::class, 'parentAnnotationNoParent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'parent')]], + [Dummy::class, 'parentAnnotation', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], ]; } - public function testUnknownPseudoType() + /** + * @group legacy + */ + public function testUnknownPseudoTypeLegacy() { - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'scalar')], $this->extractor->getTypes(PseudoTypeDummy::class, 'unknownPseudoType')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'scalar')], $this->extractor->getTypes(PseudoTypeDummy::class, 'unknownPseudoType')); } /** - * @dataProvider constructorTypesProvider + * @group legacy + * + * @dataProvider provideLegacyConstructorTypes */ - public function testExtractConstructorTypes($property, ?array $type = null) + public function testExtractConstructorTypesLegacy($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } - public static function constructorTypesProvider() + public static function provideLegacyConstructorTypes() { return [ - ['date', [new Type(Type::BUILTIN_TYPE_INT)]], - ['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], - ['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]], + ['date', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['timezone', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], + ['dateObject', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]], ['dateTime', null], ['ddd', null], ['mixed', null], @@ -433,43 +466,397 @@ public static function constructorTypesProvider() } /** - * @dataProvider pseudoTypesProvider + * @group legacy + * + * @dataProvider provideLegacyPseudoTypes */ - public function testPseudoTypes($property, array $type) + public function testPseudoTypesLegacy($property, array $type) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypesDummy', $property)); } - public static function pseudoTypesProvider(): array + public static function provideLegacyPseudoTypes(): array { return [ - ['classString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['classStringGeneric', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['htmlEscapedString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['lowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['nonEmptyLowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['nonEmptyString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['numericString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['traitString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['positiveInt', [new Type(Type::BUILTIN_TYPE_INT, false, null)]], + ['classString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['classStringGeneric', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['htmlEscapedString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['lowercaseString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['nonEmptyLowercaseString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['nonEmptyString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['numericString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['traitString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['positiveInt', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, false, null)]], ]; } /** - * @dataProvider promotedPropertyProvider + * @group legacy + * + * @dataProvider provideLegacyPromotedProperty */ - public function testExtractPromotedProperty(string $property, ?array $types) + public function testExtractPromotedPropertyLegacy(string $property, ?array $types) { $this->assertEquals($types, $this->extractor->getTypes(Php80Dummy::class, $property)); } - public static function promotedPropertyProvider(): array + public static function provideLegacyPromotedProperty(): array { return [ ['promoted', null], - ['promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]], + ['promotedAndMutated', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], ]; } + + public function testParamTagTypeIsOmitted() + { + $this->assertNull($this->extractor->getType(OmittedParamTagTypeDocBlock::class, 'omittedType')); + } + + /** + * @dataProvider typeProvider + */ + public function testExtract(string $property, ?Type $type, ?string $shortDescription, ?string $longDescription) + { + $this->assertEquals($type, $this->extractor->getType(Dummy::class, $property)); + $this->assertSame($shortDescription, $this->extractor->getShortDescription(Dummy::class, $property)); + $this->assertSame($longDescription, $this->extractor->getLongDescription(Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function typeProvider(): iterable + { + yield ['foo', null, 'Short description.', 'Long description.']; + yield ['bar', Type::string(), 'This is bar', null]; + yield ['baz', Type::int(), 'Should be used.', null]; + yield ['foo2', Type::float(), null, null]; + yield ['foo3', Type::callable(), null, null]; + yield ['foo4', Type::null(), null, null]; + yield ['foo5', Type::mixed(), null, null]; + yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource()), null, null]; + yield ['bal', Type::object(\DateTimeImmutable::class), null, null]; + yield ['parent', Type::object(ParentDummy::class), null, null]; + yield ['collection', Type::list(Type::object(\DateTimeImmutable::class)), null, null]; + yield ['nestedCollection', Type::list(Type::list(Type::string())), null, null]; + yield ['mixedCollection', Type::array(), null, null]; + yield ['a', Type::int(), 'A.', null]; + yield ['b', Type::nullable(Type::object(ParentDummy::class)), 'B.', null]; + yield ['c', Type::nullable(Type::bool()), null, null]; + yield ['ct', Type::nullable(Type::true()), null, null]; + yield ['cf', Type::nullable(Type::false()), null, null]; + yield ['d', Type::bool(), null, null]; + yield ['dt', Type::true(), null, null]; + yield ['df', Type::false(), null, null]; + yield ['e', Type::list(Type::resource()), null, null]; + yield ['f', Type::list(Type::object(\DateTimeImmutable::class)), null, null]; + yield ['g', Type::nullable(Type::array()), 'Nullable array.', null]; + yield ['h', Type::nullable(Type::string()), null, null]; + yield ['i', Type::union(Type::int(), Type::string(), Type::null()), null, null]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class)), null, null]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int())), null, null]; + yield ['donotexist', null, null, null]; + yield ['staticGetter', null, null, null]; + yield ['staticSetter', null, null, null]; + yield ['emptyVar', null, 'This should not be removed.', null]; + yield ['arrayWithKeys', Type::dict(Type::string()), null, null]; + yield ['arrayOfMixed', Type::dict(Type::mixed()), null, null]; + yield ['listOfStrings', Type::list(Type::string()), null, null]; + yield ['self', Type::object(Dummy::class), null, null]; + } + + /** + * @dataProvider invalidTypeProvider + */ + public function testInvalid(string $property, ?string $shortDescription, ?string $longDescription) + { + $this->assertNull($this->extractor->getType(InvalidDummy::class, $property)); + $this->assertSame($shortDescription, $this->extractor->getShortDescription(InvalidDummy::class, $property)); + $this->assertSame($longDescription, $this->extractor->getLongDescription(InvalidDummy::class, $property)); + } + + /** + * @return iterable + */ + public static function invalidTypeProvider(): iterable + { + yield 'pub' => ['pub', null, null]; + yield 'stat' => ['stat', null, null]; + yield 'bar' => ['bar', 'Bar.', null]; + } + + /** + * @dataProvider typeWithNoPrefixesProvider + */ + public function testExtractTypesWithNoPrefixes(string $property, ?Type $type) + { + $noPrefixExtractor = new PhpDocExtractor(null, [], [], []); + + $this->assertEquals($type, $noPrefixExtractor->getType(Dummy::class, $property)); + } + + public static function typeWithNoPrefixesProvider() + { + yield ['foo', null]; + yield ['bar', Type::string()]; + yield ['baz', Type::int()]; + yield ['foo2', Type::float()]; + yield ['foo3', Type::callable()]; + yield ['foo4', Type::null()]; + yield ['foo5', Type::mixed()]; + yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource())]; + yield ['bal', Type::object(\DateTimeImmutable::class)]; + yield ['parent', Type::object(ParentDummy::class)]; + yield ['collection', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['nestedCollection', Type::list(Type::list(Type::string()))]; + yield ['mixedCollection', Type::array()]; + yield ['a', null]; + yield ['b', null]; + yield ['c', null]; + yield ['d', null]; + yield ['e', null]; + yield ['f', null]; + yield ['g', Type::nullable(Type::array())]; + yield ['h', Type::nullable(Type::string())]; + yield ['i', Type::union(Type::int(), Type::string(), Type::null())]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))]; + yield ['donotexist', null]; + yield ['staticGetter', null]; + yield ['staticSetter', null]; + } + + /** + * @dataProvider provideCollectionTypes + */ + public function testExtractCollection(string $property, ?Type $type) + { + $this->testExtract($property, $type, null, null); + } + + /** + * @return iterable + */ + public static function provideCollectionTypes(): iterable + { + yield ['iteratorCollection', Type::collection(Type::object(\Iterator::class), Type::string())]; + yield ['iteratorCollectionWithKey', Type::collection(Type::object(\Iterator::class), Type::string(), Type::int())]; + yield ['nestedIterators', Type::collection(Type::object(\Iterator::class), Type::collection(Type::object(\Iterator::class), Type::string(), Type::int()), Type::int())]; + yield ['arrayWithKeys', Type::dict(Type::string()), null, null]; + yield ['arrayWithKeysAndComplexValue', Type::dict(Type::nullable(Type::array(Type::nullable(Type::string()), Type::int()))), null, null]; + } + + /** + * @dataProvider typeWithCustomPrefixesProvider + */ + public function testExtractTypeWithCustomPrefixes(string $property, ?Type $type) + { + $customExtractor = new PhpDocExtractor(null, ['add', 'remove'], ['is', 'can']); + + $this->assertEquals($type, $customExtractor->getType(Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function typeWithCustomPrefixesProvider(): iterable + { + yield ['foo', null]; + yield ['bar', Type::string()]; + yield ['baz', Type::int()]; + yield ['foo2', Type::float()]; + yield ['foo3', Type::callable()]; + yield ['foo4', Type::null()]; + yield ['foo5', Type::mixed()]; + yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource())]; + yield ['bal', Type::object(\DateTimeImmutable::class)]; + yield ['parent', Type::object(ParentDummy::class)]; + yield ['collection', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['nestedCollection', Type::list(Type::list(Type::string()))]; + yield ['mixedCollection', Type::array()]; + yield ['a', null]; + yield ['b', null]; + yield ['c', Type::nullable(Type::bool())]; + yield ['d', Type::bool()]; + yield ['e', Type::list(Type::resource())]; + yield ['f', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['g', Type::nullable(Type::array())]; + yield ['h', Type::nullable(Type::string())]; + yield ['i', Type::union(Type::int(), Type::string(), Type::null())]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))]; + yield ['nonNullableCollectionOfNullableElements', Type::list(Type::nullable(Type::int()))]; + yield ['nullableCollectionOfMultipleNonNullableElementTypes', Type::nullable(Type::list(Type::union(Type::int(), Type::string())))]; + yield ['donotexist', null]; + yield ['staticGetter', null]; + yield ['staticSetter', null]; + } + + /** + * @dataProvider dockBlockFallbackTypesProvider + */ + public function testDocBlockFallback(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(DockBlockFallback::class, $property)); + } + + /** + * @return iterable + */ + public static function dockBlockFallbackTypesProvider(): iterable + { + yield ['pub', Type::string()]; + yield ['protAcc', Type::int()]; + yield ['protMut', Type::bool()]; + } + + /** + * @dataProvider propertiesDefinedByTraitsProvider + */ + public function testPropertiesDefinedByTraits(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(DummyUsingTrait::class, $property)); + } + + /** + * @return iterable + */ + public static function propertiesDefinedByTraitsProvider(): iterable + { + yield ['propertyInTraitPrimitiveType', Type::string()]; + yield ['propertyInTraitObjectSameNamespace', Type::object(DummyUsedInTrait::class)]; + yield ['propertyInTraitObjectDifferentNamespace', Type::object(Dummy::class)]; + yield ['propertyInExternalTraitPrimitiveType', Type::string()]; + yield ['propertyInExternalTraitObjectSameNamespace', Type::object(Dummy::class)]; + yield ['propertyInExternalTraitObjectDifferentNamespace', Type::object(DummyUsedInTrait::class)]; + } + + /** + * @dataProvider methodsDefinedByTraitsProvider + */ + public function testMethodsDefinedByTraits(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(DummyUsingTrait::class, $property)); + } + + /** + * @return iterable + */ + public static function methodsDefinedByTraitsProvider(): iterable + { + yield ['methodInTraitPrimitiveType', Type::string()]; + yield ['methodInTraitObjectSameNamespace', Type::object(DummyUsedInTrait::class)]; + yield ['methodInTraitObjectDifferentNamespace', Type::object(Dummy::class)]; + yield ['methodInExternalTraitPrimitiveType', Type::string()]; + yield ['methodInExternalTraitObjectSameNamespace', Type::object(Dummy::class)]; + yield ['methodInExternalTraitObjectDifferentNamespace', Type::object(DummyUsedInTrait::class)]; + } + + /** + * @param class-string $class + * + * @dataProvider propertiesStaticTypeProvider + */ + public function testPropertiesStaticType(string $class, string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType($class, $property)); + } + + /** + * @return iterable + */ + public static function propertiesStaticTypeProvider(): iterable + { + yield [ParentDummy::class, 'propertyTypeStatic', Type::object(ParentDummy::class)]; + yield [Dummy::class, 'propertyTypeStatic', Type::object(Dummy::class)]; + } + + /** + * @param class-string $class + * + * @dataProvider propertiesParentTypeProvider + */ + public function testPropertiesParentType(string $class, string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType($class, $property)); + } + + /** + * @return iterable + */ + public static function propertiesParentTypeProvider(): iterable + { + yield [ParentDummy::class, 'parentAnnotationNoParent', Type::object('parent')]; + yield [Dummy::class, 'parentAnnotation', Type::object(ParentDummy::class)]; + } + + public function testUnknownPseudoType() + { + $this->assertEquals(Type::object('scalar'), $this->extractor->getType(PseudoTypeDummy::class, 'unknownPseudoType')); + } + + /** + * @dataProvider constructorTypesProvider + */ + public function testExtractConstructorType(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getTypeFromConstructor(ConstructorDummy::class, $property)); + } + + /** + * @return iterable + */ + public static function constructorTypesProvider(): iterable + { + yield ['date', Type::int()]; + yield ['timezone', Type::object(\DateTimeZone::class)]; + yield ['dateObject', Type::object(\DateTimeInterface::class)]; + yield ['dateTime', null]; + yield ['ddd', null]; + yield ['mixed', Type::mixed()]; + } + + /** + * @dataProvider pseudoTypeProvider + */ + public function testPseudoType(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(PseudoTypesDummy::class, $property)); + } + + /** + * @return iterable + */ + public static function pseudoTypeProvider(): iterable + { + yield ['classString', Type::string()]; + yield ['classStringGeneric', Type::string()]; + yield ['htmlEscapedString', Type::string()]; + yield ['lowercaseString', Type::string()]; + yield ['nonEmptyLowercaseString', Type::string()]; + yield ['nonEmptyString', Type::string()]; + yield ['numericString', Type::string()]; + yield ['traitString', Type::string()]; + yield ['positiveInt', Type::int()]; + } + + /** + * @dataProvider promotedPropertyProvider + */ + public function testExtractPromotedProperty(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(Php80Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function promotedPropertyProvider(): iterable + { + yield ['promoted', null]; + yield ['promotedAndMutated', Type::string()]; + } } class EmptyDocBlock diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index f7d931b1ba4da..1e76a5272b2dd 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -14,17 +14,26 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyNamespace; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyPropertyAndGetterWithDifferentTypes; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType; +use Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80PromotedDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\PhpStanPseudoTypesDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\RootDummy\RootDummyItem; use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait; use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Exception\LogicException; +use Symfony\Component\TypeInfo\Type; require_once __DIR__.'/../Fixtures/Extractor/DummyNamespace.php'; @@ -43,19 +52,24 @@ protected function setUp(): void } /** - * @dataProvider typesProvider + * @group legacy + * + * @dataProvider provideLegacyTypes */ - public function testExtract($property, ?array $type = null) + public function testExtractLegacy($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } - public function testParamTagTypeIsOmitted() + /** + * @group legacy + */ + public function testParamTagTypeIsOmittedLegacy() { $this->assertNull($this->extractor->getTypes(PhpStanOmittedParamTagTypeDocBlock::class, 'omittedType')); } - public static function invalidTypesProvider() + public static function provideLegacyInvalidTypes() { return [ 'pub' => ['pub'], @@ -66,118 +80,124 @@ public static function invalidTypesProvider() } /** - * @dataProvider invalidTypesProvider + * @group legacy + * + * @dataProvider provideLegacyInvalidTypes */ - public function testInvalid($property) + public function testInvalidLegacy($property) { $this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy', $property)); } /** - * @dataProvider typesWithNoPrefixesProvider + * @group legacy + * + * @dataProvider provideLegacyTypesWithNoPrefixes */ - public function testExtractTypesWithNoPrefixes($property, ?array $type = null) + public function testExtractTypesWithNoPrefixesLegacy($property, ?array $type = null) { $noPrefixExtractor = new PhpStanExtractor([], [], []); $this->assertEquals($type, $noPrefixExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } - public static function typesProvider() + public static function provideLegacyTypes() { return [ ['foo', null], - ['bar', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['baz', [new Type(Type::BUILTIN_TYPE_INT)]], - ['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)]], - ['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)]], - ['foo4', [new Type(Type::BUILTIN_TYPE_NULL)]], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['baz', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['foo2', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['foo3', [new LegacyType(LegacyType::BUILTIN_TYPE_CALLABLE)]], + ['foo4', [new LegacyType(LegacyType::BUILTIN_TYPE_NULL)]], ['foo5', null], [ 'files', [ - new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), - new Type(Type::BUILTIN_TYPE_RESOURCE), + new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), + new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE), ], ], - ['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], - ['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], - ['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], - ['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))]], - ['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], null)]], - ['a', [new Type(Type::BUILTIN_TYPE_INT)]], - ['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], - ['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)]], - ['d', [new Type(Type::BUILTIN_TYPE_BOOL)]], - ['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))]], - ['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], - ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], - ['h', [new Type(Type::BUILTIN_TYPE_STRING, true)]], - ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]], - ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))]], + ['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], + ['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], + ['nestedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)))]], + ['mixedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], null)]], + ['a', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['b', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], + ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL, true)]], + ['d', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], + ['e', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE))]], + ['f', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], + ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)]], + ['h', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true)]], + ['j', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]], + ['nullableCollectionOfNonNullableElements', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_INT, false))]], ['donotexist', null], ['staticGetter', null], ['staticSetter', null], ['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))]], - ['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)]], + ['arrayWithKeys', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], + ['arrayOfMixed', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_STRING), null)]], + ['listOfStrings', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], + ['self', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)]], + ['rootDummyItems', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RootDummyItem::class))]], + ['rootDummyItem', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RootDummyItem::class)]], ]; } /** - * @dataProvider provideCollectionTypes + * @group legacy + * + * @dataProvider provideLegacyCollectionTypes */ - public function testExtractCollection($property, ?array $type = null) + public function testExtractCollectionLegacy($property, ?array $type = null) { - $this->testExtract($property, $type); + $this->testExtractLegacy($property, $type); } - public static function provideCollectionTypes() + public static function provideLegacyCollectionTypes() { return [ - ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new Type(Type::BUILTIN_TYPE_STRING))]], - ['iteratorCollectionWithKey', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]], + ['iteratorCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], + ['iteratorCollectionWithKey', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], [ 'nestedIterators', - [new Type( - Type::BUILTIN_TYPE_OBJECT, + [new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING)) )], ], [ 'arrayWithKeys', - [new Type( - Type::BUILTIN_TYPE_ARRAY, + [new LegacyType( + LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type(Type::BUILTIN_TYPE_STRING) + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType(LegacyType::BUILTIN_TYPE_STRING) )], ], [ 'arrayWithKeysAndComplexValue', - [new Type( - Type::BUILTIN_TYPE_ARRAY, + [new LegacyType( + LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, - new Type(Type::BUILTIN_TYPE_STRING), - new Type( - Type::BUILTIN_TYPE_ARRAY, + new LegacyType(LegacyType::BUILTIN_TYPE_STRING), + new LegacyType( + LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_STRING, true) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true) ) )], ], @@ -185,253 +205,277 @@ public static function provideCollectionTypes() } /** - * @dataProvider typesWithCustomPrefixesProvider + * @group legacy + * + * @dataProvider provideLegacyTypesWithCustomPrefixes */ - public function testExtractTypesWithCustomPrefixes($property, ?array $type = null) + public function testExtractTypesWithCustomPrefixesLegacy($property, ?array $type = null) { $customExtractor = new PhpStanExtractor(['add', 'remove'], ['is', 'can']); $this->assertEquals($type, $customExtractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property)); } - public static function typesWithCustomPrefixesProvider() + public static function provideLegacyTypesWithCustomPrefixes() { return [ ['foo', null], - ['bar', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['baz', [new Type(Type::BUILTIN_TYPE_INT)]], - ['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)]], - ['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)]], - ['foo4', [new Type(Type::BUILTIN_TYPE_NULL)]], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['baz', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['foo2', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['foo3', [new LegacyType(LegacyType::BUILTIN_TYPE_CALLABLE)]], + ['foo4', [new LegacyType(LegacyType::BUILTIN_TYPE_NULL)]], ['foo5', null], [ 'files', [ - new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), - new Type(Type::BUILTIN_TYPE_RESOURCE), + new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), + new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE), ], ], - ['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], - ['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], - ['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], - ['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))]], - ['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], null)]], + ['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], + ['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], + ['nestedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)))]], + ['mixedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], null)]], ['a', null], ['b', null], - ['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)]], - ['d', [new Type(Type::BUILTIN_TYPE_BOOL)]], - ['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))]], - ['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], - ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], - ['h', [new Type(Type::BUILTIN_TYPE_STRING, true)]], - ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]], - ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))]], + ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL, true)]], + ['d', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], + ['e', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE))]], + ['f', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], + ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)]], + ['h', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true)]], + ['j', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]], + ['nullableCollectionOfNonNullableElements', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_INT, false))]], ['donotexist', null], ['staticGetter', null], ['staticSetter', null], ]; } - public static function typesWithNoPrefixesProvider() + public static function provideLegacyTypesWithNoPrefixes() { return [ ['foo', null], - ['bar', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['baz', [new Type(Type::BUILTIN_TYPE_INT)]], - ['foo2', [new Type(Type::BUILTIN_TYPE_FLOAT)]], - ['foo3', [new Type(Type::BUILTIN_TYPE_CALLABLE)]], - ['foo4', [new Type(Type::BUILTIN_TYPE_NULL)]], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['baz', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['foo2', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['foo3', [new LegacyType(LegacyType::BUILTIN_TYPE_CALLABLE)]], + ['foo4', [new LegacyType(LegacyType::BUILTIN_TYPE_NULL)]], ['foo5', null], [ 'files', [ - new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), - new Type(Type::BUILTIN_TYPE_RESOURCE), + new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'SplFileInfo')), + new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE), ], ], - ['bal', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], - ['parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], - ['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], - ['nestedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING, false)))]], - ['mixedCollection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], null)]], + ['bal', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], + ['parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], + ['nestedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)))]], + ['mixedCollection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], null)]], ['a', null], ['b', null], ['c', null], ['d', null], ['e', null], ['f', null], - ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], - ['h', [new Type(Type::BUILTIN_TYPE_STRING, true)]], - ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]], - ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))]], + ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)]], + ['h', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true)]], + ['j', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'DateTimeImmutable')]], + ['nullableCollectionOfNonNullableElements', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_INT, false))]], ['donotexist', null], ['staticGetter', null], ['staticSetter', null], ]; } - public static function dockBlockFallbackTypesProvider() + public static function provideLegacyDockBlockFallbackTypes() { return [ 'pub' => [ - 'pub', [new Type(Type::BUILTIN_TYPE_STRING)], + 'pub', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], ], 'protAcc' => [ - 'protAcc', [new Type(Type::BUILTIN_TYPE_INT)], + 'protAcc', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], ], 'protMut' => [ - 'protMut', [new Type(Type::BUILTIN_TYPE_BOOL)], + 'protMut', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)], ], ]; } /** - * @dataProvider dockBlockFallbackTypesProvider + * @group legacy + * + * @dataProvider provideLegacyDockBlockFallbackTypes */ - public function testDocBlockFallback($property, $types) + public function testDocBlockFallbackLegacy($property, $types) { $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback', $property)); } /** - * @dataProvider propertiesDefinedByTraitsProvider + * @group legacy + * + * @dataProvider provideLegacyPropertiesDefinedByTraits */ - public function testPropertiesDefinedByTraits(string $property, Type $type) + public function testPropertiesDefinedByTraitsLegacy(string $property, LegacyType $type) { $this->assertEquals([$type], $this->extractor->getTypes(DummyUsingTrait::class, $property)); } - public static function propertiesDefinedByTraitsProvider(): array + public static function provideLegacyPropertiesDefinedByTraits(): array { return [ - ['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], - ['propertyInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], - ['propertyInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], + ['propertyInTraitPrimitiveType', new LegacyType(LegacyType::BUILTIN_TYPE_STRING)], + ['propertyInTraitObjectSameNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], + ['propertyInTraitObjectDifferentNamespace', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], ]; } /** - * @dataProvider propertiesStaticTypeProvider + * @group legacy + * + * @dataProvider provideLegacyPropertiesStaticType */ - public function testPropertiesStaticType(string $class, string $property, Type $type) + public function testPropertiesStaticTypeLegacy(string $class, string $property, LegacyType $type) { $this->assertEquals([$type], $this->extractor->getTypes($class, $property)); } - public static function propertiesStaticTypeProvider(): array + public static function provideLegacyPropertiesStaticType(): array { return [ - [ParentDummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)], - [Dummy::class, 'propertyTypeStatic', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], + [ParentDummy::class, 'propertyTypeStatic', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)], + [Dummy::class, 'propertyTypeStatic', new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], ]; } /** - * @dataProvider propertiesParentTypeProvider + * @group legacy + * + * @dataProvider provideLegacyPropertiesParentType */ - public function testPropertiesParentType(string $class, string $property, ?array $types) + public function testPropertiesParentTypeLegacy(string $class, string $property, ?array $types) { $this->assertEquals($types, $this->extractor->getTypes($class, $property)); } - public static function propertiesParentTypeProvider(): array + public static function provideLegacyPropertiesParentType(): array { return [ - [ParentDummy::class, 'parentAnnotationNoParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'parent')]], - [Dummy::class, 'parentAnnotation', [new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], + [ParentDummy::class, 'parentAnnotationNoParent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'parent')]], + [Dummy::class, 'parentAnnotation', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], ]; } /** - * @dataProvider constructorTypesProvider + * @group legacy + * + * @dataProvider provideLegacyConstructorTypes */ - public function testExtractConstructorTypes($property, ?array $type = null) + public function testExtractConstructorTypesLegacy($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } /** - * @dataProvider constructorTypesProvider + * @group legacy + * + * @dataProvider provideLegacyConstructorTypes */ - public function testExtractConstructorTypesReturnNullOnEmptyDocBlock($property) + public function testExtractConstructorTypesReturnNullOnEmptyDocBlockLegacy($property) { $this->assertNull($this->extractor->getTypesFromConstructor(ConstructorDummyWithoutDocBlock::class, $property)); } - public static function constructorTypesProvider() + public static function provideLegacyConstructorTypes() { return [ - ['date', [new Type(Type::BUILTIN_TYPE_INT)]], - ['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], - ['dateObject', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]], + ['date', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['timezone', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], + ['dateObject', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeInterface')]], ['dateTime', null], ['ddd', null], ]; } /** - * @dataProvider unionTypesProvider + * @group legacy + * + * @dataProvider provideLegacyUnionTypes */ - public function testExtractorUnionTypes(string $property, ?array $types) + public function testExtractorUnionTypesLegacy(string $property, ?array $types) { $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType', $property)); } - public static function unionTypesProvider(): array + public static function provideLegacyUnionTypes(): array { return [ - ['a', [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)]], - ['b', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]], - ['c', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]], - ['d', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING)])])]], - ['e', [new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class, true, [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING)])], [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [new Type(Type::BUILTIN_TYPE_INT)], [new Type(Type::BUILTIN_TYPE_STRING, false, null, true, [], [new Type(Type::BUILTIN_TYPE_OBJECT, false, DefaultValue::class)])])]), new Type(Type::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], + ['a', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['b', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)])]], + ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)])]], + ['d', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)], [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])])]], + ['e', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, Dummy::class, true, [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])], [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [new LegacyType(LegacyType::BUILTIN_TYPE_INT)], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DefaultValue::class)])])]), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ParentDummy::class)]], ['f', null], - ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, [], [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)])]], + ['g', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, [], [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)])]], ]; } /** - * @dataProvider pseudoTypesProvider + * @group legacy + * + * @dataProvider provideLegacyPseudoTypes */ - public function testPseudoTypes($property, array $type) + public function testPseudoTypesLegacy($property, array $type) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\PhpStanPseudoTypesDummy', $property)); } - public static function pseudoTypesProvider(): array + public static function provideLegacyPseudoTypes(): array { return [ - ['classString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['classStringGeneric', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['htmlEscapedString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['lowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['nonEmptyLowercaseString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['nonEmptyString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['numericString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['traitString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['interfaceString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['literalString', [new Type(Type::BUILTIN_TYPE_STRING, false, null)]], - ['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))]], - ['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)]], - ['arrayKey', [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)]], - ['double', [new Type(Type::BUILTIN_TYPE_FLOAT)]], + ['classString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['classStringGeneric', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['htmlEscapedString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['lowercaseString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['nonEmptyLowercaseString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['nonEmptyString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['numericString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['traitString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['interfaceString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['literalString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false, null)]], + ['positiveInt', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, false, null)]], + ['negativeInt', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, false, null)]], + ['nonEmptyArray', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true)]], + ['nonEmptyList', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT))]], + ['scalar', [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], + ['number', [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['numeric', [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['arrayKey', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['double', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], ]; } - public function testDummyNamespace() + /** + * @group legacy + */ + public function testDummyNamespaceLegacy() { $this->assertEquals( - [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')], $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\DummyNamespace', 'dummy') ); } - public function testDummyNamespaceWithProperty() + /** + * @group legacy + */ + public function testDummyNamespaceWithPropertyLegacy() { $phpStanTypes = $this->extractor->getTypes(\B\Dummy::class, 'property'); $phpDocTypes = $this->phpDocExtractor->getTypes(\B\Dummy::class, 'property'); @@ -441,60 +485,468 @@ public function testDummyNamespaceWithProperty() } /** - * @dataProvider intRangeTypeProvider + * @group legacy + * + * @dataProvider provideLegacyIntRangeType */ - public function testExtractorIntRangeType(string $property, ?array $types) + public function testExtractorIntRangeTypeLegacy(string $property, ?array $types) { $this->assertEquals($types, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy', $property)); } - public static function intRangeTypeProvider(): array + public static function provideLegacyIntRangeType(): array { return [ - ['a', [new Type(Type::BUILTIN_TYPE_INT)]], - ['b', [new Type(Type::BUILTIN_TYPE_INT, true)]], - ['c', [new Type(Type::BUILTIN_TYPE_INT)]], + ['a', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['b', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, true)]], + ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], ]; } /** - * @dataProvider php80TypesProvider + * @group legacy + * + * @dataProvider provideLegacyPhp80Types */ - public function testExtractPhp80Type(string $class, $property, ?array $type = null) + public function testExtractPhp80TypeLegacy(string $class, $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes($class, $property, [])); } - public static function php80TypesProvider() + public static function provideLegacyPhp80Types() { return [ - [Php80Dummy::class, 'promotedWithDocCommentAndType', [new Type(Type::BUILTIN_TYPE_INT)]], - [Php80Dummy::class, 'promotedWithDocComment', [new Type(Type::BUILTIN_TYPE_STRING)]], - [Php80Dummy::class, 'promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]], + [Php80Dummy::class, 'promotedWithDocCommentAndType', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + [Php80Dummy::class, 'promotedWithDocComment', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + [Php80Dummy::class, 'promotedAndMutated', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], [Php80Dummy::class, 'promoted', null], - [Php80Dummy::class, 'collection', [new Type(Type::BUILTIN_TYPE_ARRAY, collection: true, collectionValueType: new Type(Type::BUILTIN_TYPE_STRING))]], + [Php80Dummy::class, 'collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, collection: true, collectionValueType: new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], [Php80PromotedDummy::class, 'promoted', null], ]; } - public static function allowPrivateAccessProvider(): array + /** + * @group legacy + * + * @dataProvider allowPrivateAccessLegacyProvider + */ + public function testAllowPrivateAccessLegacy(bool $allowPrivateAccess, array $expectedTypes) + { + $extractor = new PhpStanExtractor(allowPrivateAccess: $allowPrivateAccess); + $this->assertEquals( + $expectedTypes, + $extractor->getTypes(DummyPropertyAndGetterWithDifferentTypes::class, 'foo') + ); + } + + public static function allowPrivateAccessLegacyProvider(): array { return [ - [true, [new Type(Type::BUILTIN_TYPE_STRING)]], - [false, [new Type(Type::BUILTIN_TYPE_ARRAY, collection: true, collectionKeyType: new Type('int'), collectionValueType: new Type('string'))]], + [true, [new LegacyType('string')]], + [false, [new LegacyType('array', collection: true, collectionKeyType: new LegacyType('int'), collectionValueType: new LegacyType('string'))]], ]; } + /** + * @dataProvider typesProvider + */ + public function testExtract(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(Dummy::class, $property)); + } + + public static function typesProvider(): iterable + { + yield ['foo', null]; + yield ['bar', Type::string()]; + yield ['baz', Type::int()]; + yield ['foo2', Type::float()]; + yield ['foo3', Type::callable()]; + yield ['foo5', Type::mixed()]; + yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource()), null, null]; + yield ['bal', Type::object(\DateTimeImmutable::class)]; + yield ['parent', Type::object(ParentDummy::class)]; + yield ['collection', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['nestedCollection', Type::list(Type::list(Type::string()))]; + yield ['mixedCollection', Type::list()]; + yield ['a', Type::int()]; + yield ['b', Type::nullable(Type::object(ParentDummy::class))]; + yield ['c', Type::nullable(Type::bool())]; + yield ['d', Type::bool()]; + yield ['e', Type::list(Type::resource())]; + yield ['f', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['g', Type::nullable(Type::array())]; + yield ['h', Type::nullable(Type::string())]; + yield ['i', Type::union(Type::int(), Type::string(), Type::null())]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))]; + yield ['donotexist', null]; + yield ['staticGetter', null]; + yield ['staticSetter', null]; + yield ['emptyVar', null]; + yield ['arrayWithKeys', Type::dict(Type::string())]; + yield ['arrayOfMixed', Type::dict(Type::mixed())]; + yield ['listOfStrings', Type::list(Type::string())]; + yield ['self', Type::object(Dummy::class)]; + yield ['rootDummyItems', Type::list(Type::object(RootDummyItem::class))]; + yield ['rootDummyItem', Type::object(RootDummyItem::class)]; + } + + public function testParamTagTypeIsOmitted() + { + $this->assertNull($this->extractor->getType(PhpStanOmittedParamTagTypeDocBlock::class, 'omittedType')); + } + + /** + * @dataProvider invalidTypesProvider + */ + public function testInvalid(string $property) + { + $this->assertNull($this->extractor->getType(InvalidDummy::class, $property)); + } + + /** + * @return iterable + */ + public static function invalidTypesProvider(): iterable + { + yield 'pub' => ['pub']; + yield 'stat' => ['stat']; + yield 'foo' => ['foo']; + yield 'bar' => ['bar']; + } + + /** + * @dataProvider typesWithNoPrefixesProvider + */ + public function testExtractTypesWithNoPrefixes(string $property, ?Type $type) + { + $noPrefixExtractor = new PhpStanExtractor([], [], []); + + $this->assertEquals($type, $noPrefixExtractor->getType(Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function typesWithNoPrefixesProvider(): iterable + { + yield ['foo', null]; + yield ['bar', Type::string()]; + yield ['baz', Type::int()]; + yield ['foo2', Type::float()]; + yield ['foo3', Type::callable()]; + yield ['foo5', Type::mixed()]; + yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource())]; + yield ['bal', Type::object(\DateTimeImmutable::class)]; + yield ['parent', Type::object(ParentDummy::class)]; + yield ['collection', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['nestedCollection', Type::list(Type::list(Type::string()))]; + yield ['mixedCollection', Type::list()]; + yield ['a', null]; + yield ['b', null]; + yield ['c', null]; + yield ['d', null]; + yield ['e', null]; + yield ['f', null]; + yield ['g', Type::nullable(Type::array())]; + yield ['h', Type::nullable(Type::string())]; + yield ['i', Type::union(Type::int(), Type::string(), Type::null())]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))]; + yield ['donotexist', null]; + yield ['staticGetter', null]; + yield ['staticSetter', null]; + } + + /** + * @dataProvider provideCollectionTypes + */ + public function testExtractCollection($property, ?Type $type) + { + $this->testExtract($property, $type); + } + + /** + * @return iterable + */ + public static function provideCollectionTypes(): iterable + { + yield ['iteratorCollection', Type::collection(Type::object(\Iterator::class), Type::string())]; + yield ['iteratorCollectionWithKey', Type::collection(Type::object(\Iterator::class), Type::string(), Type::int())]; + yield ['nestedIterators', Type::collection(Type::object(\Iterator::class), Type::collection(Type::object(\Iterator::class), Type::string(), Type::int()), Type::int())]; + yield ['arrayWithKeys', Type::dict(Type::string()), null, null]; + yield ['arrayWithKeysAndComplexValue', Type::dict(Type::nullable(Type::array(Type::nullable(Type::string()), Type::int()))), null, null]; + } + + /** + * @dataProvider typesWithCustomPrefixesProvider + */ + public function testExtractTypesWithCustomPrefixes(string $property, ?Type $type) + { + $customExtractor = new PhpStanExtractor(['add', 'remove'], ['is', 'can']); + + $this->assertEquals($type, $customExtractor->getType(Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function typesWithCustomPrefixesProvider(): iterable + { + yield ['foo', null]; + yield ['bar', Type::string()]; + yield ['baz', Type::int()]; + yield ['foo2', Type::float()]; + yield ['foo3', Type::callable()]; + yield ['foo5', Type::mixed()]; + yield ['files', Type::union(Type::list(Type::object(\SplFileInfo::class)), Type::resource())]; + yield ['bal', Type::object(\DateTimeImmutable::class)]; + yield ['parent', Type::object(ParentDummy::class)]; + yield ['collection', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['nestedCollection', Type::list(Type::list(Type::string()))]; + yield ['mixedCollection', Type::list()]; + yield ['a', null]; + yield ['b', null]; + yield ['c', Type::nullable(Type::bool())]; + yield ['d', Type::bool()]; + yield ['e', Type::list(Type::resource())]; + yield ['f', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['g', Type::nullable(Type::array())]; + yield ['h', Type::nullable(Type::string())]; + yield ['i', Type::union(Type::int(), Type::string(), Type::null())]; + yield ['j', Type::nullable(Type::object(\DateTimeImmutable::class))]; + yield ['nullableCollectionOfNonNullableElements', Type::nullable(Type::list(Type::int()))]; + yield ['nonNullableCollectionOfNullableElements', Type::array(Type::nullable(Type::int()))]; + yield ['nullableCollectionOfMultipleNonNullableElementTypes', Type::nullable(Type::array(Type::union(Type::int(), Type::string())))]; + yield ['donotexist', null]; + yield ['staticGetter', null]; + yield ['staticSetter', null]; + } + + /** + * @dataProvider dockBlockFallbackTypesProvider + */ + public function testDocBlockFallback(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(DockBlockFallback::class, $property)); + } + + /** + * @return iterable + */ + public static function dockBlockFallbackTypesProvider(): iterable + { + yield ['pub', Type::string()]; + yield ['protAcc', Type::int()]; + yield ['protMut', Type::bool()]; + } + + /** + * @dataProvider propertiesDefinedByTraitsProvider + */ + public function testPropertiesDefinedByTraits(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(DummyUsingTrait::class, $property)); + } + + /** + * @return iterable + */ + public static function propertiesDefinedByTraitsProvider(): iterable + { + yield ['propertyInTraitPrimitiveType', Type::string()]; + yield ['propertyInTraitObjectSameNamespace', Type::object(DummyUsedInTrait::class)]; + yield ['propertyInTraitObjectDifferentNamespace', Type::object(Dummy::class)]; + } + + /** + * @dataProvider propertiesStaticTypeProvider + */ + public function testPropertiesStaticType(string $class, string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType($class, $property)); + } + + /** + * @return iterable + */ + public static function propertiesStaticTypeProvider(): iterable + { + yield [ParentDummy::class, 'propertyTypeStatic', Type::object(ParentDummy::class)]; + yield [Dummy::class, 'propertyTypeStatic', Type::object(Dummy::class)]; + } + + public function testPropertiesParentType() + { + $this->assertEquals(Type::object(ParentDummy::class), $this->extractor->getType(Dummy::class, 'parentAnnotation')); + } + + public function testPropertiesParentTypeThrowWithoutParent() + { + $this->expectException(LogicException::class); + $this->extractor->getType(ParentDummy::class, 'parentAnnotationNoParent'); + } + + /** + * @dataProvider constructorTypesProvider + */ + public function testExtractConstructorTypes(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getTypeFromConstructor(ConstructorDummy::class, $property)); + } + + /** + * @dataProvider constructorTypesProvider + */ + public function testExtractConstructorTypesReturnNullOnEmptyDocBlock(string $property) + { + $this->assertNull($this->extractor->getTypeFromConstructor(ConstructorDummyWithoutDocBlock::class, $property)); + } + + /** + * @return iterable + */ + public static function constructorTypesProvider(): iterable + { + yield ['date', Type::int()]; + yield ['timezone', Type::object(\DateTimeZone::class)]; + yield ['dateObject', Type::object(\DateTimeInterface::class)]; + yield ['dateTime', null]; + yield ['ddd', null]; + } + + /** + * @dataProvider unionTypesProvider + */ + public function testExtractorUnionTypes(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(DummyUnionType::class, $property)); + } + + /** + * @return iterable + */ + public static function unionTypesProvider(): iterable + { + yield ['a', Type::union(Type::string(), Type::int())]; + yield ['b', Type::list(Type::union(Type::string(), Type::int()))]; + yield ['c', Type::array(Type::union(Type::string(), Type::int()))]; + yield ['d', Type::array(Type::array(Type::string()), Type::union(Type::string(), Type::int()))]; + yield ['e', Type::union( + Type::generic( + Type::object(Dummy::class), + Type::array(Type::string(), Type::mixed()), + Type::union(Type::int(), Type::list(Type::generic(Type::string(), Type::object(DefaultValue::class)))), + ), + Type::object(ParentDummy::class), + Type::null(), + )]; + yield ['f', null]; + yield ['g', Type::array(Type::union(Type::string(), Type::int()))]; + } + + /** + * @dataProvider pseudoTypesProvider + */ + public function testPseudoTypes(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(PhpStanPseudoTypesDummy::class, $property)); + } + + /** + * @return iterable + */ + public static function pseudoTypesProvider(): iterable + { + yield ['classString', Type::string()]; + yield ['classStringGeneric', Type::generic(Type::string(), Type::object(\stdClass::class))]; + yield ['htmlEscapedString', Type::string()]; + yield ['lowercaseString', Type::string()]; + yield ['nonEmptyLowercaseString', Type::string()]; + yield ['nonEmptyString', Type::string()]; + yield ['numericString', Type::string()]; + yield ['traitString', Type::string()]; + yield ['interfaceString', Type::string()]; + yield ['literalString', Type::string()]; + yield ['positiveInt', Type::int()]; + yield ['negativeInt', Type::int()]; + yield ['nonEmptyArray', Type::array()]; + yield ['nonEmptyList', Type::list()]; + yield ['scalar', Type::union(Type::int(), Type::float(), Type::string(), Type::bool())]; + yield ['number', Type::union(Type::int(), Type::float())]; + yield ['numeric', Type::union(Type::int(), Type::float(), Type::string())]; + yield ['arrayKey', Type::union(Type::int(), Type::string())]; + yield ['double', Type::float()]; + } + + public function testDummyNamespace() + { + $this->assertEquals(Type::object(Dummy::class), $this->extractor->getType(DummyNamespace::class, 'dummy')); + } + + public function testDummyNamespaceWithProperty() + { + $phpStanType = $this->extractor->getType(\B\Dummy::class, 'property'); + $phpDocType = $this->phpDocExtractor->getType(\B\Dummy::class, 'property'); + + $this->assertEquals('A\Property', $phpStanType->getClassName()); + $this->assertEquals($phpDocType->getClassName(), $phpStanType->getClassName()); + } + + /** + * @dataProvider intRangeTypeProvider + */ + public function testExtractorIntRangeType(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(IntRangeDummy::class, $property)); + } + + /** + * @return iterable + */ + public static function intRangeTypeProvider(): iterable + { + yield ['a', Type::int()]; + yield ['b', Type::nullable(Type::int())]; + yield ['c', Type::int()]; + } + + /** + * @dataProvider php80TypesProvider + */ + public function testExtractPhp80Type(string $class, string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType($class, $property)); + } + + /** + * @return iterable + */ + public static function php80TypesProvider(): iterable + { + yield [Php80Dummy::class, 'promotedAndMutated', Type::string()]; + yield [Php80Dummy::class, 'promoted', null]; + yield [Php80Dummy::class, 'collection', Type::array(Type::string())]; + yield [Php80PromotedDummy::class, 'promoted', null]; + } + /** * @dataProvider allowPrivateAccessProvider */ - public function testAllowPrivateAccess(bool $allowPrivateAccess, array $expectedTypes) + public function testAllowPrivateAccess(bool $allowPrivateAccess, Type $expectedType) { $extractor = new PhpStanExtractor(allowPrivateAccess: $allowPrivateAccess); - $this->assertEquals( - $expectedTypes, - $extractor->getTypes(DummyPropertyAndGetterWithDifferentTypes::class, 'foo') - ); + + $this->assertEquals($expectedType, $extractor->getType(DummyPropertyAndGetterWithDifferentTypes::class, 'foo')); + } + + public static function allowPrivateAccessProvider(): array + { + return [ + [true, Type::string()], + [false, Type::array(Type::string(), Type::int())], + ]; } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index ac760793686f7..897b9c0341a2b 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -16,18 +16,23 @@ use Symfony\Component\PropertyInfo\PropertyReadInfo; use Symfony\Component\PropertyInfo\PropertyWriteInfo; use Symfony\Component\PropertyInfo\Tests\Fixtures\AdderRemoverDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\NotInstantiable; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php71DummyExtended2; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php74Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php7ParentDummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Php82Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\SnakeCaseDummy; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -204,88 +209,96 @@ public function testGetPropertiesWithNoPrefixes() } /** - * @dataProvider typesProvider + * @group legacy + * + * @dataProvider provideLegacyTypes */ - public function testExtractors($property, ?array $type = null) + public function testExtractorsLegacy($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy', $property, [])); } - public static function typesProvider() + public static function provideLegacyTypes() { return [ ['a', null], - ['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], - ['c', [new Type(Type::BUILTIN_TYPE_BOOL)]], - ['d', [new Type(Type::BUILTIN_TYPE_BOOL)]], + ['b', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], + ['c', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], + ['d', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], ['e', null], - ['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], + ['f', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable'))]], ['donotexist', null], ['staticGetter', null], ['staticSetter', null], - ['self', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')]], - ['realParent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], - ['date', [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]], - ['dates', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class))]], + ['self', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy')]], + ['realParent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')]], + ['date', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]], + ['dates', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class))]], ]; } /** - * @dataProvider php7TypesProvider + * @group legacy + * + * @dataProvider provideLegacyPhp7Types */ - public function testExtractPhp7Type(string $class, string $property, ?array $type = null) + public function testExtractPhp7TypeLegacy(string $class, string $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes($class, $property, [])); } - public static function php7TypesProvider() + public static function provideLegacyPhp7Types() { return [ - [Php7Dummy::class, 'foo', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], - [Php7Dummy::class, 'bar', [new Type(Type::BUILTIN_TYPE_INT)]], - [Php7Dummy::class, 'baz', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]], - [Php7Dummy::class, 'buz', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy')]], - [Php7Dummy::class, 'biz', [new Type(Type::BUILTIN_TYPE_OBJECT, false, Php7ParentDummy::class)]], + [Php7Dummy::class, 'foo', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true)]], + [Php7Dummy::class, 'bar', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + [Php7Dummy::class, 'baz', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], + [Php7Dummy::class, 'buz', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Symfony\Component\PropertyInfo\Tests\Fixtures\Php7Dummy')]], + [Php7Dummy::class, 'biz', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Php7ParentDummy::class)]], [Php7Dummy::class, 'donotexist', null], - [Php7ParentDummy::class, 'parent', [new Type(Type::BUILTIN_TYPE_OBJECT, false, \stdClass::class)]], + [Php7ParentDummy::class, 'parent', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, \stdClass::class)]], ]; } /** - * @dataProvider php71TypesProvider + * @group legacy + * + * @dataProvider provideLegacyPhp71Types */ - public function testExtractPhp71Type($property, ?array $type = null) + public function testExtractPhp71TypeLegacy($property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php71Dummy', $property, [])); } - public static function php71TypesProvider() + public static function provideLegacyPhp71Types() { return [ - ['foo', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], - ['buz', [new Type(Type::BUILTIN_TYPE_NULL)]], - ['bar', [new Type(Type::BUILTIN_TYPE_INT, true)]], - ['baz', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]], + ['foo', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)]], + ['buz', [new LegacyType(LegacyType::BUILTIN_TYPE_NULL)]], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, true)]], + ['baz', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]], ['donotexist', null], ]; } /** - * @dataProvider php80TypesProvider + * @group legacy + * + * @dataProvider provideLegacyPhp80Types */ - public function testExtractPhp80Type($property, ?array $type = null) + public function testExtractPhp80TypeLegacy(string $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy', $property, [])); } - public static function php80TypesProvider() + public static function provideLegacyPhp80Types() { return [ - ['foo', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)]], - ['bar', [new Type(Type::BUILTIN_TYPE_INT, true)]], - ['timeout', [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_FLOAT)]], - ['optional', [new Type(Type::BUILTIN_TYPE_INT, true), new Type(Type::BUILTIN_TYPE_FLOAT, true)]], - ['string', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Stringable'), new Type(Type::BUILTIN_TYPE_STRING)]], + ['foo', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, true, null, true)]], + ['bar', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, true)]], + ['timeout', [new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['optional', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, true), new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT, true)]], + ['string', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Stringable'), new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], ['payload', null], ['data', null], ['mixedProperty', null], @@ -293,18 +306,20 @@ public static function php80TypesProvider() } /** - * @dataProvider php81TypesProvider + * @group legacy + * + * @dataProvider provideLegacyPhp81Types */ - public function testExtractPhp81Type($property, ?array $type = null) + public function testExtractPhp81TypeLegacy(string $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy', $property, [])); } - public static function php81TypesProvider() + public static function provideLegacyPhp81Types() { return [ ['nothing', null], - ['collection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Traversable'), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Countable')]], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Traversable'), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'Countable')]], ]; } @@ -314,18 +329,20 @@ public function testReadonlyPropertiesAreNotWriteable() } /** - * @dataProvider php82TypesProvider + * @group legacy + * + * @dataProvider provideLegacyPhp82Types */ - public function testExtractPhp82Type($property, ?array $type = null) + public function testExtractPhp82TypeLegacy(string $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php82Dummy', $property, [])); } - public static function php82TypesProvider(): iterable + public static function provideLegacyPhp82Types(): iterable { yield ['nil', null]; - yield ['false', [new Type(Type::BUILTIN_TYPE_FALSE)]]; - yield ['true', [new Type(Type::BUILTIN_TYPE_TRUE)]]; + yield ['false', [new LegacyType(LegacyType::BUILTIN_TYPE_FALSE)]]; + yield ['true', [new LegacyType(LegacyType::BUILTIN_TYPE_TRUE)]]; // Nesting intersection and union types is not supported yet, // but we should make sure this kind of composite types does not crash the extractor. @@ -333,20 +350,22 @@ public static function php82TypesProvider(): iterable } /** - * @dataProvider defaultValueProvider + * @group legacy + * + * @dataProvider provideLegacyDefaultValue */ - public function testExtractWithDefaultValue($property, $type) + public function testExtractWithDefaultValueLegacy($property, $type) { $this->assertEquals($type, $this->extractor->getTypes(DefaultValue::class, $property, [])); } - public static function defaultValueProvider() + public static function provideLegacyDefaultValue() { return [ - ['defaultInt', [new Type(Type::BUILTIN_TYPE_INT, false)]], - ['defaultFloat', [new Type(Type::BUILTIN_TYPE_FLOAT, false)]], - ['defaultString', [new Type(Type::BUILTIN_TYPE_STRING, false)]], - ['defaultArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], + ['defaultInt', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, false)]], + ['defaultFloat', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT, false)]], + ['defaultString', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)]], + ['defaultArray', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true)]], ['defaultNull', null], ]; } @@ -474,9 +493,11 @@ public static function getInitializableProperties(): array } /** - * @dataProvider constructorTypesProvider + * @group legacy + * + * @dataProvider provideLegacyConstructorTypes */ - public function testExtractTypeConstructor(string $class, string $property, ?array $type = null) + public function testExtractTypeConstructorLegacy(string $class, string $property, ?array $type = null) { /* Check that constructor extractions works by default, and if passed in via context. Check that null is returned if constructor extraction is disabled */ @@ -485,15 +506,15 @@ public function testExtractTypeConstructor(string $class, string $property, ?arr $this->assertNull($this->extractor->getTypes($class, $property, ['enable_constructor_extraction' => false])); } - public static function constructorTypesProvider(): array + public static function provideLegacyConstructorTypes(): array { return [ // php71 dummy has following constructor: __construct(string $string, int $intPrivate) - [Php71Dummy::class, 'string', [new Type(Type::BUILTIN_TYPE_STRING, false)]], - [Php71Dummy::class, 'intPrivate', [new Type(Type::BUILTIN_TYPE_INT, false)]], + [Php71Dummy::class, 'string', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING, false)]], + [Php71Dummy::class, 'intPrivate', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, false)]], // Php71DummyExtended2 adds int $intWithAccessor - [Php71DummyExtended2::class, 'intWithAccessor', [new Type(Type::BUILTIN_TYPE_INT, false)]], - [Php71DummyExtended2::class, 'intPrivate', [new Type(Type::BUILTIN_TYPE_INT, false)]], + [Php71DummyExtended2::class, 'intWithAccessor', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, false)]], + [Php71DummyExtended2::class, 'intPrivate', [new LegacyType(LegacyType::BUILTIN_TYPE_INT, false)]], [DefaultValue::class, 'foo', null], ]; } @@ -511,13 +532,16 @@ public function testNullOnPrivateProtectedAccessor() $this->assertEquals(PropertyWriteInfo::TYPE_NONE, $bazMutator->getType()); } - public function testTypedProperties() + /** + * @group legacy + */ + public function testTypedPropertiesLegacy() { - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], $this->extractor->getTypes(Php74Dummy::class, 'dummy')); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_BOOL, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableBoolProp')); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], $this->extractor->getTypes(Php74Dummy::class, 'stringCollection')); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_INT, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableWithDefault')); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)], $this->extractor->getTypes(Php74Dummy::class, 'collection')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, Dummy::class)], $this->extractor->getTypes(Php74Dummy::class, 'dummy')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_BOOL, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableBoolProp')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))], $this->extractor->getTypes(Php74Dummy::class, 'stringCollection')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_INT, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableWithDefault')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true)], $this->extractor->getTypes(Php74Dummy::class, 'collection')); } /** @@ -633,21 +657,227 @@ public function testGetWriteInfoReadonlyProperties() } /** - * @dataProvider extractConstructorTypesProvider + * @group legacy + * + * @dataProvider provideLegacyExtractConstructorTypes */ - public function testExtractConstructorTypes(string $property, ?array $type = null) + public function testExtractConstructorTypesLegacy(string $property, ?array $type = null) { $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } - public static function extractConstructorTypesProvider(): array + public static function provideLegacyExtractConstructorTypes(): array { return [ - ['timezone', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], + ['timezone', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeZone')]], ['date', null], ['dateObject', null], - ['dateTime', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], + ['dateTime', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'DateTimeImmutable')]], ['ddd', null], ]; } + + /** + * @dataProvider typesProvider + */ + public function testExtractors(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function typesProvider(): iterable + { + yield ['a', null]; + yield ['b', Type::nullable(Type::object(ParentDummy::class))]; + yield ['e', null]; + yield ['f', Type::list(Type::object(\DateTimeImmutable::class))]; + yield ['donotexist', null]; + yield ['staticGetter', null]; + yield ['staticSetter', null]; + yield ['self', Type::object(Dummy::class)]; + yield ['realParent', Type::object(ParentDummy::class)]; + yield ['date', Type::object(\DateTimeImmutable::class)]; + yield ['dates', Type::list(Type::object(\DateTimeImmutable::class))]; + } + + /** + * @dataProvider php7TypesProvider + */ + public function testExtractPhp7Type(string $class, string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType($class, $property)); + } + + /** + * @return iterable + */ + public static function php7TypesProvider(): iterable + { + yield [Php7Dummy::class, 'foo', Type::array()]; + yield [Php7Dummy::class, 'bar', Type::int()]; + yield [Php7Dummy::class, 'baz', Type::list(Type::string())]; + yield [Php7Dummy::class, 'buz', Type::object(Php7Dummy::class)]; + yield [Php7Dummy::class, 'biz', Type::object(Php7ParentDummy::class)]; + yield [Php7Dummy::class, 'donotexist', null]; + yield [Php7ParentDummy::class, 'parent', Type::object(\stdClass::class)]; + } + + /** + * @dataProvider php71TypesProvider + */ + public function testExtractPhp71Type(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(Php71Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function php71TypesProvider(): iterable + { + yield ['foo', Type::nullable(Type::array())]; + yield ['buz', Type::void()]; + yield ['bar', Type::nullable(Type::int())]; + yield ['baz', Type::list(Type::string())]; + yield ['donotexist', null]; + } + + /** + * @dataProvider php80TypesProvider + */ + public function testExtractPhp80Type(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(Php80Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function php80TypesProvider(): iterable + { + yield ['foo', Type::nullable(Type::array())]; + yield ['bar', Type::nullable(Type::int())]; + yield ['timeout', Type::union(Type::int(), Type::float())]; + yield ['optional', Type::union(Type::nullable(Type::int()), Type::nullable(Type::float()))]; + yield ['string', Type::union(Type::string(), Type::object(\Stringable::class))]; + yield ['payload', Type::mixed()]; + yield ['data', Type::mixed()]; + yield ['mixedProperty', Type::mixed()]; + } + + /** + * @dataProvider php81TypesProvider + */ + public function testExtractPhp81Type(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(Php81Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function php81TypesProvider(): iterable + { + yield ['nothing', Type::never()]; + yield ['collection', Type::intersection(Type::object(\Traversable::class), Type::object(\Countable::class))]; + } + + /** + * @dataProvider php82TypesProvider + */ + public function testExtractPhp82Type(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(Php82Dummy::class, $property)); + } + + /** + * @return iterable + */ + public static function php82TypesProvider(): iterable + { + yield ['nil', Type::null()]; + yield ['false', Type::false()]; + yield ['true', Type::true()]; + yield ['someCollection', Type::union(Type::intersection(Type::object(\Traversable::class), Type::object(\Countable::class)), Type::null())]; + } + + /** + * @dataProvider defaultValueProvider + */ + public function testExtractWithDefaultValue(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getType(DefaultValue::class, $property)); + } + + /** + * @return iterable + */ + public static function defaultValueProvider(): iterable + { + yield ['defaultInt', Type::int()]; + yield ['defaultFloat', Type::float()]; + yield ['defaultString', Type::string()]; + yield ['defaultArray', Type::array()]; + yield ['defaultNull', null]; + } + + /** + * @dataProvider constructorTypesProvider + */ + public function testExtractTypeConstructor(string $class, string $property, ?Type $type) + { + /* Check that constructor extractions works by default, and if passed in via context. + Check that null is returned if constructor extraction is disabled */ + $this->assertEquals($type, $this->extractor->getType($class, $property)); + $this->assertEquals($type, $this->extractor->getType($class, $property, ['enable_constructor_extraction' => true])); + $this->assertNull($this->extractor->getType($class, $property, ['enable_constructor_extraction' => false])); + } + + /** + * @return iterable + */ + public static function constructorTypesProvider(): iterable + { + // php71 dummy has following constructor: __construct(string $string, int $intPrivate) + yield [Php71Dummy::class, 'string', Type::string()]; + + // Php71DummyExtended2 adds int $intWithAccessor + yield [Php71DummyExtended2::class, 'intWithAccessor', Type::int()]; + + yield [Php71Dummy::class, 'intPrivate', Type::int()]; + yield [Php71DummyExtended2::class, 'intPrivate', Type::int()]; + yield [DefaultValue::class, 'foo', null]; + } + + public function testTypedProperties() + { + $this->assertEquals(Type::object(Dummy::class), $this->extractor->getType(Php74Dummy::class, 'dummy')); + $this->assertEquals(Type::nullable(Type::bool()), $this->extractor->getType(Php74Dummy::class, 'nullableBoolProp')); + $this->assertEquals(Type::list(Type::string()), $this->extractor->getType(Php74Dummy::class, 'stringCollection')); + $this->assertEquals(Type::nullable(Type::int()), $this->extractor->getType(Php74Dummy::class, 'nullableWithDefault')); + $this->assertEquals(Type::array(), $this->extractor->getType(Php74Dummy::class, 'collection')); + } + + /** + * @dataProvider extractConstructorTypesProvider + */ + public function testExtractConstructorType(string $property, ?Type $type) + { + $this->assertEquals($type, $this->extractor->getTypeFromConstructor(ConstructorDummy::class, $property)); + } + + /** + * @return iterable + */ + public static function extractConstructorTypesProvider(): iterable + { + yield ['timezone', Type::object(\DateTimeZone::class)]; + yield ['date', null]; + yield ['dateObject', null]; + yield ['dateTime', Type::object(\DateTimeImmutable::class)]; + yield ['ddd', null]; + } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php index 31cd4a7f8fffc..cfffd45e0c05f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyExtractor.php @@ -17,7 +17,8 @@ use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -36,12 +37,22 @@ public function getLongDescription($class, $property, array $context = []): ?str public function getTypes($class, $property, array $context = []): ?array { - return [new Type(Type::BUILTIN_TYPE_INT)]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]; + } + + public function getType($class, $property, array $context = []): ?Type + { + return Type::int(); } public function getTypesFromConstructor(string $class, string $property): ?array { - return [new Type(Type::BUILTIN_TYPE_STRING)]; + return [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]; + } + + public function getTypeFromConstructor(string $class, string $property): ?Type + { + return Type::string(); } public function isReadable($class, $property, array $context = []): ?bool diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php index 3a18b0d8eaef0..1d924044531ff 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/NullExtractor.php @@ -16,6 +16,7 @@ use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\TypeInfo\Type; /** * Not able to guess anything. @@ -48,6 +49,14 @@ public function getTypes($class, $property, array $context = []): ?array return null; } + public function getType($class, $property, array $context = []): ?Type + { + $this->assertIsString($class); + $this->assertIsString($property); + + return null; + } + public function isReadable($class, $property, array $context = []): ?bool { $this->assertIsString($class); diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php index 5a5de4727e3ba..f9b1a8fc3358e 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php @@ -38,6 +38,15 @@ public function testGetLongDescription() parent::testGetLongDescription(); } + public function testGetType() + { + parent::testGetType(); + parent::testGetType(); + } + + /** + * @group legacy + */ public function testGetTypes() { parent::testGetTypes(); diff --git a/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php index e871ed49f7b2a..6fa4ed9a4b163 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/TypeTest.php @@ -16,6 +16,8 @@ /** * @author Kévin Dunglas + * + * @group legacy */ class TypeTest extends TestCase { diff --git a/src/Symfony/Component/PropertyInfo/Type.php b/src/Symfony/Component/PropertyInfo/Type.php index 1ce71301dfd20..b47e2be411484 100644 --- a/src/Symfony/Component/PropertyInfo/Type.php +++ b/src/Symfony/Component/PropertyInfo/Type.php @@ -11,11 +11,15 @@ namespace Symfony\Component\PropertyInfo; +trigger_deprecation('symfony/property-info', '7.1', 'The "%s" class is deprecated. Use "%s" from the "symfony/type-info" component instead.', Type::class, \Symfony\Component\TypeInfo\Type::class); + /** * Type value object (immutable). * * @author Kévin Dunglas * + * @deprecated since Symfony 7.1, use "Symfony\Component\TypeInfo\Type" from the "symfony/type-info" component instead + * * @final */ class Type diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index ffdf717ad2061..6d983453337af 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -22,7 +22,9 @@ use phpDocumentor\Reflection\Types\Null_; use phpDocumentor\Reflection\Types\Nullable; use phpDocumentor\Reflection\Types\String_; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeIdentifier; // Workaround for phpdocumentor/type-resolver < 1.6 // We trigger the autoloader here, so we don't need to trigger it inside the loop later. @@ -37,12 +39,16 @@ class_exists(List_::class); final class PhpDocTypeHelper { /** - * Creates a {@see Type} from a PHPDoc type. + * Creates a {@see LegacyType} from a PHPDoc type. + * + * @deprecated since Symfony 7.1, use "getType" instead * - * @return Type[] + * @return LegacyType[] */ public function getTypes(DocType $varType): array { + trigger_deprecation('symfony/property-info', '7.1', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + if ($varType instanceof ConstExpression) { // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment return []; @@ -61,7 +67,7 @@ public function getTypes(DocType $varType): array $nullable = true; } - $type = $this->createType($varType, $nullable); + $type = $this->createLegacyType($varType, $nullable); if (null !== $type) { $types[] = $type; } @@ -93,7 +99,7 @@ public function getTypes(DocType $varType): array } foreach ($varTypes as $varType) { - $type = $this->createType($varType, $nullable); + $type = $this->createLegacyType($varType, $nullable); if (null !== $type) { $types[] = $type; } @@ -105,7 +111,68 @@ public function getTypes(DocType $varType): array /** * Creates a {@see Type} from a PHPDoc type. */ - private function createType(DocType $type, bool $nullable, ?string $docType = null): ?Type + public function getType(DocType $varType): ?Type + { + if ($varType instanceof ConstExpression) { + // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment + return null; + } + + $nullable = false; + + if ($varType instanceof Nullable) { + $nullable = true; + $varType = $varType->getActualType(); + } + + if (!$varType instanceof Compound) { + if ($varType instanceof Null_) { + $nullable = true; + } + + return $this->createType($varType, $nullable); + } + + $varTypes = []; + for ($typeIndex = 0; $varType->has($typeIndex); ++$typeIndex) { + $type = $varType->get($typeIndex); + + if ($type instanceof ConstExpression) { + // It's safer to fall back to other extractors here, as resolving const types correctly is not easy at the moment + return null; + } + + // If null is present, all types are nullable + if ($type instanceof Null_) { + $nullable = true; + continue; + } + + if ($type instanceof Nullable) { + $nullable = true; + $type = $type->getActualType(); + } + + $varTypes[] = $type; + } + + $unionTypes = []; + foreach ($varTypes as $varType) { + $t = $this->createType($varType, $nullable); + if (null !== $t) { + $unionTypes[] = $t; + } + } + + $type = 1 === \count($unionTypes) ? $unionTypes[0] : Type::union(...$unionTypes); + + return $nullable ? Type::nullable($type) : $type; + } + + /** + * Creates a {@see LegacyType} from a PHPDoc type. + */ + private function createLegacyType(DocType $type, bool $nullable, ?string $docType = null): ?LegacyType { $docType ??= (string) $type; @@ -113,7 +180,7 @@ private function createType(DocType $type, bool $nullable, ?string $docType = nu $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 LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $this->getTypes($type->getValueType())); } [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); @@ -121,7 +188,7 @@ private function createType(DocType $type, bool $nullable, ?string $docType = nu $keys = $this->getTypes($type->getKeyType()); $values = $this->getTypes($type->getValueType()); - return new Type($phpType, $nullable, $class, true, $keys, $values); + return new LegacyType($phpType, $nullable, $class, true, $keys, $values); } // Cannot guess @@ -130,10 +197,10 @@ private function createType(DocType $type, bool $nullable, ?string $docType = nu } if (str_ends_with($docType, '[]') && $type instanceof Array_) { - $collectionKeyTypes = new Type(Type::BUILTIN_TYPE_INT); + $collectionKeyTypes = new LegacyType(LegacyType::BUILTIN_TYPE_INT); $collectionValueTypes = $this->getTypes($type->getValueType()); - return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); + return new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); } if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) { @@ -142,14 +209,14 @@ private function createType(DocType $type, bool $nullable, ?string $docType = nu $collectionKeyTypes = $this->getTypes($type->getKeyType()); $collectionValueTypes = $this->getTypes($type->getValueType()); - return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); + return new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); } if ($type instanceof PseudoType) { if ($type->underlyingType() instanceof Integer) { - return new Type(Type::BUILTIN_TYPE_INT, $nullable, null); + return new LegacyType(LegacyType::BUILTIN_TYPE_INT, $nullable, null); } elseif ($type->underlyingType() instanceof String_) { - return new Type(Type::BUILTIN_TYPE_STRING, $nullable, null); + return new LegacyType(LegacyType::BUILTIN_TYPE_STRING, $nullable, null); } } @@ -157,10 +224,97 @@ private function createType(DocType $type, bool $nullable, ?string $docType = nu [$phpType, $class] = $this->getPhpTypeAndClass($docType); if ('array' === $docType) { - return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, null, null); + return new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, $nullable, null, true, null, null); + } + + return new LegacyType($phpType, $nullable, $class); + } + + /** + * Creates a {@see Type} from a PHPDoc type. + */ + private function createType(DocType $docType, bool $nullable): ?Type + { + $docTypeString = (string) $docType; + + if ($docType instanceof Collection) { + $fqsen = $docType->getFqsen(); + if ($fqsen && 'list' === $fqsen->getName() && !class_exists(List_::class, false) && !class_exists((string) $fqsen)) { + // Workaround for phpdocumentor/type-resolver < 1.6 + return Type::list($this->getType($docType->getValueType())); + } + + [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); + + $variableTypes = []; + + if (null !== $valueType = $this->getType($docType->getValueType())) { + $variableTypes[] = $valueType; + } + + if (null !== $keyType = $this->getType($docType->getKeyType())) { + $variableTypes[] = $keyType; + } + + $type = null !== $class ? Type::object($class) : Type::builtin($phpType); + $type = Type::collection($type, ...$variableTypes); + + return $nullable ? Type::nullable($type) : $type; + } + + if (!$docTypeString) { + return null; } - return new Type($phpType, $nullable, $class); + if (str_ends_with($docTypeString, '[]') && $docType instanceof Array_) { + return Type::list($this->getType($docType->getValueType())); + } + + if (str_starts_with($docTypeString, 'list<') && $docType instanceof Array_) { + $collectionValueType = $this->getType($docType->getValueType()); + $type = Type::list($collectionValueType); + + return $nullable ? Type::nullable($type) : $type; + } + + if (str_starts_with($docTypeString, 'array<') && $docType instanceof Array_) { + // array is converted to x[] which is handled above + // so it's only necessary to handle array here + $collectionKeyType = $this->getType($docType->getKeyType()); + $collectionValueType = $this->getType($docType->getValueType()); + + $type = Type::array($collectionValueType, $collectionKeyType); + + return $nullable ? Type::nullable($type) : $type; + } + + if ($docType instanceof PseudoType) { + if ($docType->underlyingType() instanceof Integer) { + return $nullable ? Type::nullable(Type::int()) : Type::int(); + } elseif ($docType->underlyingType() instanceof String_) { + return $nullable ? Type::nullable(Type::string()) : Type::string(); + } + } + + $docTypeString = match ($docTypeString) { + 'integer' => 'int', + 'boolean' => 'bool', + // real is not part of the PHPDoc standard, so we ignore it + 'double' => 'float', + 'callback' => 'callable', + 'void' => 'null', + default => $docTypeString, + }; + + [$phpType, $class] = $this->getPhpTypeAndClass($docTypeString); + + if ('array' === $docTypeString) { + return $nullable ? Type::nullable(Type::array()) : Type::array(); + } + + $type = null !== $class ? Type::object($class) : Type::builtin($phpType); + + return $nullable ? Type::nullable($type) : $type; } private function normalizeType(string $docType): string @@ -178,7 +332,7 @@ private function normalizeType(string $docType): string private function getPhpTypeAndClass(string $docType): array { - if (\in_array($docType, Type::$builtinTypes, true)) { + if (\in_array($docType, TypeIdentifier::values(), true)) { return [$docType, null]; } diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json index 8032b752a108a..5c7901623e398 100644 --- a/src/Symfony/Component/PropertyInfo/composer.json +++ b/src/Symfony/Component/PropertyInfo/composer.json @@ -24,7 +24,9 @@ ], "require": { "php": ">=8.2", - "symfony/string": "^6.4|^7.0" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0", + "symfony/type-info": "^7.1" }, "require-dev": { "symfony/serializer": "^6.4|^7.0", diff --git a/src/Symfony/Component/Serializer/Exception/NotNormalizableValueException.php b/src/Symfony/Component/Serializer/Exception/NotNormalizableValueException.php index 279be41278afc..4b956f1b0454d 100644 --- a/src/Symfony/Component/Serializer/Exception/NotNormalizableValueException.php +++ b/src/Symfony/Component/Serializer/Exception/NotNormalizableValueException.php @@ -22,17 +22,17 @@ class NotNormalizableValueException extends UnexpectedValueException private bool $useMessageForUser = false; /** - * @param string[] $expectedTypes - * @param bool $useMessageForUser If the message passed to this exception is something that can be shown - * safely to your user. In other words, avoid catching other exceptions and - * passing their message directly to this class. + * @param list $expectedTypes + * @param bool $useMessageForUser If the message passed to this exception is something that can be shown + * safely to your user. In other words, avoid catching other exceptions and + * passing their message directly to this class. */ public static function createForUnexpectedDataType(string $message, mixed $data, array $expectedTypes, ?string $path = null, bool $useMessageForUser = false, int $code = 0, ?\Throwable $previous = null): self { $self = new self($message, $code, $previous); $self->currentType = get_debug_type($data); - $self->expectedTypes = $expectedTypes; + $self->expectedTypes = array_map(strval(...), $expectedTypes); $self->path = $path; $self->useMessageForUser = $useMessageForUser; diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 1c2f52fcd45c3..c89b200479edb 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -17,7 +17,7 @@ use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; @@ -32,6 +32,12 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Base class for a normalizer dealing with objects. @@ -51,7 +57,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer public const DEPTH_KEY_PATTERN = 'depth_%s::%s'; /** - * While denormalizing, we can verify that types match. + * While denormalizing, we can verify that type matches. * * You can disable this by setting this flag to true. */ @@ -109,7 +115,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer protected ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver; - private array $typesCache = []; + /** + * @var array|false> + */ + private array $typeCache = []; private array $attributesCache = []; private readonly \Closure $objectClassResolver; @@ -290,7 +299,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $this->validateCallbackContext($context); - if (null === $data && isset($context['value_type']) && $context['value_type'] instanceof Type && $context['value_type']->isNullable()) { + if (null === $data && isset($context['value_type']) && ($context['value_type'] instanceof Type || $context['value_type'] instanceof LegacyType) && $context['value_type']->isNullable()) { return null; } @@ -352,11 +361,15 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } } - $types = $this->getTypes($resolvedClass, $attribute); - - if (null !== $types) { + if (null !== $type = $this->getType($resolvedClass, $attribute)) { try { - $value = $this->validateAndDenormalize($types, $resolvedClass, $attribute, $value, $format, $attributeContext); + // BC layer for PropertyTypeExtractorInterface::getTypes(). + // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + if (\is_array($type)) { + $value = $this->validateAndDenormalizeLegacy($type, $resolvedClass, $attribute, $value, $format, $attributeContext); + } else { + $value = $this->validateAndDenormalize($type, $resolvedClass, $attribute, $value, $format, $attributeContext); + } } catch (NotNormalizableValueException $exception) { if (isset($context['not_normalizable_value_exceptions'])) { $context['not_normalizable_value_exceptions'][] = $exception; @@ -372,7 +385,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $this->setAttributeValue($object, $attribute, $value, $format, $attributeContext); } catch (PropertyAccessInvalidArgumentException $e) { $exception = NotNormalizableValueException::createForUnexpectedDataType( - sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), + sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $resolvedClass), $data, $e instanceof InvalidTypeException ? [$e->expectedType] : ['unknown'], $attributeContext['deserialization_path'] ?? null, @@ -400,14 +413,17 @@ abstract protected function setAttributeValue(object $object, string $attribute, /** * Validates the submitted data and denormalizes it. * - * @param Type[] $types + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + * + * @param LegacyType[] $types * * @throws NotNormalizableValueException * @throws ExtraAttributesException * @throws MissingConstructorArgumentsException * @throws LogicException */ - private function validateAndDenormalize(array $types, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed + private function validateAndDenormalizeLegacy(array $types, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed { $expectedTypes = []; $isUnionType = \count($types) > 1; @@ -440,11 +456,11 @@ private function validateAndDenormalize(array $types, string $currentClass, stri $builtinType = $type->getBuiltinType(); if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { if ('' === $data) { - if (Type::BUILTIN_TYPE_ARRAY === $builtinType) { + if (LegacyType::BUILTIN_TYPE_ARRAY === $builtinType) { return []; } - if (Type::BUILTIN_TYPE_STRING === $builtinType) { + if (LegacyType::BUILTIN_TYPE_STRING === $builtinType) { return ''; } @@ -453,24 +469,24 @@ private function validateAndDenormalize(array $types, string $currentClass, stri } switch ($builtinType) { - case Type::BUILTIN_TYPE_BOOL: + case LegacyType::BUILTIN_TYPE_BOOL: // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" if ('false' === $data || '0' === $data) { $data = false; } elseif ('true' === $data || '1' === $data) { $data = true; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null); } break; - case Type::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_INT: if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) { $data = (int) $data; } else { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); } break; - case Type::BUILTIN_TYPE_FLOAT: + case LegacyType::BUILTIN_TYPE_FLOAT: if (is_numeric($data)) { return (float) $data; } @@ -479,13 +495,13 @@ private function validateAndDenormalize(array $types, string $currentClass, stri 'NaN' => \NAN, 'INF' => \INF, '-INF' => -\INF, - default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null), + default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [LegacyType::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null), }; } } - if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { - $builtinType = Type::BUILTIN_TYPE_OBJECT; + if (null !== $collectionValueType && LegacyType::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { + $builtinType = LegacyType::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; if (\count($collectionKeyType = $type->getCollectionKeyTypes()) > 0) { @@ -493,13 +509,13 @@ private function validateAndDenormalize(array $types, string $currentClass, stri } $context['value_type'] = $collectionValueType; - } elseif ($type->isCollection() && \count($collectionValueType = $type->getCollectionValueTypes()) > 0 && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) { + } elseif ($type->isCollection() && \count($collectionValueType = $type->getCollectionValueTypes()) > 0 && LegacyType::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) { // get inner type for any nested array [$innerType] = $collectionValueType; // note that it will break for any other builtinType $dimensions = '[]'; - while (\count($innerType->getCollectionValueTypes()) > 0 && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) { + while (\count($innerType->getCollectionValueTypes()) > 0 && LegacyType::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) { $dimensions .= '[]'; [$innerType] = $innerType->getCollectionValueTypes(); } @@ -518,9 +534,9 @@ private function validateAndDenormalize(array $types, string $currentClass, stri $class = $type->getClassName(); } - $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; + $expectedTypes[LegacyType::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; - if (Type::BUILTIN_TYPE_OBJECT === $builtinType && null !== $class) { + if (LegacyType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $class) { if (!$this->serializer instanceof DenormalizerInterface) { throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); } @@ -537,11 +553,11 @@ private function validateAndDenormalize(array $types, string $currentClass, stri // PHP's json_decode automatically converts Numbers without a decimal part to integers. // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when // a float is expected. - if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { + if (LegacyType::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { return (float) $data; } - if ((Type::BUILTIN_TYPE_FALSE === $builtinType && false === $data) || (Type::BUILTIN_TYPE_TRUE === $builtinType && true === $data)) { + if ((LegacyType::BUILTIN_TYPE_FALSE === $builtinType && false === $data) || (LegacyType::BUILTIN_TYPE_TRUE === $builtinType && true === $data)) { return $data; } @@ -590,16 +606,221 @@ private function validateAndDenormalize(array $types, string $currentClass, stri throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); } + /** + * Validates the submitted data and denormalizes it. + * + * @throws NotNormalizableValueException + * @throws ExtraAttributesException + * @throws MissingConstructorArgumentsException + * @throws LogicException + */ + private function validateAndDenormalize(Type $type, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed + { + $expectedTypes = []; + $extraAttributesException = null; + $missingConstructorArgumentsException = null; + + $types = match (true) { + $type instanceof IntersectionType => throw new LogicException('Unable to handle intersection type.'), + $type instanceof UnionType => $type->getTypes(), + default => [$type], + }; + + foreach ($types as $t) { + if (null === $data && $type->isNullable()) { + return null; + } + + $collectionKeyType = $collectionValueType = null; + if ($t instanceof CollectionType) { + $collectionKeyType = $t->getCollectionKeyType(); + $collectionValueType = $t->getCollectionValueType(); + } + + $t = $t->getBaseType(); + + // Fix a collection that contains the only one element + // This is special to xml format only + if ('xml' === $format && $collectionValueType && !$collectionValueType->isA(TypeIdentifier::MIXED) && (!\is_array($data) || !\is_int(key($data)))) { + $data = [$data]; + } + + // This try-catch should cover all NotNormalizableValueException (and all return branches after the first + // exception) so we could try denormalizing all types of an union type. If the target type is not an union + // type, we will just re-throw the catched exception. + // In the case of no denormalization succeeds with an union type, it will fall back to the default exception + // with the acceptable types list. + try { + // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine, + // if a value is meant to be a string, float, int or a boolean value from the serialized representation. + // That's why we have to transform the values, if one of these non-string basic datatypes is expected. + $typeIdentifier = $t->getTypeIdentifier(); + if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { + switch ($typeIdentifier) { + case TypeIdentifier::ARRAY: + if ('' === $data) { + return []; + } + break; + case TypeIdentifier::BOOL: + // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" + if ('false' === $data || '0' === $data) { + $data = false; + } elseif ('true' === $data || '1' === $data) { + $data = true; + } else { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::bool()], $context['deserialization_path'] ?? null); + } + break; + case TypeIdentifier::INT: + if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) { + $data = (int) $data; + } else { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::int()], $context['deserialization_path'] ?? null); + } + break; + case TypeIdentifier::FLOAT: + if (is_numeric($data)) { + return (float) $data; + } + + return match ($data) { + 'NaN' => \NAN, + 'INF' => \INF, + '-INF' => -\INF, + default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::float()], $context['deserialization_path'] ?? null), + }; + } + } + + if ($collectionValueType) { + $collectionValueBaseType = $collectionValueType instanceof UnionType ? $collectionValueType->asNonNullable()->getBaseType() : $collectionValueType->getBaseType(); + + if ($collectionValueBaseType instanceof ObjectType) { + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $collectionValueBaseType->getClassName().'[]'; + $context['key_type'] = $collectionKeyType; + $context['value_type'] = $collectionValueType; + } elseif (TypeIdentifier::ARRAY === $collectionValueBaseType->getTypeIdentifier()) { + // get inner type for any nested array + $innerType = $collectionValueType; + + // note that it will break for any other builtinType + $dimensions = '[]'; + while ($innerType instanceof CollectionType) { + $dimensions .= '[]'; + $innerType = $innerType->getCollectionValueType(); + } + + if ($innerType instanceof ObjectType) { + // the builtinType is the inner one and the class is the class followed by []...[] + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $innerType->getClassName().$dimensions; + } else { + // default fallback (keep it as array) + if ($t instanceof ObjectType) { + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $t->getClassName(); + } else { + $typeIdentifier = $t->getTypeIdentifier()->value; + $class = null; + } + } + } + } else { + if ($t instanceof ObjectType) { + $typeIdentifier = TypeIdentifier::OBJECT; + $class = $t->getClassName(); + } else { + $typeIdentifier = $t->getTypeIdentifier(); + $class = null; + } + } + + $expectedTypes[TypeIdentifier::OBJECT === $typeIdentifier && $class ? $class : $typeIdentifier->value] = true; + + if (TypeIdentifier::OBJECT === $typeIdentifier && null !== $class) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class)); + } + + $childContext = $this->createChildContext($context, $attribute, $format); + if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) { + return $this->serializer->denormalize($data, $class, $format, $childContext); + } + } + + // JSON only has a Number type corresponding to both int and float PHP types. + // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert + // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible). + // PHP's json_decode automatically converts Numbers without a decimal part to integers. + // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when + // a float is expected. + if (TypeIdentifier::FLOAT === $typeIdentifier && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) { + return (float) $data; + } + + if ((TypeIdentifier::FALSE === $typeIdentifier && false === $data) || (TypeIdentifier::TRUE === $typeIdentifier && true === $data)) { + return $data; + } + + if (('is_'.$typeIdentifier->value)($data)) { + return $data; + } + } catch (NotNormalizableValueException|InvalidArgumentException $e) { + if (!$type instanceof UnionType) { + throw $e; + } + } catch (ExtraAttributesException $e) { + if (!$type instanceof UnionType) { + throw $e; + } + + $extraAttributesException ??= $e; + } catch (MissingConstructorArgumentsException $e) { + if (!$type instanceof UnionType) { + throw $e; + } + + $missingConstructorArgumentsException ??= $e; + } + } + + if ('' === $data && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format) && $type->isNullable()) { + return null; + } + + if ($extraAttributesException) { + throw $extraAttributesException; + } + + if ($missingConstructorArgumentsException) { + throw $missingConstructorArgumentsException; + } + + if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { + return $data; + } + + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute); + } + /** * @internal */ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, mixed $parameterData, array $context, ?string $format = null): mixed { - if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $types = $this->getTypes($class->getName(), $parameterName)) { + if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $type = $this->getType($class->getName(), $parameterName)) { return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format); } - $parameterData = $this->validateAndDenormalize($types, $class->getName(), $parameterName, $parameterData, $format, $context); + // BC layer for PropertyTypeExtractorInterface::getTypes(). + // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + if (\is_array($type)) { + $parameterData = $this->validateAndDenormalizeLegacy($type, $class->getName(), $parameterName, $parameterData, $format, $context); + } else { + $parameterData = $this->validateAndDenormalize($type, $class->getName(), $parameterName, $parameterData, $format, $context); + } $parameterData = $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context); @@ -607,42 +828,55 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara } /** - * @return Type[]|null + * @return Type|list|null */ - private function getTypes(string $currentClass, string $attribute): ?array + private function getType(string $currentClass, string $attribute): Type|array|null { if (null === $this->propertyTypeExtractor) { return null; } $key = $currentClass.'::'.$attribute; - if (isset($this->typesCache[$key])) { - return false === $this->typesCache[$key] ? null : $this->typesCache[$key]; + if (isset($this->typeCache[$key])) { + return false === $this->typeCache[$key] ? null : $this->typeCache[$key]; } - if (null !== $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) { - return $this->typesCache[$key] = $types; + if (null !== $type = $this->getPropertyType($currentClass, $attribute)) { + return $this->typeCache[$key] = $type; } if ($discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForClass($currentClass)) { if ($discriminatorMapping->getTypeProperty() === $attribute) { - return $this->typesCache[$key] = [ - new Type(Type::BUILTIN_TYPE_STRING), - ]; + return $this->typeCache[$key] = Type::string(); } foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) { - if (null !== $types = $this->propertyTypeExtractor->getTypes($mappedClass, $attribute)) { - return $this->typesCache[$key] = $types; + if (null !== $type = $this->getPropertyType($mappedClass, $attribute)) { + return $this->typeCache[$key] = $type; } } } - $this->typesCache[$key] = false; + $this->typeCache[$key] = false; return null; } + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + * + * @return Type|list|null + */ + private function getPropertyType(string $className, string $property): Type|array|null + { + if (method_exists($this->propertyTypeExtractor, 'getType')) { + return $this->propertyTypeExtractor->getType($className, $property); + } + + return $this->propertyTypeExtractor->getTypes($className, $property); + } + /** * Sets an attribute and apply the name converter if necessary. */ diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php index a9c64a1e7371e..1bd6c54b374ce 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php @@ -11,10 +11,12 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Exception\BadMethodCallException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\UnionType; /** * Denormalizes arrays of objects. @@ -46,7 +48,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!'); } if (!\is_array($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, ['array'], $context['deserialization_path'] ?? null); } if (!str_ends_with($type, '[]')) { throw new InvalidArgumentException('Unsupported class: '.$type); @@ -54,15 +56,20 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $type = substr($type, 0, -2); - $builtinTypes = array_map(static function (Type $keyType) { - return $keyType->getBuiltinType(); - }, \is_array($keyType = $context['key_type'] ?? []) ? $keyType : [$keyType]); + $typeIdentifiers = []; + if (null !== $keyType = ($context['key_type'] ?? null)) { + if ($keyType instanceof Type) { + $typeIdentifiers = array_map(fn (Type $t): string => $t->getBaseType()->getTypeIdentifier()->value, $keyType instanceof UnionType ? $keyType->getTypes() : [$keyType]); + } else { + $typeIdentifiers = array_map(fn (LegacyType $t): string => $t->getBuiltinType(), \is_array($keyType) ? $keyType : [$keyType]); + } + } foreach ($data as $key => $value) { $subContext = $context; $subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]"; - $this->validateKeyType($builtinTypes, $key, $subContext['deserialization_path']); + $this->validateKeyType($typeIdentifiers, $key, $subContext['deserialization_path']); $data[$key] = $this->denormalizer->denormalize($value, $type, $format, $subContext); } @@ -80,18 +87,21 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form && $this->denormalizer->supportsDenormalization($data, substr($type, 0, -2), $format, $context); } - private function validateKeyType(array $builtinTypes, mixed $key, string $path): void + /** + * @param list $typeIdentifiers + */ + private function validateKeyType(array $typeIdentifiers, mixed $key, string $path): void { - if (!$builtinTypes) { + if (!$typeIdentifiers) { return; } - foreach ($builtinTypes as $builtinType) { - if (('is_'.$builtinType)($key)) { + foreach ($typeIdentifiers as $typeIdentifier) { + if (('is_'.$typeIdentifier)($key)) { return; } } - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $builtinTypes), get_debug_type($key)), $key, $builtinTypes, $path, true); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $typeIdentifiers), get_debug_type($key)), $key, $typeIdentifiers, $path, true); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php index f6cefed8771f2..504047bc1e384 100644 --- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -70,7 +69,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } if (!\is_int($data) && !\is_string($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, ['int', 'string'], $context['deserialization_path'] ?? null, true); } try { diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index 46dff93f75031..71ce264960542 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -104,7 +103,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } if (!\is_string($data) || '' === trim($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType('The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType('The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', $data, ['string'], $context['deserialization_path'] ?? null, true); } try { @@ -122,7 +121,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a $dateTimeErrors = $type::getLastErrors(); - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, ['string'], $context['deserialization_path'] ?? null, true); } $defaultDateTimeFormat = $this->defaultContext[self::FORMAT_KEY] ?? null; @@ -137,7 +136,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } catch (NotNormalizableValueException $e) { throw $e; } catch (\Exception $e) { - throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, false, $e->getCode(), $e); + throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, false, $e->getCode(), $e); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php index 437e40bfc580d..f4528a03dae40 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -52,13 +51,13 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \DateTimeZone { if ('' === $data || null === $data) { - throw NotNormalizableValueException::createForUnexpectedDataType('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.', $data, ['string'], $context['deserialization_path'] ?? null, true); } try { return new \DateTimeZone($data); } catch (\Exception $e) { - throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); } } diff --git a/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php index 732f802be11fe..a6cc190a97a1b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/UidNormalizer.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Uid\AbstractUid; @@ -71,7 +70,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a try { return $type::fromString($data); } catch (\InvalidArgumentException|\TypeError) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The data is not a valid "%s" string representation.', $type), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The data is not a valid "%s" string representation.', $type), $data, ['string'], $context['deserialization_path'] ?? null, true); } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index a8e0b6eb13d1b..1f5a556cec210 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -15,7 +15,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Attribute\Context; use Symfony\Component\Serializer\Attribute\DiscriminatorMap; use Symfony\Component\Serializer\Attribute\SerializedName; @@ -56,6 +56,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithStringObject; use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectDummyWithContextAttribute; +use Symfony\Component\TypeInfo\Type; class AbstractObjectNormalizerTest extends TestCase { @@ -433,11 +434,20 @@ public function testDenormalizeCollectionDecodedFromXmlWithTwoChildren() private function getDenormalizerForDummyCollection() { $extractor = $this->createMock(PhpDocExtractor::class); - $extractor->method('getTypes') - ->will($this->onConsecutiveCalls( - [new Type('array', false, null, true, new Type('int'), new Type('object', false, DummyChild::class))], - null - )); + + if (method_exists(PhpDocExtractor::class, 'getType')) { + $extractor->method('getType') + ->will($this->onConsecutiveCalls( + Type::list(Type::object(DummyChild::class)), + null, + )); + } else { + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new LegacyType('array', false, null, true, new LegacyType('int'), new LegacyType('object', false, DummyChild::class))], + null + )); + } $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); $arrayDenormalizer = new ArrayDenormalizerDummy(); @@ -488,11 +498,20 @@ public function testDenormalizeNotSerializableObjectToPopulate() private function getDenormalizerForStringCollection() { $extractor = $this->createMock(PhpDocExtractor::class); - $extractor->method('getTypes') - ->will($this->onConsecutiveCalls( - [new Type('array', false, null, true, new Type('int'), new Type('string'))], - null - )); + + if (method_exists(PhpDocExtractor::class, 'getType')) { + $extractor->method('getType') + ->will($this->onConsecutiveCalls( + Type::list(Type::string()), + null, + )); + } else { + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new LegacyType('array', false, null, true, new LegacyType('int'), new LegacyType('string'))], + null + )); + } $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); $arrayDenormalizer = new ArrayDenormalizerDummy(); @@ -675,21 +694,40 @@ public function testDenormalizeBasicTypePropertiesFromXml() private function getDenormalizerForObjectWithBasicProperties() { $extractor = $this->createMock(PhpDocExtractor::class); - $extractor->method('getTypes') - ->will($this->onConsecutiveCalls( - [new Type('bool')], - [new Type('bool')], - [new Type('bool')], - [new Type('bool')], - [new Type('int')], - [new Type('int')], - [new Type('float')], - [new Type('float')], - [new Type('float')], - [new Type('float')], - [new Type('float')], - [new Type('float')] - )); + + if (method_exists(PhpDocExtractor::class, 'getType')) { + $extractor->method('getType') + ->will($this->onConsecutiveCalls( + Type::bool(), + Type::bool(), + Type::bool(), + Type::bool(), + Type::int(), + Type::int(), + Type::float(), + Type::float(), + Type::float(), + Type::float(), + Type::float(), + Type::float(), + )); + } else { + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new LegacyType('bool')], + [new LegacyType('bool')], + [new LegacyType('bool')], + [new LegacyType('bool')], + [new LegacyType('int')], + [new LegacyType('int')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')], + [new LegacyType('float')] + )); + } $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); $arrayDenormalizer = new ArrayDenormalizerDummy(); diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 627bfccaf3061..452dcc0a89628 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -36,6 +36,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.1", "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php index 1d22d22a9008b..bb1f1c3c8ba5c 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\TypeInfo\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum; use Symfony\Component\TypeInfo\Type; diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index f616c39ae167c..3cd7477fdbcea 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -50,7 +50,7 @@ public function resolveDataProvider(): iterable yield [Type::callable(), 'callable(string, int): mixed']; // array - yield [Type::array(Type::bool()), 'bool[]']; + yield [Type::list(Type::bool()), 'bool[]']; // array shape yield [Type::array(), 'array{0: true, 1: false}']; diff --git a/src/Symfony/Component/TypeInfo/TypeIdentifier.php b/src/Symfony/Component/TypeInfo/TypeIdentifier.php index 5326262a8562d..8776ef661ba83 100644 --- a/src/Symfony/Component/TypeInfo/TypeIdentifier.php +++ b/src/Symfony/Component/TypeInfo/TypeIdentifier.php @@ -34,4 +34,12 @@ enum TypeIdentifier: string case TRUE = 'true'; case NEVER = 'never'; case VOID = 'void'; + + /** + * @return list + */ + public static function values(): array + { + return array_column(self::cases(), 'value'); + } } diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 2ebd0e0a3042b..40b4dd231286f 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -92,7 +92,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ } if ($node instanceof ArrayTypeNode) { - return Type::array($this->getTypeFromNode($node->type, $typeContext)); + return Type::list($this->getTypeFromNode($node->type, $typeContext)); } if ($node instanceof ArrayShapeNode) { @@ -186,6 +186,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ $variableTypes = array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->genericTypes); if ($type instanceof CollectionType) { + $asList = $type->isList(); $keyType = $type->getCollectionKeyType(); $type = $type->getType(); @@ -194,9 +195,9 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ } if (1 === \count($variableTypes)) { - return Type::collection($type, $variableTypes[0], $keyType); + return new CollectionType(Type::generic($type, $keyType, $variableTypes[0]), $asList); } elseif (2 === \count($variableTypes)) { - return Type::collection($type, $variableTypes[1], $variableTypes[0]); + return Type::collection($type, $variableTypes[1], $variableTypes[0], $asList); } } diff --git a/src/Symfony/Component/TypeInfo/composer.json b/src/Symfony/Component/TypeInfo/composer.json index 2967e0d1ed79d..54b14975d9f49 100644 --- a/src/Symfony/Component/TypeInfo/composer.json +++ b/src/Symfony/Component/TypeInfo/composer.json @@ -29,8 +29,14 @@ "psr/container": "^1.1|^2.0" }, "require-dev": { - "symfony/dependency-injection": "^7.1", - "phpstan/phpdoc-parser": "^1.0" + "phpstan/phpdoc-parser": "^1.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.0", + "symfony/dependency-injection": "<6.4", + "symfony/property-info": "<6.4" }, "autoload": { "psr-4": { "Symfony\\Component\\TypeInfo\\": "" }, diff --git a/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php index e1a5d24309f28..f878974ecc811 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php @@ -15,6 +15,12 @@ use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type as PropertyInfoType; +use Symfony\Component\TypeInfo\Type as TypeInfoType; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; @@ -57,7 +63,7 @@ public function loadClassMetadata(ClassMetadata $metadata): bool continue; } - $types = $this->typeExtractor->getTypes($className, $property); + $types = $this->getPropertyTypes($className, $property); if (null === $types) { continue; } @@ -95,42 +101,92 @@ public function loadClassMetadata(ClassMetadata $metadata): bool } $loaded = true; - $builtinTypes = []; - $nullable = false; - $scalar = true; - foreach ($types as $type) { - $builtinTypes[] = $type->getBuiltinType(); - - if ($scalar && !\in_array($type->getBuiltinType(), [PropertyInfoType::BUILTIN_TYPE_INT, PropertyInfoType::BUILTIN_TYPE_FLOAT, PropertyInfoType::BUILTIN_TYPE_STRING, PropertyInfoType::BUILTIN_TYPE_BOOL], true)) { - $scalar = false; + + // BC layer for PropertyTypeExtractorInterface::getTypes(). + // Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + if (\is_array($types)) { + $builtinTypes = []; + $nullable = false; + $scalar = true; + + foreach ($types as $type) { + $builtinTypes[] = $type->getBuiltinType(); + + if ($scalar && !\in_array($type->getBuiltinType(), ['int', 'float', 'string', 'bool'], true)) { + $scalar = false; + } + + if (!$nullable && $type->isNullable()) { + $nullable = true; + } + } + + if (!$hasTypeConstraint) { + if (1 === \count($builtinTypes)) { + if ($types[0]->isCollection() && \count($collectionValueType = $types[0]->getCollectionValueTypes()) > 0) { + [$collectionValueType] = $collectionValueType; + $this->handleAllConstraintLegacy($property, $allConstraint, $collectionValueType, $metadata); + } + + $metadata->addPropertyConstraint($property, $this->getTypeConstraintLegacy($builtinTypes[0], $types[0])); + } elseif ($scalar) { + $metadata->addPropertyConstraint($property, new Type(['type' => 'scalar'])); + } + } + + if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) { + $metadata->addPropertyConstraint($property, new NotNull()); } + } else { + if ($hasTypeConstraint) { + continue; + } + + $type = $types; + $nullable = false; - if (!$nullable && $type->isNullable()) { + if ($type instanceof UnionType && $type->isNullable()) { $nullable = true; + $type = $type->asNonNullable(); } - } - if (!$hasTypeConstraint) { - if (1 === \count($builtinTypes)) { - if ($types[0]->isCollection() && \count($collectionValueType = $types[0]->getCollectionValueTypes()) > 0) { - [$collectionValueType] = $collectionValueType; - $this->handleAllConstraint($property, $allConstraint, $collectionValueType, $metadata); - } - $metadata->addPropertyConstraint($property, $this->getTypeConstraint($builtinTypes[0], $types[0])); - } elseif ($scalar) { - $metadata->addPropertyConstraint($property, new Type(['type' => 'scalar'])); + if ($type instanceof CollectionType) { + $this->handleAllConstraint($property, $allConstraint, $type->getCollectionValueType(), $metadata); + } + + if (null !== $typeConstraint = $this->getTypeConstraint($type)) { + $metadata->addPropertyConstraint($property, $typeConstraint); } - } - if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) { - $metadata->addPropertyConstraint($property, new NotNull()); + if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) { + $metadata->addPropertyConstraint($property, new NotNull()); + } } } return $loaded; } - private function getTypeConstraint(string $builtinType, PropertyInfoType $type): Type + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + * + * @return TypeInfoType|list|null + */ + private function getPropertyTypes(string $className, string $property): TypeInfoType|array|null + { + if (method_exists($this->typeExtractor, 'getType')) { + return $this->typeExtractor->getType($className, $property); + } + + return $this->typeExtractor->getTypes($className, $property); + } + + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + */ + private function getTypeConstraintLegacy(string $builtinType, PropertyInfoType $type): Type { if (PropertyInfoType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $className = $type->getClassName()) { return new Type(['type' => $className]); @@ -139,7 +195,64 @@ private function getTypeConstraint(string $builtinType, PropertyInfoType $type): return new Type(['type' => $builtinType]); } - private function handleAllConstraint(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata): void + private function getTypeConstraint(TypeInfoType $type): ?Type + { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return ($type->isA(TypeIdentifier::INT) || $type->isA(TypeIdentifier::FLOAT) || $type->isA(TypeIdentifier::STRING) || $type->isA(TypeIdentifier::BOOL)) ? new Type(['type' => 'scalar']) : null; + } + + $baseType = $type->getBaseType(); + + if ($baseType instanceof ObjectType) { + return new Type(['type' => $baseType->getClassName()]); + } + + if (TypeIdentifier::MIXED !== $baseType->getTypeIdentifier()) { + return new Type(['type' => $baseType->getTypeIdentifier()->value]); + } + + return null; + } + + private function handleAllConstraint(string $property, ?All $allConstraint, TypeInfoType $type, ClassMetadata $metadata): void + { + $containsTypeConstraint = false; + $containsNotNullConstraint = false; + if (null !== $allConstraint) { + foreach ($allConstraint->constraints as $constraint) { + if ($constraint instanceof Type) { + $containsTypeConstraint = true; + } elseif ($constraint instanceof NotNull) { + $containsNotNullConstraint = true; + } + } + } + + $constraints = []; + if (!$containsNotNullConstraint && !$type->isNullable()) { + $constraints[] = new NotNull(); + } + + if (!$containsTypeConstraint && null !== $typeConstraint = $this->getTypeConstraint($type)) { + $constraints[] = $typeConstraint; + } + + if (!$constraints) { + return; + } + + if (null === $allConstraint) { + $metadata->addPropertyConstraint($property, new All(['constraints' => $constraints])); + } else { + $allConstraint->constraints = array_merge($allConstraint->constraints, $constraints); + } + } + + /** + * BC layer for PropertyTypeExtractorInterface::getTypes(). + * Can be removed as soon as PropertyTypeExtractorInterface::getTypes() is removed (8.0). + */ + private function handleAllConstraintLegacy(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata): void { $containsTypeConstraint = false; $containsNotNullConstraint = false; @@ -159,7 +272,7 @@ private function handleAllConstraint(string $property, ?All $allConstraint, Prop } if (!$containsTypeConstraint) { - $constraints[] = $this->getTypeConstraint($propertyInfoType->getBuiltinType(), $propertyInfoType); + $constraints[] = $this->getTypeConstraintLegacy($propertyInfoType->getBuiltinType(), $propertyInfoType); } if (null === $allConstraint) { diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php index d07994c15fa26..c4dabc89320c5 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Validator\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; -use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\Iban; use Symfony\Component\Validator\Constraints\NotBlank; @@ -35,8 +38,8 @@ class PropertyInfoLoaderTest extends TestCase { public function testLoadClassMetadata() { - $propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class); - $propertyInfoStub + $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor ->method('getProperties') ->willReturn([ 'nullableString', @@ -54,24 +57,62 @@ public function testLoadClassMetadata() 'noAutoMapping', ]) ; - $propertyInfoStub - ->method('getTypes') - ->will($this->onConsecutiveCalls( - [new Type(Type::BUILTIN_TYPE_STRING, true)], - [new Type(Type::BUILTIN_TYPE_STRING)], - [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_BOOL)], - [new Type(Type::BUILTIN_TYPE_OBJECT, true, Entity::class)], - [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, Entity::class))], - [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], - [new Type(Type::BUILTIN_TYPE_FLOAT, true)], // The existing constraint is float - [new Type(Type::BUILTIN_TYPE_STRING, true)], - [new Type(Type::BUILTIN_TYPE_STRING, true)], - [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_FLOAT))], - [new Type(Type::BUILTIN_TYPE_STRING)], - [new Type(Type::BUILTIN_TYPE_STRING)] - )) - ; - $propertyInfoStub + + $propertyTypeExtractor = new class() implements PropertyTypeExtractorInterface { + private int $i = 0; + private int $j = 0; + private array $types; + private array $legacyTypes; + + public function getType(string $class, string $property, array $context = []): ?Type + { + $this->types ??= [ + Type::nullable(Type::string()), + Type::string(), + Type::union(Type::string(), Type::int(), Type::bool(), Type::null()), + Type::nullable(Type::object(Entity::class)), + Type::nullable(Type::array(Type::object(Entity::class))), + Type::nullable(Type::array()), + Type::nullable(Type::float()), // The existing constraint is float + Type::nullable(Type::string()), + Type::nullable(Type::string()), + Type::nullable(Type::array(Type::float())), + Type::string(), + Type::string(), + ]; + + $type = $this->types[$this->i]; + ++$this->i; + + return $type; + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + $this->legacyTypes ??= [ + [new LegacyType('string', true)], + [new LegacyType('string')], + [new LegacyType('string', true), new LegacyType('int'), new LegacyType('bool')], + [new LegacyType('object', true, Entity::class)], + [new LegacyType('array', true, null, true, null, new LegacyType('object', false, Entity::class))], + [new LegacyType('array', true, null, true)], + [new LegacyType('float', true)], // The existing constraint is float + [new LegacyType('string', true)], + [new LegacyType('string', true)], + [new LegacyType('array', true, null, true, null, new LegacyType('float'))], + [new LegacyType('string')], + [new LegacyType('string')], + ]; + + $legacyType = $this->legacyTypes[$this->j]; + ++$this->j; + + return $legacyType; + } + }; + + $propertyAccessExtractor = $this->createMock(PropertyAccessExtractorInterface::class); + $propertyAccessExtractor ->method('isWritable') ->will($this->onConsecutiveCalls( true, @@ -89,7 +130,7 @@ public function testLoadClassMetadata() )) ; - $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub, '{.*}'); + $propertyInfoLoader = new PropertyInfoLoader($propertyListExtractor, $propertyTypeExtractor, $propertyAccessExtractor, '{.*}'); $validator = Validation::createValidatorBuilder() ->enableAttributeMapping() @@ -170,7 +211,6 @@ public function testLoadClassMetadata() $this->assertInstanceOf(TypeConstraint::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]); $this->assertSame('string', $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]->type); $this->assertInstanceOf(Iban::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[1]); - $this->assertInstanceOf(NotNull::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[2]); $readOnlyMetadata = $classMetadata->getPropertyMetadata('readOnly'); $this->assertEmpty($readOnlyMetadata); @@ -188,17 +228,27 @@ public function testLoadClassMetadata() */ public function testClassValidator(bool $expected, ?string $classValidatorRegexp = null) { - $propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class); - $propertyInfoStub + $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor ->method('getProperties') ->willReturn(['string']) ; - $propertyInfoStub - ->method('getTypes') - ->willReturn([new Type(Type::BUILTIN_TYPE_STRING)]) - ; - $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub, $classValidatorRegexp); + $propertyTypeExtractor = new class() implements PropertyTypeExtractorInterface { + public function getType(string $class, string $property, array $context = []): ?Type + { + return Type::string(); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return [new LegacyType('string')]; + } + }; + + $propertyAccessExtractor = $this->createMock(PropertyAccessExtractorInterface::class); + + $propertyInfoLoader = new PropertyInfoLoader($propertyListExtractor, $propertyTypeExtractor, $propertyAccessExtractor, $classValidatorRegexp); $classMetadata = new ClassMetadata(PropertyInfoLoaderEntity::class); $this->assertSame($expected, $propertyInfoLoader->loadClassMetadata($classMetadata)); @@ -214,21 +264,31 @@ public static function regexpProvider(): array ]; } - public function testClassNoAutoMapping() + public function testClassNoAutoMapping(?PropertyTypeExtractorInterface $propertyListExtractor = null) { - $propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class); - $propertyInfoStub - ->method('getProperties') - ->willReturn(['string', 'autoMappingExplicitlyEnabled']) - ; - $propertyInfoStub - ->method('getTypes') - ->willReturnOnConsecutiveCalls( - [new Type(Type::BUILTIN_TYPE_STRING)], - [new Type(Type::BUILTIN_TYPE_BOOL)] - ); - - $propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $propertyInfoStub, '{.*}'); + if (null === $propertyListExtractor) { + $propertyListExtractor = $this->createMock(PropertyListExtractorInterface::class); + $propertyListExtractor + ->method('getProperties') + ->willReturn(['string', 'autoMappingExplicitlyEnabled']) + ; + + $propertyTypeExtractor = new class() implements PropertyTypeExtractorInterface { + public function getType(string $class, string $property, array $context = []): ?Type + { + return Type::string(); + } + + public function getTypes(string $class, string $property, array $context = []): ?array + { + return [new LegacyType('string')]; + } + }; + } + + $propertyAccessExtractor = $this->createMock(PropertyAccessExtractorInterface::class); + + $propertyInfoLoader = new PropertyInfoLoader($propertyListExtractor, $propertyTypeExtractor, $propertyAccessExtractor, '{.*}'); $validator = Validation::createValidatorBuilder() ->enableAttributeMapping() ->addLoader($propertyInfoLoader) diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 4f55dd2d1f827..31d88f870bcc2 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -38,6 +38,7 @@ "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", "symfony/translation": "^6.4.3|^7.0.3", + "symfony/type-info": "^7.1", "egulias/email-validator": "^2.1.10|^3|^4" }, "conflict": {