├── CHANGELOG.md ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── LogicException.php ├── RuntimeException.php └── UnsupportedException.php ├── LICENSE ├── README.md ├── Type.php ├── Type ├── ArrayShapeType.php ├── BackedEnumType.php ├── BuiltinType.php ├── CollectionType.php ├── CompositeTypeInterface.php ├── EnumType.php ├── GenericType.php ├── IntersectionType.php ├── NullableType.php ├── ObjectType.php ├── TemplateType.php ├── UnionType.php └── WrappingTypeInterface.php ├── TypeContext ├── TypeContext.php └── TypeContextFactory.php ├── TypeFactoryTrait.php ├── TypeIdentifier.php ├── TypeResolver ├── PhpDocAwareReflectionTypeResolver.php ├── ReflectionParameterTypeResolver.php ├── ReflectionPropertyTypeResolver.php ├── ReflectionReturnTypeResolver.php ├── ReflectionTypeResolver.php ├── StringTypeResolver.php ├── TypeResolver.php └── TypeResolverInterface.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.3 5 | --- 6 | 7 | * Add `Type::accepts()` method 8 | * Add the `TypeFactoryTrait::fromValue()`, `TypeFactoryTrait::arrayShape()`, and `TypeFactoryTrait::arrayKey()` methods 9 | * Deprecate constructing a `CollectionType` instance as a list that is not an array 10 | * Deprecate the third `$asList` argument of `TypeFactoryTrait::iterable()`, use `TypeFactoryTrait::list()` instead 11 | * Add type alias support in `TypeContext` and `StringTypeResolver` 12 | * Add `CollectionType::mergeCollectionValueTypes()` method 13 | * Add `ArrayShapeType` to represent the exact shape of an array 14 | * Add `Type::traverse()` method 15 | 16 | 7.2 17 | --- 18 | 19 | * Add construction validation for `BackedEnumType`, `CollectionType`, `GenericType`, `IntersectionType`, and `UnionType` 20 | * Add `TypeIdentifier::isStandalone()`, `TypeIdentifier::isScalar()`, and `TypeIdentifier::isBool()` methods 21 | * Add `WrappingTypeInterface` and `CompositeTypeInterface` type interfaces 22 | * Add `NullableType` type class 23 | * Rename `Type::isA()` to `Type::isIdentifiedBy()` and `Type::is()` to `Type::isSatisfiedBy()` 24 | * Remove `Type::__call()` 25 | * Remove `Type::getBaseType()`, use `WrappingTypeInterface::getWrappedType()` instead 26 | * Remove `Type::asNonNullable()`, use `NullableType::getWrappedType()` instead 27 | * Remove `CompositeTypeTrait` 28 | * Add `PhpDocAwareReflectionTypeResolver` resolver 29 | * The type resolvers are not marked as `@internal` anymore 30 | * The component is not marked as `@experimental` anymore 31 | 32 | 7.1 33 | --- 34 | 35 | * Add the component as experimental 36 | -------------------------------------------------------------------------------- /Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Exception; 13 | 14 | /** 15 | * @author Mathias Arlaud 16 | * @author Baptiste Leduc 17 | */ 18 | interface ExceptionInterface extends \Throwable 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Exception; 13 | 14 | /** 15 | * @author Mathias Arlaud 16 | * @author Baptiste Leduc 17 | */ 18 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Exception; 13 | 14 | /** 15 | * @author Mathias Arlaud 16 | * @author Baptiste Leduc 17 | */ 18 | class LogicException extends \LogicException implements ExceptionInterface 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Exception; 13 | 14 | /** 15 | * @author Mathias Arlaud 16 | * @author Baptiste Leduc 17 | */ 18 | class RuntimeException extends \RuntimeException implements ExceptionInterface 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /Exception/UnsupportedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Exception; 13 | 14 | /** 15 | * @author Mathias Arlaud 16 | * @author Baptiste Leduc 17 | */ 18 | class UnsupportedException extends \LogicException implements ExceptionInterface 19 | { 20 | public function __construct( 21 | string $message, 22 | public readonly mixed $subject, 23 | int $code = 0, 24 | ?\Throwable $previous = null, 25 | ) { 26 | parent::__construct($message, $code, $previous); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TypeInfo Component 2 | ================== 3 | 4 | The TypeInfo component extracts PHP types information. 5 | 6 | Getting Started 7 | --------------- 8 | 9 | ```bash 10 | composer require symfony/type-info 11 | composer require phpstan/phpdoc-parser # to support raw string resolving 12 | ``` 13 | 14 | ```php 15 | resolve(new \ReflectionProperty(Dummy::class, 'id')); // returns an "int" Type instance 26 | $typeResolver->resolve('bool'); // returns a "bool" Type instance 27 | 28 | // Types can be instantiated thanks to static factories 29 | $type = Type::list(Type::nullable(Type::bool())); 30 | 31 | // Type classes have their specific methods 32 | Type::object(FooClass::class)->getClassName(); 33 | Type::enum(FooEnum::class, Type::int())->getBackingType(); 34 | Type::list(Type::int())->isList(); 35 | 36 | // Every type can be cast to string 37 | (string) Type::generic(Type::object(Collection::class), Type::int()) // returns "Collection" 38 | 39 | // You can check that a type (or one of its wrapped/composed parts) is identified by one of some identifiers. 40 | $type->isIdentifiedBy(Foo::class, Bar::class); 41 | $type->isIdentifiedBy(TypeIdentifier::OBJECT); 42 | $type->isIdentifiedBy('float'); 43 | 44 | // You can also check that a type satifies specific conditions 45 | $type->isSatisfiedBy(fn (Type $type): bool => !$type->isNullable() && $type->isIdentifiedBy(TypeIdentifier::INT)); 46 | ``` 47 | 48 | Resources 49 | --------- 50 | * [Documentation](https://symfony.com/doc/current/components/type_info.html) 51 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 52 | * [Report issues](https://github.com/symfony/symfony/issues) and 53 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 54 | in the [main Symfony repository](https://github.com/symfony/symfony) 55 | -------------------------------------------------------------------------------- /Type.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo; 13 | 14 | use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; 15 | use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; 16 | 17 | /** 18 | * @author Mathias Arlaud 19 | * @author Baptiste Leduc 20 | */ 21 | abstract class Type implements \Stringable 22 | { 23 | use TypeFactoryTrait; 24 | 25 | /** 26 | * Tells if the type is satisfied by the $specification callable. 27 | * 28 | * @param callable(self): bool $specification 29 | */ 30 | public function isSatisfiedBy(callable $specification): bool 31 | { 32 | if ($this instanceof WrappingTypeInterface && $this->wrappedTypeIsSatisfiedBy($specification)) { 33 | return true; 34 | } 35 | 36 | if ($this instanceof CompositeTypeInterface && $this->composedTypesAreSatisfiedBy($specification)) { 37 | return true; 38 | } 39 | 40 | return $specification($this); 41 | } 42 | 43 | /** 44 | * Tells if the type (or one of its wrapped/composed parts) is identified by one of the $identifiers. 45 | */ 46 | public function isIdentifiedBy(TypeIdentifier|string ...$identifiers): bool 47 | { 48 | $specification = static fn (Type $type): bool => $type->isIdentifiedBy(...$identifiers); 49 | 50 | if ($this instanceof WrappingTypeInterface && $this->wrappedTypeIsSatisfiedBy($specification)) { 51 | return true; 52 | } 53 | 54 | if ($this instanceof CompositeTypeInterface && $this->composedTypesAreSatisfiedBy($specification)) { 55 | return true; 56 | } 57 | 58 | return false; 59 | } 60 | 61 | public function isNullable(): bool 62 | { 63 | return false; 64 | } 65 | 66 | /** 67 | * Tells if the type (or one of its wrapped/composed parts) accepts the given $value. 68 | */ 69 | public function accepts(mixed $value): bool 70 | { 71 | $specification = static function (Type $type) use (&$specification, $value): bool { 72 | if ($type instanceof WrappingTypeInterface) { 73 | return $type->wrappedTypeIsSatisfiedBy($specification); 74 | } 75 | 76 | if ($type instanceof CompositeTypeInterface) { 77 | return $type->composedTypesAreSatisfiedBy($specification); 78 | } 79 | 80 | return $type->accepts($value); 81 | }; 82 | 83 | return $this->isSatisfiedBy($specification); 84 | } 85 | 86 | /** 87 | * Traverses the whole type tree. 88 | * 89 | * @return iterable 90 | */ 91 | public function traverse(bool $traverseComposite = true, bool $traverseWrapped = true): iterable 92 | { 93 | yield $this; 94 | 95 | if ($this instanceof CompositeTypeInterface && $traverseComposite) { 96 | foreach ($this->getTypes() as $type) { 97 | yield $type; 98 | } 99 | 100 | // prevent yielding twice when having a type that is both composite and wrapped 101 | return; 102 | } 103 | 104 | if ($this instanceof WrappingTypeInterface && $traverseWrapped) { 105 | yield $this->getWrappedType(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Type/ArrayShapeType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeIdentifier; 17 | 18 | /** 19 | * Represents the exact shape of an array. 20 | * 21 | * @author Mathias Arlaud 22 | * 23 | * @extends CollectionType>> 24 | */ 25 | final class ArrayShapeType extends CollectionType 26 | { 27 | /** 28 | * @var array 29 | */ 30 | private readonly array $shape; 31 | 32 | /** 33 | * @param array $shape 34 | */ 35 | public function __construct( 36 | array $shape, 37 | private readonly ?Type $extraKeyType = null, 38 | private readonly ?Type $extraValueType = null, 39 | ) { 40 | $keyTypes = []; 41 | $valueTypes = []; 42 | 43 | foreach ($shape as $k => $v) { 44 | $keyTypes[] = self::fromValue($k); 45 | $valueTypes[] = $v['type']; 46 | } 47 | 48 | if ($keyTypes) { 49 | $keyTypes = array_values(array_unique($keyTypes)); 50 | $keyType = \count($keyTypes) > 1 ? self::union(...$keyTypes) : $keyTypes[0]; 51 | } else { 52 | $keyType = Type::arrayKey(); 53 | } 54 | 55 | $valueType = $valueTypes ? CollectionType::mergeCollectionValueTypes($valueTypes) : Type::mixed(); 56 | 57 | parent::__construct(self::generic(self::builtin(TypeIdentifier::ARRAY), $keyType, $valueType)); 58 | 59 | $sortedShape = $shape; 60 | ksort($sortedShape); 61 | 62 | $this->shape = $sortedShape; 63 | 64 | if ($this->extraKeyType xor $this->extraValueType) { 65 | throw new InvalidArgumentException(\sprintf('You must provide a value for "$%s" when "$%s" is provided.', $this->extraKeyType ? 'extraValueType' : 'extraKeyType', $this->extraKeyType ? 'extraKeyType' : 'extraValueType')); 66 | } 67 | 68 | if ($extraKeyType && !$extraKeyType->isIdentifiedBy(TypeIdentifier::INT, TypeIdentifier::STRING)) { 69 | throw new InvalidArgumentException(\sprintf('"%s" is not a valid array key type.', (string) $extraKeyType)); 70 | } 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function getShape(): array 77 | { 78 | return $this->shape; 79 | } 80 | 81 | public function isSealed(): bool 82 | { 83 | return null === $this->extraValueType; 84 | } 85 | 86 | public function getExtraKeyType(): ?Type 87 | { 88 | return $this->extraKeyType; 89 | } 90 | 91 | public function getExtraValueType(): ?Type 92 | { 93 | return $this->extraKeyType; 94 | } 95 | 96 | public function accepts(mixed $value): bool 97 | { 98 | if (!\is_array($value)) { 99 | return false; 100 | } 101 | 102 | foreach ($this->shape as $key => $shapeValue) { 103 | if (!($shapeValue['optional'] ?? false) && !\array_key_exists($key, $value)) { 104 | return false; 105 | } 106 | } 107 | 108 | foreach ($value as $key => $itemValue) { 109 | $valueType = $this->shape[$key]['type'] ?? false; 110 | 111 | if ($valueType && !$valueType->accepts($itemValue)) { 112 | return false; 113 | } 114 | 115 | if (!$valueType && ($this->isSealed() || !$this->extraKeyType->accepts($key) || !$this->extraValueType->accepts($itemValue))) { 116 | return false; 117 | } 118 | } 119 | 120 | return true; 121 | } 122 | 123 | public function __toString(): string 124 | { 125 | $items = []; 126 | 127 | foreach ($this->shape as $key => $value) { 128 | $itemKey = \is_int($key) ? (string) $key : \sprintf("'%s'", $key); 129 | if ($value['optional'] ?? false) { 130 | $itemKey = \sprintf('%s?', $itemKey); 131 | } 132 | 133 | $items[] = \sprintf('%s: %s', $itemKey, $value['type']); 134 | } 135 | 136 | if (!$this->isSealed()) { 137 | $items[] = $this->extraKeyType->isIdentifiedBy(TypeIdentifier::INT) && $this->extraKeyType->isIdentifiedBy(TypeIdentifier::STRING) && $this->extraValueType->isIdentifiedBy(TypeIdentifier::MIXED) 138 | ? '...' 139 | : \sprintf('...<%s, %s>', $this->extraKeyType, $this->extraValueType); 140 | } 141 | 142 | return \sprintf('array{%s}', implode(', ', $items)); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Type/BackedEnumType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\TypeIdentifier; 16 | 17 | /** 18 | * @author Mathias Arlaud 19 | * @author Baptiste Leduc 20 | * 21 | * @template T of class-string<\BackedEnum> 22 | * @template U of BuiltinType|BuiltinType 23 | * 24 | * @extends EnumType 25 | */ 26 | final class BackedEnumType extends EnumType 27 | { 28 | /** 29 | * @param T $className 30 | * @param U $backingType 31 | */ 32 | public function __construct( 33 | string $className, 34 | private readonly BuiltinType $backingType, 35 | ) { 36 | if (TypeIdentifier::INT !== $backingType->getTypeIdentifier() && TypeIdentifier::STRING !== $backingType->getTypeIdentifier()) { 37 | throw new InvalidArgumentException(\sprintf('Cannot create "%s" with "%s" backing type.', self::class, $backingType)); 38 | } 39 | 40 | parent::__construct($className); 41 | } 42 | 43 | /** 44 | * @return U 45 | */ 46 | public function getBackingType(): BuiltinType 47 | { 48 | return $this->backingType; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Type/BuiltinType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Type; 15 | use Symfony\Component\TypeInfo\TypeIdentifier; 16 | 17 | /** 18 | * @author Mathias Arlaud 19 | * @author Baptiste Leduc 20 | * 21 | * @template T of TypeIdentifier 22 | */ 23 | final class BuiltinType extends Type 24 | { 25 | /** 26 | * @param T $typeIdentifier 27 | */ 28 | public function __construct( 29 | private readonly TypeIdentifier $typeIdentifier, 30 | ) { 31 | } 32 | 33 | /** 34 | * @return T 35 | */ 36 | public function getTypeIdentifier(): TypeIdentifier 37 | { 38 | return $this->typeIdentifier; 39 | } 40 | 41 | public function isIdentifiedBy(TypeIdentifier|string ...$identifiers): bool 42 | { 43 | foreach ($identifiers as $identifier) { 44 | if (\is_string($identifier)) { 45 | try { 46 | $identifier = TypeIdentifier::from($identifier); 47 | } catch (\ValueError) { 48 | continue; 49 | } 50 | } 51 | 52 | if ($identifier === $this->typeIdentifier) { 53 | return true; 54 | } 55 | } 56 | 57 | return false; 58 | } 59 | 60 | public function isNullable(): bool 61 | { 62 | return \in_array($this->typeIdentifier, [TypeIdentifier::NULL, TypeIdentifier::MIXED]); 63 | } 64 | 65 | public function accepts(mixed $value): bool 66 | { 67 | return match ($this->typeIdentifier) { 68 | TypeIdentifier::ARRAY => \is_array($value), 69 | TypeIdentifier::BOOL => \is_bool($value), 70 | TypeIdentifier::CALLABLE => \is_callable($value), 71 | TypeIdentifier::FALSE => false === $value, 72 | TypeIdentifier::FLOAT => \is_float($value), 73 | TypeIdentifier::INT => \is_int($value), 74 | TypeIdentifier::ITERABLE => is_iterable($value), 75 | TypeIdentifier::MIXED => true, 76 | TypeIdentifier::NULL => null === $value, 77 | TypeIdentifier::OBJECT => \is_object($value), 78 | TypeIdentifier::RESOURCE => \is_resource($value), 79 | TypeIdentifier::STRING => \is_string($value), 80 | TypeIdentifier::TRUE => true === $value, 81 | default => false, 82 | }; 83 | } 84 | 85 | public function __toString(): string 86 | { 87 | return $this->typeIdentifier->value; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Type/CollectionType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeIdentifier; 17 | 18 | /** 19 | * Represents a key/value collection type. 20 | * 21 | * @author Mathias Arlaud 22 | * @author Baptiste Leduc 23 | * 24 | * @template T of BuiltinType|BuiltinType|ObjectType|GenericType 25 | * 26 | * @implements WrappingTypeInterface 27 | */ 28 | class CollectionType extends Type implements WrappingTypeInterface 29 | { 30 | /** 31 | * @param T $type 32 | */ 33 | public function __construct( 34 | private readonly BuiltinType|ObjectType|GenericType $type, 35 | private readonly bool $isList = false, 36 | ) { 37 | if ($type instanceof BuiltinType && TypeIdentifier::ARRAY !== $type->getTypeIdentifier() && TypeIdentifier::ITERABLE !== $type->getTypeIdentifier()) { 38 | throw new InvalidArgumentException(\sprintf('Cannot create "%s" with "%s" type.', self::class, $type)); 39 | } 40 | 41 | if ($this->isList()) { 42 | if (!$type->isIdentifiedBy(TypeIdentifier::ARRAY)) { 43 | trigger_deprecation('symfony/type-info', '7.3', 'Creating a "%s" that is a list and not an array is deprecated and will throw a "%s" in 8.0.', self::class, InvalidArgumentException::class); 44 | // throw new InvalidArgumentException(\sprintf('Cannot create a "%s" as list when type is not "array".', self::class)); 45 | } 46 | 47 | $keyType = $this->getCollectionKeyType(); 48 | 49 | if (!$keyType instanceof BuiltinType || TypeIdentifier::INT !== $keyType->getTypeIdentifier()) { 50 | throw new InvalidArgumentException(\sprintf('"%s" is not a valid list key type.', (string) $keyType)); 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * @param array $types 57 | */ 58 | public static function mergeCollectionValueTypes(array $types): Type 59 | { 60 | if (!$types) { 61 | throw new InvalidArgumentException('The $types cannot be empty.'); 62 | } 63 | 64 | $normalizedTypes = []; 65 | $boolTypes = []; 66 | $objectTypes = []; 67 | 68 | foreach ($types as $type) { 69 | foreach (($type instanceof UnionType ? $type->getTypes() : [$type]) as $t) { 70 | // cannot create an union with a standalone type 71 | if ($t->isIdentifiedBy(TypeIdentifier::MIXED)) { 72 | return Type::mixed(); 73 | } 74 | 75 | if ($t->isIdentifiedBy(TypeIdentifier::TRUE, TypeIdentifier::FALSE, TypeIdentifier::BOOL)) { 76 | $boolTypes[] = $t; 77 | 78 | continue; 79 | } 80 | 81 | if ($t->isIdentifiedBy(TypeIdentifier::OBJECT)) { 82 | $objectTypes[] = $t; 83 | 84 | continue; 85 | } 86 | 87 | $normalizedTypes[] = $t; 88 | } 89 | } 90 | 91 | $boolTypes = array_unique($boolTypes); 92 | $objectTypes = array_unique($objectTypes); 93 | 94 | // cannot create an union with either "true" and "false", "bool" must be used instead 95 | if ($boolTypes) { 96 | $normalizedTypes[] = \count($boolTypes) > 1 ? Type::bool() : $boolTypes[0]; 97 | } 98 | 99 | // cannot create a union with either "object" and a class name, "object" must be used instead 100 | if ($objectTypes) { 101 | $hasBuiltinObjectType = array_filter($objectTypes, static fn (Type $t): bool => $t->isSatisfiedBy(static fn (Type $t): bool => $t instanceof BuiltinType)); 102 | $normalizedTypes = [...$normalizedTypes, ...($hasBuiltinObjectType ? [Type::object()] : $objectTypes)]; 103 | } 104 | 105 | $normalizedTypes = array_values(array_unique($normalizedTypes)); 106 | 107 | return \count($normalizedTypes) > 1 ? self::union(...$normalizedTypes) : $normalizedTypes[0]; 108 | } 109 | 110 | public function getWrappedType(): Type 111 | { 112 | return $this->type; 113 | } 114 | 115 | public function isList(): bool 116 | { 117 | return $this->isList; 118 | } 119 | 120 | public function getCollectionKeyType(): Type 121 | { 122 | $defaultCollectionKeyType = self::arrayKey(); 123 | 124 | if ($this->type instanceof GenericType) { 125 | return match (\count($this->type->getVariableTypes())) { 126 | 2 => $this->type->getVariableTypes()[0], 127 | 1 => self::int(), 128 | default => $defaultCollectionKeyType, 129 | }; 130 | } 131 | 132 | return $defaultCollectionKeyType; 133 | } 134 | 135 | public function getCollectionValueType(): Type 136 | { 137 | $defaultCollectionValueType = self::mixed(); 138 | 139 | if ($this->type instanceof GenericType) { 140 | return match (\count($this->type->getVariableTypes())) { 141 | 2 => $this->type->getVariableTypes()[1], 142 | 1 => $this->type->getVariableTypes()[0], 143 | default => $defaultCollectionValueType, 144 | }; 145 | } 146 | 147 | return $defaultCollectionValueType; 148 | } 149 | 150 | public function wrappedTypeIsSatisfiedBy(callable $specification): bool 151 | { 152 | return $this->getWrappedType()->isSatisfiedBy($specification); 153 | } 154 | 155 | public function accepts(mixed $value): bool 156 | { 157 | if (!parent::accepts($value)) { 158 | return false; 159 | } 160 | 161 | if ($this->isList() && (!\is_array($value) || !array_is_list($value))) { 162 | return false; 163 | } 164 | 165 | $keyType = $this->getCollectionKeyType(); 166 | $valueType = $this->getCollectionValueType(); 167 | 168 | if (is_iterable($value)) { 169 | foreach ($value as $k => $v) { 170 | // key or value do not match 171 | if (!$keyType->accepts($k) || !$valueType->accepts($v)) { 172 | return false; 173 | } 174 | } 175 | } 176 | 177 | return true; 178 | } 179 | 180 | public function __toString(): string 181 | { 182 | return (string) $this->type; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Type/CompositeTypeInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Type; 15 | 16 | /** 17 | * Represents a type composed of several other types. 18 | * 19 | * @author Mathias Arlaud 20 | * 21 | * @template T of Type 22 | */ 23 | interface CompositeTypeInterface 24 | { 25 | /** 26 | * @return list 27 | */ 28 | public function getTypes(): array; 29 | 30 | /** 31 | * @param callable(Type): bool $specification 32 | */ 33 | public function composedTypesAreSatisfiedBy(callable $specification): bool; 34 | } 35 | -------------------------------------------------------------------------------- /Type/EnumType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | /** 15 | * @author Mathias Arlaud 16 | * @author Baptiste Leduc 17 | * 18 | * @template T of class-string<\UnitEnum> 19 | * 20 | * @extends ObjectType 21 | */ 22 | class EnumType extends ObjectType 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /Type/GenericType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeIdentifier; 17 | 18 | /** 19 | * Represents a generic type, which is a type that holds variable parts. 20 | * 21 | * @author Mathias Arlaud 22 | * @author Baptiste Leduc 23 | * 24 | * @template T of BuiltinType|BuiltinType|ObjectType 25 | * 26 | * @implements WrappingTypeInterface 27 | */ 28 | final class GenericType extends Type implements WrappingTypeInterface 29 | { 30 | /** 31 | * @var list 32 | */ 33 | private readonly array $variableTypes; 34 | 35 | /** 36 | * @param T $type 37 | */ 38 | public function __construct( 39 | private readonly BuiltinType|ObjectType $type, 40 | Type ...$variableTypes, 41 | ) { 42 | if ($type instanceof BuiltinType && TypeIdentifier::ARRAY !== $type->getTypeIdentifier() && TypeIdentifier::ITERABLE !== $type->getTypeIdentifier()) { 43 | throw new InvalidArgumentException(\sprintf('Cannot create "%s" with "%s" type.', self::class, $type)); 44 | } 45 | 46 | $this->variableTypes = $variableTypes; 47 | } 48 | 49 | public function getWrappedType(): Type 50 | { 51 | return $this->type; 52 | } 53 | 54 | /** 55 | * @return list 56 | */ 57 | public function getVariableTypes(): array 58 | { 59 | return $this->variableTypes; 60 | } 61 | 62 | public function wrappedTypeIsSatisfiedBy(callable $specification): bool 63 | { 64 | return $this->getWrappedType()->isSatisfiedBy($specification); 65 | } 66 | 67 | public function __toString(): string 68 | { 69 | $typeString = (string) $this->type; 70 | 71 | $variableTypesString = ''; 72 | $glue = ''; 73 | foreach ($this->variableTypes as $t) { 74 | $variableTypesString .= $glue.$t; 75 | $glue = ','; 76 | } 77 | 78 | return $typeString.'<'.$variableTypesString.'>'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Type/IntersectionType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\Type; 16 | 17 | /** 18 | * @author Mathias Arlaud 19 | * @author Baptiste Leduc 20 | * 21 | * @template T of ObjectType|GenericType|CollectionType> 22 | * 23 | * @implements CompositeTypeInterface 24 | */ 25 | final class IntersectionType extends Type implements CompositeTypeInterface 26 | { 27 | /** 28 | * @var list 29 | */ 30 | private readonly array $types; 31 | 32 | /** 33 | * @param list $types 34 | */ 35 | public function __construct(Type ...$types) 36 | { 37 | if (\count($types) < 2) { 38 | throw new InvalidArgumentException(\sprintf('"%s" expects at least 2 types.', self::class)); 39 | } 40 | 41 | foreach ($types as $type) { 42 | if ($type instanceof CompositeTypeInterface || $type instanceof NullableType) { 43 | throw new InvalidArgumentException(\sprintf('Cannot set "%s" as a "%s" part.', $type, self::class)); 44 | } 45 | 46 | while ($type instanceof WrappingTypeInterface) { 47 | $type = $type->getWrappedType(); 48 | } 49 | 50 | if (!$type instanceof ObjectType) { 51 | throw new InvalidArgumentException(\sprintf('Cannot set "%s" as a "%s" part.', $type, self::class)); 52 | } 53 | } 54 | 55 | usort($types, fn (Type $a, Type $b): int => (string) $a <=> (string) $b); 56 | $this->types = array_values(array_unique($types)); 57 | } 58 | 59 | /** 60 | * @return list 61 | */ 62 | public function getTypes(): array 63 | { 64 | return $this->types; 65 | } 66 | 67 | public function composedTypesAreSatisfiedBy(callable $specification): bool 68 | { 69 | foreach ($this->types as $type) { 70 | if (!$type->isSatisfiedBy($specification)) { 71 | return false; 72 | } 73 | } 74 | 75 | return true; 76 | } 77 | 78 | public function __toString(): string 79 | { 80 | $string = ''; 81 | $glue = ''; 82 | 83 | foreach ($this->types as $t) { 84 | $string .= $glue.($t instanceof CompositeTypeInterface ? '('.$t.')' : $t); 85 | $glue = '&'; 86 | } 87 | 88 | return $string; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Type/NullableType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeIdentifier; 17 | 18 | /** 19 | * @author Mathias Arlaud 20 | * 21 | * @template T of Type 22 | * 23 | * @extends UnionType> 24 | * 25 | * @implements WrappingTypeInterface 26 | */ 27 | final class NullableType extends UnionType implements WrappingTypeInterface 28 | { 29 | /** 30 | * @param T $type 31 | */ 32 | public function __construct( 33 | private readonly Type $type, 34 | ) { 35 | if ($type->isNullable()) { 36 | throw new InvalidArgumentException(\sprintf('Cannot create a "%s" with "%s" because it is already nullable.', self::class, $type)); 37 | } 38 | 39 | if ($type instanceof UnionType) { 40 | parent::__construct(Type::null(), ...$type->getTypes()); 41 | 42 | return; 43 | } 44 | 45 | parent::__construct(Type::null(), $type); 46 | } 47 | 48 | public function getWrappedType(): Type 49 | { 50 | return $this->type; 51 | } 52 | 53 | public function wrappedTypeIsSatisfiedBy(callable $specification): bool 54 | { 55 | return $this->getWrappedType()->isSatisfiedBy($specification); 56 | } 57 | 58 | public function isNullable(): bool 59 | { 60 | return true; 61 | } 62 | 63 | public function accepts(mixed $value): bool 64 | { 65 | return null === $value || parent::accepts($value); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Type/ObjectType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Type; 15 | use Symfony\Component\TypeInfo\TypeIdentifier; 16 | 17 | /** 18 | * @author Mathias Arlaud 19 | * @author Baptiste Leduc 20 | * 21 | * @template T of class-string 22 | */ 23 | class ObjectType extends Type 24 | { 25 | /** 26 | * @param T $className 27 | */ 28 | public function __construct( 29 | private readonly string $className, 30 | ) { 31 | } 32 | 33 | public function getTypeIdentifier(): TypeIdentifier 34 | { 35 | return TypeIdentifier::OBJECT; 36 | } 37 | 38 | /** 39 | * @return T 40 | */ 41 | public function getClassName(): string 42 | { 43 | return $this->className; 44 | } 45 | 46 | public function isIdentifiedBy(TypeIdentifier|string ...$identifiers): bool 47 | { 48 | foreach ($identifiers as $identifier) { 49 | if ($identifier instanceof TypeIdentifier) { 50 | if (TypeIdentifier::OBJECT === $identifier) { 51 | return true; 52 | } 53 | 54 | continue; 55 | } 56 | 57 | if (TypeIdentifier::OBJECT->value === $identifier) { 58 | return true; 59 | } 60 | 61 | if (is_a($this->className, $identifier, allow_string: true)) { 62 | return true; 63 | } 64 | } 65 | 66 | return false; 67 | } 68 | 69 | public function accepts(mixed $value): bool 70 | { 71 | return $value instanceof $this->className; 72 | } 73 | 74 | public function __toString(): string 75 | { 76 | return $this->className; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Type/TemplateType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Type; 15 | 16 | /** 17 | * Represents a template placeholder, such as "T" in "Collection". 18 | * 19 | * @author Mathias Arlaud 20 | * @author Baptiste Leduc 21 | * 22 | * @template T of Type 23 | * 24 | * @implements WrappingTypeInterface 25 | */ 26 | final class TemplateType extends Type implements WrappingTypeInterface 27 | { 28 | /** 29 | * @param T $bound 30 | */ 31 | public function __construct( 32 | private readonly string $name, 33 | private readonly Type $bound, 34 | ) { 35 | } 36 | 37 | public function getName(): string 38 | { 39 | return $this->name; 40 | } 41 | 42 | /** 43 | * @return T 44 | */ 45 | public function getBound(): Type 46 | { 47 | return $this->bound; 48 | } 49 | 50 | public function getWrappedType(): Type 51 | { 52 | return $this->bound; 53 | } 54 | 55 | public function wrappedTypeIsSatisfiedBy(callable $specification): bool 56 | { 57 | return $this->getWrappedType()->isSatisfiedBy($specification); 58 | } 59 | 60 | public function __toString(): string 61 | { 62 | return $this->name; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Type/UnionType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeIdentifier; 17 | 18 | /** 19 | * @author Mathias Arlaud 20 | * @author Baptiste Leduc 21 | * 22 | * @template T of Type 23 | * 24 | * @implements CompositeTypeInterface 25 | */ 26 | class UnionType extends Type implements CompositeTypeInterface 27 | { 28 | /** 29 | * @var list 30 | */ 31 | private readonly array $types; 32 | 33 | /** 34 | * @param list $types 35 | */ 36 | public function __construct(Type ...$types) 37 | { 38 | if (\count($types) < 2) { 39 | throw new InvalidArgumentException(\sprintf('"%s" expects at least 2 types.', self::class)); 40 | } 41 | 42 | foreach ($types as $type) { 43 | if ($type instanceof self) { 44 | throw new InvalidArgumentException(\sprintf('Cannot set "%s" as a "%1$s" part.', self::class)); 45 | } 46 | 47 | if ($type instanceof BuiltinType) { 48 | if (TypeIdentifier::NULL === $type->getTypeIdentifier() && !is_a(static::class, NullableType::class, allow_string: true)) { 49 | throw new InvalidArgumentException(\sprintf('Cannot create union with "null", please use "%s" instead.', NullableType::class)); 50 | } 51 | 52 | if ($type->getTypeIdentifier()->isStandalone()) { 53 | throw new InvalidArgumentException(\sprintf('Cannot create union with "%s" standalone type.', $type)); 54 | } 55 | } 56 | } 57 | 58 | usort($types, fn (Type $a, Type $b): int => (string) $a <=> (string) $b); 59 | $this->types = array_values(array_unique($types)); 60 | 61 | $builtinTypesIdentifiers = array_map( 62 | fn (BuiltinType $t): TypeIdentifier => $t->getTypeIdentifier(), 63 | array_filter($this->types, fn (Type $t): bool => $t instanceof BuiltinType), 64 | ); 65 | 66 | if ((\in_array(TypeIdentifier::TRUE, $builtinTypesIdentifiers, true) || \in_array(TypeIdentifier::FALSE, $builtinTypesIdentifiers, true)) && \in_array(TypeIdentifier::BOOL, $builtinTypesIdentifiers, true)) { 67 | throw new InvalidArgumentException('Cannot create union with redundant boolean type.'); 68 | } 69 | 70 | if (\in_array(TypeIdentifier::TRUE, $builtinTypesIdentifiers, true) && \in_array(TypeIdentifier::FALSE, $builtinTypesIdentifiers, true)) { 71 | throw new InvalidArgumentException('Cannot create union with both "true" and "false", "bool" should be used instead.'); 72 | } 73 | 74 | if (\in_array(TypeIdentifier::OBJECT, $builtinTypesIdentifiers, true) && \count(array_filter($this->types, fn (Type $t): bool => $t instanceof ObjectType))) { 75 | throw new InvalidArgumentException('Cannot create union with both "object" and class type.'); 76 | } 77 | } 78 | 79 | /** 80 | * @return list 81 | */ 82 | public function getTypes(): array 83 | { 84 | return $this->types; 85 | } 86 | 87 | public function composedTypesAreSatisfiedBy(callable $specification): bool 88 | { 89 | foreach ($this->types as $type) { 90 | if ($type->isSatisfiedBy($specification)) { 91 | return true; 92 | } 93 | } 94 | 95 | return false; 96 | } 97 | 98 | public function __toString(): string 99 | { 100 | $string = ''; 101 | $glue = ''; 102 | 103 | foreach ($this->types as $t) { 104 | $string .= $glue.($t instanceof CompositeTypeInterface ? '('.$t.')' : $t); 105 | $glue = '|'; 106 | } 107 | 108 | return $string; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Type/WrappingTypeInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\Type; 13 | 14 | use Symfony\Component\TypeInfo\Type; 15 | 16 | /** 17 | * Represents a type wrapping another type. 18 | * 19 | * @author Mathias Arlaud 20 | * 21 | * @template T of Type 22 | */ 23 | interface WrappingTypeInterface 24 | { 25 | /** 26 | * @return T 27 | */ 28 | public function getWrappedType(): Type; 29 | 30 | /** 31 | * @param callable(Type): bool $specification 32 | */ 33 | public function wrappedTypeIsSatisfiedBy(callable $specification): bool; 34 | } 35 | -------------------------------------------------------------------------------- /TypeContext/TypeContext.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeContext; 13 | 14 | use Symfony\Component\TypeInfo\Exception\LogicException; 15 | use Symfony\Component\TypeInfo\Type; 16 | 17 | /** 18 | * Type resolving context. 19 | * 20 | * Helps to retrieve declaring class, called class, parent class, templates 21 | * and normalize classes according to the current namespace and uses. 22 | * 23 | * @author Mathias Arlaud 24 | * @author Baptiste Leduc 25 | */ 26 | final class TypeContext 27 | { 28 | /** 29 | * @var array 30 | */ 31 | private static array $classExistCache = []; 32 | 33 | /** 34 | * @param array $uses 35 | * @param array $templates 36 | * @param array $typeAliases 37 | */ 38 | public function __construct( 39 | public readonly string $calledClassName, 40 | public readonly string $declaringClassName, 41 | public readonly ?string $namespace = null, 42 | public readonly array $uses = [], 43 | public readonly array $templates = [], 44 | public readonly array $typeAliases = [], 45 | ) { 46 | } 47 | 48 | /** 49 | * Normalize class name according to current namespace and uses. 50 | */ 51 | public function normalize(string $name): string 52 | { 53 | if (str_starts_with($name, '\\')) { 54 | return ltrim($name, '\\'); 55 | } 56 | 57 | $nameParts = explode('\\', $name); 58 | $firstNamePart = $nameParts[0]; 59 | if (isset($this->uses[$firstNamePart])) { 60 | if (1 === \count($nameParts)) { 61 | return $this->uses[$firstNamePart]; 62 | } 63 | array_shift($nameParts); 64 | 65 | return \sprintf('%s\\%s', $this->uses[$firstNamePart], implode('\\', $nameParts)); 66 | } 67 | 68 | if (null !== $this->namespace) { 69 | return \sprintf('%s\\%s', $this->namespace, $name); 70 | } 71 | 72 | return $name; 73 | } 74 | 75 | /** 76 | * @return class-string 77 | */ 78 | public function getDeclaringClass(): string 79 | { 80 | return $this->normalize($this->declaringClassName); 81 | } 82 | 83 | /** 84 | * @return class-string 85 | */ 86 | public function getCalledClass(): string 87 | { 88 | return $this->normalize($this->calledClassName); 89 | } 90 | 91 | /** 92 | * @return class-string 93 | */ 94 | public function getParentClass(): string 95 | { 96 | $declaringClassName = $this->getDeclaringClass(); 97 | 98 | if (false === $parentClass = get_parent_class($declaringClassName)) { 99 | throw new LogicException(\sprintf('"%s" do not extend any class.', $declaringClassName)); 100 | } 101 | 102 | if (!isset(self::$classExistCache[$parentClass])) { 103 | self::$classExistCache[$parentClass] = false; 104 | 105 | if (class_exists($parentClass)) { 106 | self::$classExistCache[$parentClass] = true; 107 | } else { 108 | try { 109 | new \ReflectionClass($parentClass); 110 | self::$classExistCache[$parentClass] = true; 111 | } catch (\Throwable) { 112 | } 113 | } 114 | } 115 | 116 | return self::$classExistCache[$parentClass] ? $parentClass : $this->normalize(str_replace($this->namespace.'\\', '', $parentClass)); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /TypeContext/TypeContextFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeContext; 13 | 14 | use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; 15 | use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; 16 | use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; 17 | use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; 18 | use PHPStan\PhpDocParser\Lexer\Lexer; 19 | use PHPStan\PhpDocParser\Parser\ConstExprParser; 20 | use PHPStan\PhpDocParser\Parser\PhpDocParser; 21 | use PHPStan\PhpDocParser\Parser\TokenIterator; 22 | use PHPStan\PhpDocParser\Parser\TypeParser; 23 | use PHPStan\PhpDocParser\ParserConfig; 24 | use Symfony\Component\TypeInfo\Exception\LogicException; 25 | use Symfony\Component\TypeInfo\Exception\RuntimeException; 26 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 27 | use Symfony\Component\TypeInfo\Type; 28 | use Symfony\Component\TypeInfo\Type\ObjectType; 29 | use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; 30 | 31 | /** 32 | * Creates a type resolving context. 33 | * 34 | * @author Mathias Arlaud 35 | * @author Baptiste Leduc 36 | */ 37 | final class TypeContextFactory 38 | { 39 | /** 40 | * @var array 41 | */ 42 | private static array $reflectionClassCache = []; 43 | 44 | private ?Lexer $phpstanLexer = null; 45 | private ?PhpDocParser $phpstanParser = null; 46 | 47 | public function __construct( 48 | private readonly ?StringTypeResolver $stringTypeResolver = null, 49 | ) { 50 | } 51 | 52 | public function createFromClassName(string $calledClassName, ?string $declaringClassName = null): TypeContext 53 | { 54 | $declaringClassName ??= $calledClassName; 55 | 56 | $calledClassPath = explode('\\', $calledClassName); 57 | $declaringClassPath = explode('\\', $declaringClassName); 58 | 59 | $declaringClassReflection = self::$reflectionClassCache[$declaringClassName] ??= new \ReflectionClass($declaringClassName); 60 | 61 | $typeContext = new TypeContext( 62 | end($calledClassPath), 63 | end($declaringClassPath), 64 | trim($declaringClassReflection->getNamespaceName(), '\\'), 65 | $this->collectUses($declaringClassReflection), 66 | ); 67 | 68 | return new TypeContext( 69 | $typeContext->calledClassName, 70 | $typeContext->declaringClassName, 71 | $typeContext->namespace, 72 | $typeContext->uses, 73 | $this->collectTemplates($declaringClassReflection, $typeContext), 74 | $this->collectTypeAliases($declaringClassReflection, $typeContext), 75 | ); 76 | } 77 | 78 | public function createFromReflection(\Reflector $reflection): ?TypeContext 79 | { 80 | $declaringClassReflection = match (true) { 81 | $reflection instanceof \ReflectionClass => $reflection, 82 | $reflection instanceof \ReflectionMethod => $reflection->getDeclaringClass(), 83 | $reflection instanceof \ReflectionProperty => $reflection->getDeclaringClass(), 84 | $reflection instanceof \ReflectionParameter => $reflection->getDeclaringClass(), 85 | $reflection instanceof \ReflectionFunctionAbstract => $reflection->getClosureScopeClass(), 86 | default => null, 87 | }; 88 | 89 | if (null === $declaringClassReflection) { 90 | return null; 91 | } 92 | 93 | $typeContext = new TypeContext( 94 | $declaringClassReflection->getShortName(), 95 | $declaringClassReflection->getShortName(), 96 | $declaringClassReflection->getNamespaceName(), 97 | $this->collectUses($declaringClassReflection), 98 | ); 99 | 100 | $templates = match (true) { 101 | $reflection instanceof \ReflectionFunctionAbstract => $this->collectTemplates($reflection, $typeContext) + $this->collectTemplates($declaringClassReflection, $typeContext), 102 | $reflection instanceof \ReflectionParameter => $this->collectTemplates($reflection->getDeclaringFunction(), $typeContext) + $this->collectTemplates($declaringClassReflection, $typeContext), 103 | default => $this->collectTemplates($declaringClassReflection, $typeContext), 104 | }; 105 | 106 | return new TypeContext( 107 | $typeContext->calledClassName, 108 | $typeContext->declaringClassName, 109 | $typeContext->namespace, 110 | $typeContext->uses, 111 | $templates, 112 | $this->collectTypeAliases($declaringClassReflection, $typeContext), 113 | ); 114 | } 115 | 116 | /** 117 | * @return array 118 | */ 119 | private function collectUses(\ReflectionClass $reflection): array 120 | { 121 | $fileName = $reflection->getFileName(); 122 | if (!\is_string($fileName) || !is_file($fileName)) { 123 | return []; 124 | } 125 | 126 | if (false === $lines = @file($fileName)) { 127 | throw new RuntimeException(\sprintf('Unable to read file "%s".', $fileName)); 128 | } 129 | 130 | $uses = []; 131 | $inUseSection = false; 132 | 133 | foreach ($lines as $line) { 134 | if (str_starts_with($line, 'use ')) { 135 | $inUseSection = true; 136 | $use = explode(' as ', substr($line, 4, -2), 2); 137 | 138 | $alias = 1 === \count($use) ? substr($use[0], false !== ($p = strrpos($use[0], '\\')) ? 1 + $p : 0) : $use[1]; 139 | $uses[$alias] = $use[0]; 140 | } elseif ($inUseSection) { 141 | break; 142 | } 143 | } 144 | 145 | $traitUses = []; 146 | foreach ($reflection->getTraits() as $traitReflection) { 147 | $traitUses[] = $this->collectUses($traitReflection); 148 | } 149 | 150 | return array_merge($uses, ...$traitUses); 151 | } 152 | 153 | /** 154 | * @return array 155 | */ 156 | private function collectTemplates(\ReflectionClass|\ReflectionFunctionAbstract $reflection, TypeContext $typeContext): array 157 | { 158 | if (!$this->stringTypeResolver || !class_exists(PhpDocParser::class)) { 159 | return []; 160 | } 161 | 162 | if (!$rawDocNode = $reflection->getDocComment()) { 163 | return []; 164 | } 165 | 166 | $templates = []; 167 | foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@template') as $tag) { 168 | if (!$tag->value instanceof TemplateTagValueNode) { 169 | continue; 170 | } 171 | 172 | $type = Type::mixed(); 173 | $typeString = ((string) $tag->value->bound) ?: null; 174 | 175 | try { 176 | if (null !== $typeString) { 177 | $type = $this->stringTypeResolver->resolve($typeString, $typeContext); 178 | } 179 | } catch (UnsupportedException) { 180 | } 181 | 182 | $templates[$tag->value->name] = $type; 183 | } 184 | 185 | return $templates; 186 | } 187 | 188 | /** 189 | * @return array 190 | */ 191 | private function collectTypeAliases(\ReflectionClass $reflection, TypeContext $typeContext): array 192 | { 193 | if (!$this->stringTypeResolver || !class_exists(PhpDocParser::class)) { 194 | return []; 195 | } 196 | 197 | if (!$rawDocNode = $reflection->getDocComment()) { 198 | return []; 199 | } 200 | 201 | $aliases = []; 202 | $resolvedAliases = []; 203 | 204 | foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-import-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-import-type') as $tag) { 205 | if (!$tag->value instanceof TypeAliasImportTagValueNode) { 206 | continue; 207 | } 208 | 209 | $importedFromType = $this->stringTypeResolver->resolve((string) $tag->value->importedFrom, $typeContext); 210 | if (!$importedFromType instanceof ObjectType) { 211 | throw new LogicException(\sprintf('Type alias "%s" is not imported from a valid class name.', $tag->value->importedAlias)); 212 | } 213 | 214 | $importedFromContext = $this->createFromClassName($importedFromType->getClassName()); 215 | 216 | $typeAlias = $importedFromContext->typeAliases[$tag->value->importedAlias] ?? null; 217 | if (!$typeAlias) { 218 | throw new LogicException(\sprintf('Cannot find any "%s" type alias in "%s".', $tag->value->importedAlias, $importedFromType->getClassName())); 219 | } 220 | 221 | $resolvedAliases[$tag->value->importedAs ?? $tag->value->importedAlias] = $typeAlias; 222 | } 223 | 224 | foreach ($this->getPhpDocNode($rawDocNode)->getTagsByName('@psalm-type') + $this->getPhpDocNode($rawDocNode)->getTagsByName('@phpstan-type') as $tag) { 225 | if (!$tag->value instanceof TypeAliasTagValueNode) { 226 | continue; 227 | } 228 | 229 | $aliases[$tag->value->alias] = (string) $tag->value->type; 230 | } 231 | 232 | return $this->resolveTypeAliases($aliases, $resolvedAliases, $typeContext); 233 | } 234 | 235 | /** 236 | * @param array $toResolve 237 | * @param array $resolved 238 | * 239 | * @return array 240 | */ 241 | private function resolveTypeAliases(array $toResolve, array $resolved, TypeContext $typeContext): array 242 | { 243 | if (!$toResolve) { 244 | return []; 245 | } 246 | 247 | $typeContext = new TypeContext( 248 | $typeContext->calledClassName, 249 | $typeContext->declaringClassName, 250 | $typeContext->namespace, 251 | $typeContext->uses, 252 | $typeContext->templates, 253 | $typeContext->typeAliases + $resolved, 254 | ); 255 | 256 | $succeeded = false; 257 | $lastFailure = null; 258 | $lastFailingAlias = null; 259 | 260 | foreach ($toResolve as $alias => $type) { 261 | try { 262 | $resolved[$alias] = $this->stringTypeResolver->resolve($type, $typeContext); 263 | unset($toResolve[$alias]); 264 | $succeeded = true; 265 | } catch (UnsupportedException $lastFailure) { 266 | $lastFailingAlias = $alias; 267 | } 268 | } 269 | 270 | // nothing has succeeded, the result won't be different from the 271 | // previous one, we can stop here. 272 | if (!$succeeded) { 273 | throw new LogicException(\sprintf('Cannot resolve "%s" type alias.', $lastFailingAlias), 0, $lastFailure); 274 | } 275 | 276 | if ($toResolve) { 277 | return $this->resolveTypeAliases($toResolve, $resolved, $typeContext); 278 | } 279 | 280 | return $resolved; 281 | } 282 | 283 | private function getPhpDocNode(string $rawDocNode): PhpDocNode 284 | { 285 | if (class_exists(ParserConfig::class)) { 286 | $this->phpstanLexer ??= new Lexer($config = new ParserConfig([])); 287 | $this->phpstanParser ??= new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config)); 288 | } else { 289 | $this->phpstanLexer ??= new Lexer(); 290 | $this->phpstanParser ??= new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); 291 | } 292 | 293 | return $this->phpstanParser->parse(new TokenIterator($this->phpstanLexer->tokenize($rawDocNode))); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /TypeFactoryTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo; 13 | 14 | use Symfony\Component\TypeInfo\Type\ArrayShapeType; 15 | use Symfony\Component\TypeInfo\Type\BackedEnumType; 16 | use Symfony\Component\TypeInfo\Type\BuiltinType; 17 | use Symfony\Component\TypeInfo\Type\CollectionType; 18 | use Symfony\Component\TypeInfo\Type\EnumType; 19 | use Symfony\Component\TypeInfo\Type\GenericType; 20 | use Symfony\Component\TypeInfo\Type\IntersectionType; 21 | use Symfony\Component\TypeInfo\Type\NullableType; 22 | use Symfony\Component\TypeInfo\Type\ObjectType; 23 | use Symfony\Component\TypeInfo\Type\TemplateType; 24 | use Symfony\Component\TypeInfo\Type\UnionType; 25 | 26 | /** 27 | * Helper trait to create any type easily. 28 | * 29 | * @author Mathias Arlaud 30 | * @author Baptiste Leduc 31 | */ 32 | trait TypeFactoryTrait 33 | { 34 | /** 35 | * @template T of TypeIdentifier 36 | * @template U value-of 37 | * 38 | * @param T|U $identifier 39 | * 40 | * @return BuiltinType 41 | */ 42 | public static function builtin(TypeIdentifier|string $identifier): BuiltinType 43 | { 44 | /** @var T $identifier */ 45 | $identifier = \is_string($identifier) ? TypeIdentifier::from($identifier) : $identifier; 46 | 47 | return new BuiltinType($identifier); 48 | } 49 | 50 | /** 51 | * @return BuiltinType 52 | */ 53 | public static function int(): BuiltinType 54 | { 55 | return self::builtin(TypeIdentifier::INT); 56 | } 57 | 58 | /** 59 | * @return BuiltinType 60 | */ 61 | public static function float(): BuiltinType 62 | { 63 | return self::builtin(TypeIdentifier::FLOAT); 64 | } 65 | 66 | /** 67 | * @return BuiltinType 68 | */ 69 | public static function string(): BuiltinType 70 | { 71 | return self::builtin(TypeIdentifier::STRING); 72 | } 73 | 74 | /** 75 | * @return BuiltinType 76 | */ 77 | public static function bool(): BuiltinType 78 | { 79 | return self::builtin(TypeIdentifier::BOOL); 80 | } 81 | 82 | /** 83 | * @return BuiltinType 84 | */ 85 | public static function resource(): BuiltinType 86 | { 87 | return self::builtin(TypeIdentifier::RESOURCE); 88 | } 89 | 90 | /** 91 | * @return BuiltinType 92 | */ 93 | public static function false(): BuiltinType 94 | { 95 | return self::builtin(TypeIdentifier::FALSE); 96 | } 97 | 98 | /** 99 | * @return BuiltinType 100 | */ 101 | public static function true(): BuiltinType 102 | { 103 | return self::builtin(TypeIdentifier::TRUE); 104 | } 105 | 106 | /** 107 | * @return BuiltinType 108 | */ 109 | public static function callable(): BuiltinType 110 | { 111 | return self::builtin(TypeIdentifier::CALLABLE); 112 | } 113 | 114 | /** 115 | * @return BuiltinType 116 | */ 117 | public static function mixed(): BuiltinType 118 | { 119 | return self::builtin(TypeIdentifier::MIXED); 120 | } 121 | 122 | /** 123 | * @return BuiltinType 124 | */ 125 | public static function null(): BuiltinType 126 | { 127 | return self::builtin(TypeIdentifier::NULL); 128 | } 129 | 130 | /** 131 | * @return BuiltinType 132 | */ 133 | public static function void(): BuiltinType 134 | { 135 | return self::builtin(TypeIdentifier::VOID); 136 | } 137 | 138 | /** 139 | * @return BuiltinType 140 | */ 141 | public static function never(): BuiltinType 142 | { 143 | return self::builtin(TypeIdentifier::NEVER); 144 | } 145 | 146 | /** 147 | * @template T of BuiltinType|BuiltinType|ObjectType|GenericType 148 | * 149 | * @param T $type 150 | * 151 | * @return CollectionType 152 | */ 153 | public static function collection(BuiltinType|ObjectType|GenericType $type, ?Type $value = null, ?Type $key = null, bool $asList = false): CollectionType 154 | { 155 | if (!$type instanceof GenericType && (null !== $value || null !== $key)) { 156 | $type = self::generic($type, $key ?? self::arrayKey(), $value ?? self::mixed()); 157 | } 158 | 159 | return new CollectionType($type, $asList); 160 | } 161 | 162 | /** 163 | * @return CollectionType> 164 | */ 165 | public static function array(?Type $value = null, ?Type $key = null, bool $asList = false): CollectionType 166 | { 167 | return self::collection(self::builtin(TypeIdentifier::ARRAY), $value, $key, $asList); 168 | } 169 | 170 | /** 171 | * @return CollectionType> 172 | */ 173 | public static function iterable(?Type $value = null, ?Type $key = null, bool $asList = false): CollectionType 174 | { 175 | if ($asList) { 176 | trigger_deprecation('symfony/type-info', '7.3', 'The third argument of "%s()" is deprecated. Use the "%s::list()" method to create a list instead.', __METHOD__, self::class); 177 | } 178 | 179 | return self::collection(self::builtin(TypeIdentifier::ITERABLE), $value, $key, $asList); 180 | } 181 | 182 | /** 183 | * @return CollectionType> 184 | */ 185 | public static function list(?Type $value = null): CollectionType 186 | { 187 | return self::array($value, self::int(), asList: true); 188 | } 189 | 190 | /** 191 | * @return CollectionType> 192 | */ 193 | public static function dict(?Type $value = null): CollectionType 194 | { 195 | return self::array($value, self::string()); 196 | } 197 | 198 | /** 199 | * @param array $shape 200 | */ 201 | public static function arrayShape(array $shape, bool $sealed = true, ?Type $extraKeyType = null, ?Type $extraValueType = null): ArrayShapeType 202 | { 203 | $shape = array_map(static function (array|Type $item): array { 204 | return $item instanceof Type 205 | ? ['type' => $item, 'optional' => false] 206 | : ['type' => $item['type'], 'optional' => $item['optional'] ?? false]; 207 | }, $shape); 208 | 209 | if ($extraKeyType || $extraValueType) { 210 | $sealed = false; 211 | } 212 | 213 | $extraKeyType ??= !$sealed ? Type::arrayKey() : null; 214 | $extraValueType ??= !$sealed ? Type::mixed() : null; 215 | 216 | return new ArrayShapeType($shape, $extraKeyType, $extraValueType); 217 | } 218 | 219 | public static function arrayKey(): UnionType 220 | { 221 | return self::union(self::int(), self::string()); 222 | } 223 | 224 | /** 225 | * @template T of class-string 226 | * 227 | * @param T|null $className 228 | * 229 | * @return ($className is class-string ? ObjectType : BuiltinType) 230 | */ 231 | public static function object(?string $className = null): BuiltinType|ObjectType 232 | { 233 | return null !== $className ? new ObjectType($className) : new BuiltinType(TypeIdentifier::OBJECT); 234 | } 235 | 236 | /** 237 | * @template T of class-string<\UnitEnum>|class-string<\BackedEnum> 238 | * @template U of BuiltinType|BuiltinType 239 | * 240 | * @param T $className 241 | * @param U|null $backingType 242 | * 243 | * @return ($className is class-string<\BackedEnum> ? ($backingType is U ? BackedEnumType : BackedEnumType|BuiltinType>) : EnumType)) 244 | */ 245 | public static function enum(string $className, ?BuiltinType $backingType = null): EnumType 246 | { 247 | if (is_subclass_of($className, \BackedEnum::class)) { 248 | if (null === $backingType) { 249 | $reflectionBackingType = (new \ReflectionEnum($className))->getBackingType(); 250 | $typeIdentifier = TypeIdentifier::INT->value === (string) $reflectionBackingType ? TypeIdentifier::INT : TypeIdentifier::STRING; 251 | $backingType = new BuiltinType($typeIdentifier); 252 | } 253 | 254 | return new BackedEnumType($className, $backingType); 255 | } 256 | 257 | return new EnumType($className); 258 | } 259 | 260 | /** 261 | * @template T of BuiltinType|BuiltinType|ObjectType 262 | * 263 | * @param T $mainType 264 | * 265 | * @return GenericType 266 | */ 267 | public static function generic(BuiltinType|ObjectType $mainType, Type ...$variableTypes): GenericType 268 | { 269 | return new GenericType($mainType, ...$variableTypes); 270 | } 271 | 272 | /** 273 | * @template T of Type 274 | * 275 | * @param T|null $bound 276 | * 277 | * @return ($bound is null ? TemplateType> : TemplateType) 278 | */ 279 | public static function template(string $name, ?Type $bound = null): TemplateType 280 | { 281 | return new TemplateType($name, $bound ?? Type::mixed()); 282 | } 283 | 284 | /** 285 | * @template T of Type 286 | * 287 | * @param list $types 288 | * 289 | * @return UnionType|NullableType 290 | */ 291 | public static function union(Type ...$types): UnionType 292 | { 293 | /** @var list $unionTypes */ 294 | $unionTypes = []; 295 | 296 | $nullableUnion = false; 297 | $isNullable = fn (Type $type): bool => $type instanceof BuiltinType && TypeIdentifier::NULL === $type->getTypeIdentifier(); 298 | 299 | foreach ($types as $type) { 300 | if ($type instanceof NullableType) { 301 | $nullableUnion = true; 302 | $unionTypes[] = $type->getWrappedType(); 303 | 304 | continue; 305 | } 306 | 307 | if ($type instanceof UnionType) { 308 | foreach ($type->getTypes() as $unionType) { 309 | if ($isNullable($type)) { 310 | $nullableUnion = true; 311 | 312 | continue; 313 | } 314 | 315 | $unionTypes[] = $unionType; 316 | } 317 | 318 | continue; 319 | } 320 | 321 | if ($isNullable($type)) { 322 | $nullableUnion = true; 323 | 324 | continue; 325 | } 326 | 327 | $unionTypes[] = $type; 328 | } 329 | 330 | if (1 === \count($unionTypes)) { 331 | return self::nullable($unionTypes[0]); 332 | } 333 | 334 | $unionType = new UnionType(...$unionTypes); 335 | 336 | return $nullableUnion ? self::nullable($unionType) : $unionType; 337 | } 338 | 339 | /** 340 | * @template T of ObjectType|GenericType|CollectionType> 341 | * 342 | * @param list> $types 343 | * 344 | * @return IntersectionType 345 | */ 346 | public static function intersection(Type ...$types): IntersectionType 347 | { 348 | /** @var list $intersectionTypes */ 349 | $intersectionTypes = []; 350 | 351 | foreach ($types as $type) { 352 | if (!$type instanceof IntersectionType) { 353 | $intersectionTypes[] = $type; 354 | 355 | continue; 356 | } 357 | 358 | foreach ($type->getTypes() as $intersectionType) { 359 | $intersectionTypes[] = $intersectionType; 360 | } 361 | } 362 | 363 | return new IntersectionType(...$intersectionTypes); 364 | } 365 | 366 | /** 367 | * @template T of Type 368 | * 369 | * @param T $type 370 | * 371 | * @return T|NullableType 372 | */ 373 | public static function nullable(Type $type): Type 374 | { 375 | if ($type->isNullable()) { 376 | return $type; 377 | } 378 | 379 | return new NullableType($type); 380 | } 381 | 382 | public static function fromValue(mixed $value): Type 383 | { 384 | $type = match ($value) { 385 | null => self::null(), 386 | true => self::true(), 387 | false => self::false(), 388 | default => null, 389 | }; 390 | 391 | if (null !== $type) { 392 | return $type; 393 | } 394 | 395 | if (\is_callable($value)) { 396 | return Type::callable(); 397 | } 398 | 399 | if (\is_resource($value)) { 400 | return Type::resource(); 401 | } 402 | 403 | $type = match (get_debug_type($value)) { 404 | TypeIdentifier::INT->value => self::int(), 405 | TypeIdentifier::FLOAT->value => self::float(), 406 | TypeIdentifier::STRING->value => self::string(), 407 | default => null, 408 | }; 409 | 410 | if (null !== $type) { 411 | return $type; 412 | } 413 | 414 | $type = match (true) { 415 | \is_object($value) => \stdClass::class === $value::class ? self::object() : self::object($value::class), 416 | \is_array($value) => self::builtin(TypeIdentifier::ARRAY), 417 | default => null, 418 | }; 419 | 420 | if (null === $type) { 421 | return Type::mixed(); 422 | } 423 | 424 | if (is_iterable($value)) { 425 | /** @var list|BuiltinType> $keyTypes */ 426 | $keyTypes = []; 427 | 428 | /** @var list $valueTypes */ 429 | $valueTypes = []; 430 | 431 | $i = 0; 432 | 433 | foreach ($value as $k => $v) { 434 | $keyTypes[] = self::fromValue($k); 435 | $valueTypes[] = self::fromValue($v); 436 | } 437 | 438 | if ($keyTypes) { 439 | $keyTypes = array_values(array_unique($keyTypes)); 440 | $keyType = \count($keyTypes) > 1 ? self::union(...$keyTypes) : $keyTypes[0]; 441 | } else { 442 | $keyType = Type::arrayKey(); 443 | } 444 | 445 | $valueType = $valueTypes ? CollectionType::mergeCollectionValueTypes($valueTypes) : Type::mixed(); 446 | 447 | return self::collection($type, $valueType, $keyType, \is_array($value) && array_is_list($value)); 448 | } 449 | 450 | if ($value instanceof \ArrayAccess) { 451 | return self::collection($type); 452 | } 453 | 454 | return $type; 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /TypeIdentifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo; 13 | 14 | /** 15 | * Identifier of a PHP native type. 16 | * 17 | * @author Mathias Arlaud 18 | * @author Baptiste Leduc 19 | */ 20 | enum TypeIdentifier: string 21 | { 22 | case ARRAY = 'array'; 23 | case BOOL = 'bool'; 24 | case CALLABLE = 'callable'; 25 | case FALSE = 'false'; 26 | case FLOAT = 'float'; 27 | case INT = 'int'; 28 | case ITERABLE = 'iterable'; 29 | case MIXED = 'mixed'; 30 | case NULL = 'null'; 31 | case OBJECT = 'object'; 32 | case RESOURCE = 'resource'; 33 | case STRING = 'string'; 34 | case TRUE = 'true'; 35 | case NEVER = 'never'; 36 | case VOID = 'void'; 37 | 38 | /** 39 | * @return list 40 | */ 41 | public static function values(): array 42 | { 43 | return array_column(self::cases(), 'value'); 44 | } 45 | 46 | public function isStandalone(): bool 47 | { 48 | return \in_array($this, [self::MIXED, self::NEVER, self::VOID], true); 49 | } 50 | 51 | public function isScalar(): bool 52 | { 53 | return \in_array($this, [self::STRING, self::FLOAT, self::INT, self::BOOL, self::FALSE, self::TRUE], true); 54 | } 55 | 56 | public function isBool(): bool 57 | { 58 | return \in_array($this, [self::BOOL, self::FALSE, self::TRUE], true); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TypeResolver/PhpDocAwareReflectionTypeResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; 15 | use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; 16 | use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; 17 | use PHPStan\PhpDocParser\Lexer\Lexer; 18 | use PHPStan\PhpDocParser\Parser\ConstExprParser; 19 | use PHPStan\PhpDocParser\Parser\PhpDocParser; 20 | use PHPStan\PhpDocParser\Parser\TokenIterator; 21 | use PHPStan\PhpDocParser\Parser\TypeParser; 22 | use PHPStan\PhpDocParser\ParserConfig; 23 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 24 | use Symfony\Component\TypeInfo\Type; 25 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 26 | use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; 27 | 28 | /** 29 | * Resolves type on reflection prioriziting PHP documentation. 30 | * 31 | * @author Mathias Arlaud 32 | */ 33 | final readonly class PhpDocAwareReflectionTypeResolver implements TypeResolverInterface 34 | { 35 | private PhpDocParser $phpDocParser; 36 | private Lexer $lexer; 37 | 38 | public function __construct( 39 | private TypeResolverInterface $reflectionTypeResolver, 40 | private TypeResolverInterface $stringTypeResolver, 41 | private TypeContextFactory $typeContextFactory, 42 | ?PhpDocParser $phpDocParser = null, 43 | ?Lexer $lexer = null, 44 | ) { 45 | if (class_exists(ParserConfig::class)) { 46 | $this->lexer = $lexer ?? new Lexer(new ParserConfig([])); 47 | $this->phpDocParser = $phpDocParser ?? new PhpDocParser( 48 | $config = new ParserConfig([]), 49 | new TypeParser($config, $constExprParser = new ConstExprParser($config)), 50 | $constExprParser, 51 | ); 52 | } else { 53 | $this->lexer = $lexer ?? new Lexer(); 54 | $this->phpDocParser = $phpDocParser ?? new PhpDocParser( 55 | new TypeParser($constExprParser = new ConstExprParser()), 56 | $constExprParser, 57 | ); 58 | } 59 | } 60 | 61 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type 62 | { 63 | if (!$subject instanceof \ReflectionProperty && !$subject instanceof \ReflectionParameter && !$subject instanceof \ReflectionFunctionAbstract) { 64 | throw new UnsupportedException(\sprintf('Expected subject to be a "ReflectionProperty", a "ReflectionParameter" or a "ReflectionFunctionAbstract", "%s" given.', get_debug_type($subject)), $subject); 65 | } 66 | 67 | $typeContext ??= $this->typeContextFactory->createFromReflection($subject); 68 | 69 | $docComments = match (true) { 70 | $subject instanceof \ReflectionProperty => $subject->isPromoted() 71 | ? ['@var' => $subject->getDocComment(), '@param' => $subject->getDeclaringClass()?->getConstructor()?->getDocComment()] 72 | : ['@var' => $subject->getDocComment()], 73 | $subject instanceof \ReflectionParameter => ['@param' => $subject->getDeclaringFunction()->getDocComment()], 74 | $subject instanceof \ReflectionFunctionAbstract => ['@return' => $subject->getDocComment()], 75 | }; 76 | 77 | foreach ($docComments as $tagName => $docComment) { 78 | if (!$docComment) { 79 | continue; 80 | } 81 | 82 | $tokens = new TokenIterator($this->lexer->tokenize($docComment)); 83 | $docNode = $this->phpDocParser->parse($tokens); 84 | 85 | foreach ($docNode->getTagsByName($tagName) as $tag) { 86 | $tagValue = $tag->value; 87 | 88 | if ('@var' === $tagName && $tagValue instanceof VarTagValueNode) { 89 | return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); 90 | } 91 | 92 | if ('@param' === $tagName && $tagValue instanceof ParamTagValueNode && '$'.$subject->getName() === $tagValue->parameterName) { 93 | return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); 94 | } 95 | 96 | if ('@return' === $tagName && $tagValue instanceof ReturnTagValueNode) { 97 | return $this->stringTypeResolver->resolve((string) $tagValue, $typeContext); 98 | } 99 | } 100 | } 101 | 102 | return $this->reflectionTypeResolver->resolve($subject); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /TypeResolver/ReflectionParameterTypeResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 17 | use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; 18 | 19 | /** 20 | * Resolves type for a given parameter reflection. 21 | * 22 | * @author Mathias Arlaud 23 | * @author Baptiste Leduc 24 | */ 25 | final readonly class ReflectionParameterTypeResolver implements TypeResolverInterface 26 | { 27 | public function __construct( 28 | private ReflectionTypeResolver $reflectionTypeResolver, 29 | private TypeContextFactory $typeContextFactory, 30 | ) { 31 | } 32 | 33 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type 34 | { 35 | if (!$subject instanceof \ReflectionParameter) { 36 | throw new UnsupportedException(\sprintf('Expected subject to be a "ReflectionParameter", "%s" given.', get_debug_type($subject)), $subject); 37 | } 38 | 39 | $typeContext ??= $this->typeContextFactory->createFromReflection($subject); 40 | 41 | try { 42 | return $this->reflectionTypeResolver->resolve($subject->getType(), $typeContext); 43 | } catch (UnsupportedException $e) { 44 | $path = null !== $typeContext 45 | ? \sprintf('%s::%s($%s)', $typeContext->calledClassName, $subject->getDeclaringFunction()->getName(), $subject->getName()) 46 | : \sprintf('%s($%s)', $subject->getDeclaringFunction()->getName(), $subject->getName()); 47 | 48 | throw new UnsupportedException(\sprintf('Cannot resolve type for "%s".', $path), $subject, previous: $e); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TypeResolver/ReflectionPropertyTypeResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 17 | use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; 18 | 19 | /** 20 | * Resolves type for a given property reflection. 21 | * 22 | * @author Mathias Arlaud 23 | * @author Baptiste Leduc 24 | */ 25 | final readonly class ReflectionPropertyTypeResolver implements TypeResolverInterface 26 | { 27 | public function __construct( 28 | private ReflectionTypeResolver $reflectionTypeResolver, 29 | private TypeContextFactory $typeContextFactory, 30 | ) { 31 | } 32 | 33 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type 34 | { 35 | if (!$subject instanceof \ReflectionProperty) { 36 | throw new UnsupportedException(\sprintf('Expected subject to be a "ReflectionProperty", "%s" given.', get_debug_type($subject)), $subject); 37 | } 38 | 39 | $typeContext ??= $this->typeContextFactory->createFromReflection($subject); 40 | 41 | try { 42 | return $this->reflectionTypeResolver->resolve($subject->getType(), $typeContext); 43 | } catch (UnsupportedException $e) { 44 | $path = \sprintf('%s::$%s', $subject->getDeclaringClass()->getName(), $subject->getName()); 45 | 46 | throw new UnsupportedException(\sprintf('Cannot resolve type for "%s".', $path), $subject, previous: $e); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TypeResolver/ReflectionReturnTypeResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 17 | use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; 18 | 19 | /** 20 | * Resolves return type for a given function reflection. 21 | * 22 | * @author Mathias Arlaud 23 | * @author Baptiste Leduc 24 | */ 25 | final readonly class ReflectionReturnTypeResolver implements TypeResolverInterface 26 | { 27 | public function __construct( 28 | private ReflectionTypeResolver $reflectionTypeResolver, 29 | private TypeContextFactory $typeContextFactory, 30 | ) { 31 | } 32 | 33 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type 34 | { 35 | if (!$subject instanceof \ReflectionFunctionAbstract) { 36 | throw new UnsupportedException(\sprintf('Expected subject to be a "ReflectionFunctionAbstract", "%s" given.', get_debug_type($subject)), $subject); 37 | } 38 | 39 | $typeContext ??= $this->typeContextFactory->createFromReflection($subject); 40 | 41 | try { 42 | return $this->reflectionTypeResolver->resolve($subject->getReturnType(), $typeContext); 43 | } catch (UnsupportedException $e) { 44 | $path = null !== $typeContext 45 | ? \sprintf('%s::%s()', $typeContext->calledClassName, $subject->getName()) 46 | : \sprintf('%s()', $subject->getName()); 47 | 48 | throw new UnsupportedException(\sprintf('Cannot resolve type for "%s".', $path), $subject, previous: $e); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TypeResolver/ReflectionTypeResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 15 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 16 | use Symfony\Component\TypeInfo\Type; 17 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 18 | use Symfony\Component\TypeInfo\TypeIdentifier; 19 | 20 | /** 21 | * Resolves type for a given type reflection. 22 | * 23 | * @author Mathias Arlaud 24 | * @author Baptiste Leduc 25 | */ 26 | final class ReflectionTypeResolver implements TypeResolverInterface 27 | { 28 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type 29 | { 30 | if ($subject instanceof \ReflectionUnionType) { 31 | return Type::union(...array_map(fn (mixed $t): Type => $this->resolve($t, $typeContext), $subject->getTypes())); 32 | } 33 | 34 | if ($subject instanceof \ReflectionIntersectionType) { 35 | return Type::intersection(...array_map(fn (mixed $t): Type => $this->resolve($t, $typeContext), $subject->getTypes())); 36 | } 37 | 38 | if (!$subject instanceof \ReflectionNamedType) { 39 | throw new UnsupportedException(\sprintf('Expected subject to be a "ReflectionNamedType", a "ReflectionUnionType" or a "ReflectionIntersectionType", "%s" given.', get_debug_type($subject)), $subject); 40 | } 41 | 42 | $identifier = $subject->getName(); 43 | $nullable = $subject->allowsNull(); 44 | 45 | if (TypeIdentifier::ARRAY->value === $identifier) { 46 | $type = Type::array(); 47 | 48 | return $nullable ? Type::nullable($type) : $type; 49 | } 50 | 51 | if (TypeIdentifier::ITERABLE->value === $identifier) { 52 | $type = Type::iterable(); 53 | 54 | return $nullable ? Type::nullable($type) : $type; 55 | } 56 | 57 | if (TypeIdentifier::NULL->value === $identifier || TypeIdentifier::MIXED->value === $identifier) { 58 | return Type::builtin($identifier); 59 | } 60 | 61 | if ($subject->isBuiltin()) { 62 | $type = Type::builtin(TypeIdentifier::from($identifier)); 63 | 64 | return $nullable ? Type::nullable($type) : $type; 65 | } 66 | 67 | if (\in_array(strtolower($identifier), ['self', 'static', 'parent'], true) && !$typeContext) { 68 | throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "%s".', TypeContext::class, strtolower($identifier))); 69 | } 70 | 71 | /** @var class-string $className */ 72 | $className = match (true) { 73 | 'self' === strtolower($identifier) => $typeContext->getDeclaringClass(), 74 | 'static' === strtolower($identifier) => $typeContext->getCalledClass(), 75 | 'parent' === strtolower($identifier) => $typeContext->getParentClass(), 76 | default => $identifier, 77 | }; 78 | 79 | if (is_subclass_of($className, \UnitEnum::class)) { 80 | $type = Type::enum($className); 81 | } else { 82 | $type = Type::object($className); 83 | } 84 | 85 | return $nullable ? Type::nullable($type) : $type; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /TypeResolver/StringTypeResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; 15 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; 16 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; 17 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; 18 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; 19 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; 20 | use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode; 21 | use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; 22 | use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; 23 | use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; 24 | use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; 25 | use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; 26 | use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; 27 | use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; 28 | use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; 29 | use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; 30 | use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; 31 | use PHPStan\PhpDocParser\Ast\Type\TypeNode; 32 | use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; 33 | use PHPStan\PhpDocParser\Lexer\Lexer; 34 | use PHPStan\PhpDocParser\Parser\ConstExprParser; 35 | use PHPStan\PhpDocParser\Parser\TokenIterator; 36 | use PHPStan\PhpDocParser\Parser\TypeParser; 37 | use PHPStan\PhpDocParser\ParserConfig; 38 | use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; 39 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 40 | use Symfony\Component\TypeInfo\Type; 41 | use Symfony\Component\TypeInfo\Type\BackedEnumType; 42 | use Symfony\Component\TypeInfo\Type\BuiltinType; 43 | use Symfony\Component\TypeInfo\Type\CollectionType; 44 | use Symfony\Component\TypeInfo\Type\GenericType; 45 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 46 | use Symfony\Component\TypeInfo\TypeIdentifier; 47 | 48 | /** 49 | * Resolves type for a given string. 50 | * 51 | * @author Mathias Arlaud 52 | * @author Baptiste Leduc 53 | */ 54 | final class StringTypeResolver implements TypeResolverInterface 55 | { 56 | /** 57 | * @var array 58 | */ 59 | private static array $classExistCache = []; 60 | 61 | private readonly Lexer $lexer; 62 | private readonly TypeParser $parser; 63 | 64 | public function __construct(?Lexer $lexer = null, ?TypeParser $parser = null) 65 | { 66 | if (class_exists(ParserConfig::class)) { 67 | $this->lexer = $lexer ?? new Lexer(new ParserConfig([])); 68 | $this->parser = $parser ?? new TypeParser($config = new ParserConfig([]), new ConstExprParser($config)); 69 | } else { 70 | $this->lexer = $lexer ?? new Lexer(); 71 | $this->parser = $parser ?? new TypeParser(new ConstExprParser()); 72 | } 73 | } 74 | 75 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type 76 | { 77 | if ($subject instanceof \Stringable) { 78 | $subject = (string) $subject; 79 | } elseif (!\is_string($subject)) { 80 | throw new UnsupportedException(\sprintf('Expected subject to be a "string", "%s" given.', get_debug_type($subject)), $subject); 81 | } 82 | 83 | try { 84 | $tokens = new TokenIterator($this->lexer->tokenize($subject)); 85 | $node = $this->parser->parse($tokens); 86 | 87 | return $this->getTypeFromNode($node, $typeContext); 88 | } catch (\DomainException $e) { 89 | throw new UnsupportedException(\sprintf('Cannot resolve "%s".', $subject), $subject, previous: $e); 90 | } 91 | } 92 | 93 | private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Type 94 | { 95 | $typeIsCollectionObject = fn (Type $type): bool => $type->isIdentifiedBy(\Traversable::class) || $type->isIdentifiedBy(\ArrayAccess::class); 96 | 97 | if ($node instanceof CallableTypeNode) { 98 | return Type::callable(); 99 | } 100 | 101 | if ($node instanceof ArrayTypeNode) { 102 | return Type::list($this->getTypeFromNode($node->type, $typeContext)); 103 | } 104 | 105 | if ($node instanceof ArrayShapeNode) { 106 | $shape = []; 107 | foreach ($node->items as $item) { 108 | $shape[(string) $item->keyName] = [ 109 | 'type' => $this->getTypeFromNode($item->valueType, $typeContext), 110 | 'optional' => $item->optional, 111 | ]; 112 | } 113 | 114 | return Type::arrayShape( 115 | $shape, 116 | $node->sealed, 117 | $node->unsealedType?->keyType ? $this->getTypeFromNode($node->unsealedType->keyType, $typeContext) : null, 118 | $node->unsealedType?->valueType ? $this->getTypeFromNode($node->unsealedType->valueType, $typeContext) : null, 119 | ); 120 | } 121 | 122 | if ($node instanceof ObjectShapeNode) { 123 | return Type::object(); 124 | } 125 | 126 | if ($node instanceof ThisTypeNode) { 127 | if (null === $typeContext) { 128 | throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "$this".', TypeContext::class)); 129 | } 130 | 131 | return Type::object($typeContext->getCalledClass()); 132 | } 133 | 134 | if ($node instanceof ConstTypeNode) { 135 | return match ($node->constExpr::class) { 136 | ConstExprArrayNode::class => Type::array(), 137 | ConstExprFalseNode::class => Type::false(), 138 | ConstExprFloatNode::class => Type::float(), 139 | ConstExprIntegerNode::class => Type::int(), 140 | ConstExprNullNode::class => Type::null(), 141 | ConstExprStringNode::class => Type::string(), 142 | ConstExprTrueNode::class => Type::true(), 143 | default => throw new \DomainException(\sprintf('Unhandled "%s" constant expression.', $node->constExpr::class)), 144 | }; 145 | } 146 | 147 | if ($node instanceof IdentifierTypeNode) { 148 | $type = match ($node->name) { 149 | 'bool', 'boolean' => Type::bool(), 150 | 'true' => Type::true(), 151 | 'false' => Type::false(), 152 | 'int', 'integer', 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int' => Type::int(), 153 | 'float', 'double' => Type::float(), 154 | 'string', 155 | 'class-string', 156 | 'trait-string', 157 | 'interface-string', 158 | 'callable-string', 159 | 'numeric-string', 160 | 'lowercase-string', 161 | 'non-empty-lowercase-string', 162 | 'non-empty-string', 163 | 'non-falsy-string', 164 | 'truthy-string', 165 | 'literal-string', 166 | 'html-escaped-string' => Type::string(), 167 | 'resource' => Type::resource(), 168 | 'object' => Type::object(), 169 | 'callable' => Type::callable(), 170 | 'array', 'non-empty-array' => Type::array(), 171 | 'list', 'non-empty-list' => Type::list(), 172 | 'iterable' => Type::iterable(), 173 | 'mixed' => Type::mixed(), 174 | 'null' => Type::null(), 175 | 'array-key' => Type::arrayKey(), 176 | 'scalar' => Type::union(Type::int(), Type::float(), Type::string(), Type::bool()), 177 | 'number' => Type::union(Type::int(), Type::float()), 178 | 'numeric' => Type::union(Type::int(), Type::float(), Type::string()), 179 | 'self' => $typeContext ? Type::object($typeContext->getDeclaringClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "self".', TypeContext::class)), 180 | 'static' => $typeContext ? Type::object($typeContext->getCalledClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "static".', TypeContext::class)), 181 | 'parent' => $typeContext ? Type::object($typeContext->getParentClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "parent".', TypeContext::class)), 182 | 'void' => Type::void(), 183 | 'never', 'never-return', 'never-returns', 'no-return' => Type::never(), 184 | default => $this->resolveCustomIdentifier($node->name, $typeContext), 185 | }; 186 | 187 | if ($typeIsCollectionObject($type)) { 188 | return Type::collection($type); 189 | } 190 | 191 | return $type; 192 | } 193 | 194 | if ($node instanceof NullableTypeNode) { 195 | return Type::nullable($this->getTypeFromNode($node->type, $typeContext)); 196 | } 197 | 198 | if ($node instanceof GenericTypeNode) { 199 | if ($node->type instanceof IdentifierTypeNode && 'value-of' === $node->type->name) { 200 | $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext); 201 | if ($type instanceof BackedEnumType) { 202 | return $type->getBackingType(); 203 | } 204 | 205 | if ($type instanceof CollectionType) { 206 | return $type->getCollectionValueType(); 207 | } 208 | 209 | throw new \DomainException(\sprintf('"%s" is not a valid type for "value-of".', $node->genericTypes[0])); 210 | } 211 | 212 | if ($node->type instanceof IdentifierTypeNode && 'key-of' === $node->type->name) { 213 | $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext); 214 | if ($type instanceof CollectionType) { 215 | return $type->getCollectionKeyType(); 216 | } 217 | 218 | throw new \DomainException(\sprintf('"%s" is not a valid type for "key-of".', $node->genericTypes[0])); 219 | } 220 | 221 | $type = $this->getTypeFromNode($node->type, $typeContext); 222 | 223 | // handle integer ranges as simple integers 224 | if ($type->isIdentifiedBy(TypeIdentifier::INT)) { 225 | return $type; 226 | } 227 | 228 | $variableTypes = array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->genericTypes); 229 | 230 | if ($type instanceof CollectionType) { 231 | $asList = $type->isList(); 232 | $keyType = $type->getCollectionKeyType(); 233 | $type = $type->getWrappedType(); 234 | 235 | if ($type instanceof GenericType) { 236 | $type = $type->getWrappedType(); 237 | } 238 | 239 | if (1 === \count($variableTypes)) { 240 | return new CollectionType(Type::generic($type, $keyType, $variableTypes[0]), $asList); 241 | } elseif (2 === \count($variableTypes)) { 242 | return Type::collection($type, $variableTypes[1], $variableTypes[0], $asList); 243 | } 244 | } 245 | 246 | if ($typeIsCollectionObject($type)) { 247 | return match (\count($variableTypes)) { 248 | 1 => Type::collection($type, $variableTypes[0]), 249 | 2 => Type::collection($type, $variableTypes[1], $variableTypes[0]), 250 | default => Type::collection($type), 251 | }; 252 | } 253 | 254 | if ($type instanceof BuiltinType && TypeIdentifier::ARRAY !== $type->getTypeIdentifier() && TypeIdentifier::ITERABLE !== $type->getTypeIdentifier()) { 255 | return $type; 256 | } 257 | 258 | return Type::generic($type, ...$variableTypes); 259 | } 260 | 261 | if ($node instanceof UnionTypeNode) { 262 | $types = []; 263 | 264 | foreach ($node->types as $nodeType) { 265 | $type = $this->getTypeFromNode($nodeType, $typeContext); 266 | 267 | if ($type instanceof BuiltinType && TypeIdentifier::MIXED === $type->getTypeIdentifier()) { 268 | return Type::mixed(); 269 | } 270 | 271 | $types[] = $type; 272 | } 273 | 274 | return Type::union(...$types); 275 | } 276 | 277 | if ($node instanceof IntersectionTypeNode) { 278 | return Type::intersection(...array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->types)); 279 | } 280 | 281 | throw new \DomainException(\sprintf('Unhandled "%s" node.', $node::class)); 282 | } 283 | 284 | private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeContext): Type 285 | { 286 | $className = $typeContext ? $typeContext->normalize($identifier) : $identifier; 287 | 288 | if (!isset(self::$classExistCache[$className])) { 289 | self::$classExistCache[$className] = false; 290 | 291 | if (class_exists($className) || interface_exists($className)) { 292 | self::$classExistCache[$className] = true; 293 | } else { 294 | try { 295 | new \ReflectionClass($className); 296 | self::$classExistCache[$className] = true; 297 | } catch (\Throwable) { 298 | } 299 | } 300 | } 301 | 302 | if (self::$classExistCache[$className]) { 303 | if (is_subclass_of($className, \UnitEnum::class)) { 304 | return Type::enum($className); 305 | } 306 | 307 | return Type::object($className); 308 | } 309 | 310 | if (isset($typeContext?->templates[$identifier])) { 311 | return Type::template($identifier, $typeContext->templates[$identifier]); 312 | } 313 | 314 | if (isset($typeContext?->typeAliases[$identifier])) { 315 | return $typeContext->typeAliases[$identifier]; 316 | } 317 | 318 | throw new \DomainException(\sprintf('Unhandled "%s" identifier.', $identifier)); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /TypeResolver/TypeResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use PHPStan\PhpDocParser\Parser\PhpDocParser; 15 | use Psr\Container\ContainerInterface; 16 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 17 | use Symfony\Component\TypeInfo\Type; 18 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 19 | use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; 20 | 21 | /** 22 | * Resolves type for a given subject by delegating resolving to nested type resolvers. 23 | * 24 | * @author Mathias Arlaud 25 | * @author Baptiste Leduc 26 | */ 27 | final readonly class TypeResolver implements TypeResolverInterface 28 | { 29 | /** 30 | * @param ContainerInterface $resolvers Locator of type resolvers, keyed by supported subject type 31 | */ 32 | public function __construct( 33 | private ContainerInterface $resolvers, 34 | ) { 35 | } 36 | 37 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type 38 | { 39 | $subjectType = match (\is_object($subject)) { 40 | true => match (true) { 41 | is_subclass_of($subject::class, \ReflectionType::class) => \ReflectionType::class, 42 | is_subclass_of($subject::class, \ReflectionFunctionAbstract::class) => \ReflectionFunctionAbstract::class, 43 | default => $subject::class, 44 | }, 45 | false => get_debug_type($subject), 46 | }; 47 | 48 | if (!$this->resolvers->has($subjectType)) { 49 | if ('string' === $subjectType) { 50 | throw new UnsupportedException('Cannot find any resolver for "string" type. Try running "composer require phpstan/phpdoc-parser".', $subject); 51 | } 52 | 53 | throw new UnsupportedException(\sprintf('Cannot find any resolver for "%s" type.', $subjectType), $subject); 54 | } 55 | 56 | /** @param TypeResolverInterface $resolver */ 57 | $resolver = $this->resolvers->get($subjectType); 58 | 59 | return $resolver->resolve($subject, $typeContext); 60 | } 61 | 62 | /** 63 | * @param array|null $resolvers 64 | */ 65 | public static function create(?array $resolvers = null): self 66 | { 67 | if (null === $resolvers) { 68 | $stringTypeResolver = class_exists(PhpDocParser::class) ? new StringTypeResolver() : null; 69 | $typeContextFactory = new TypeContextFactory($stringTypeResolver); 70 | $reflectionTypeResolver = new ReflectionTypeResolver(); 71 | 72 | $resolvers = [ 73 | \ReflectionType::class => $reflectionTypeResolver, 74 | \ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory), 75 | \ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory), 76 | \ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory), 77 | ]; 78 | 79 | if (null !== $stringTypeResolver) { 80 | $resolvers['string'] = $stringTypeResolver; 81 | $resolvers[\ReflectionParameter::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionParameter::class], $stringTypeResolver, $typeContextFactory); 82 | $resolvers[\ReflectionProperty::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionProperty::class], $stringTypeResolver, $typeContextFactory); 83 | $resolvers[\ReflectionFunctionAbstract::class] = new PhpDocAwareReflectionTypeResolver($resolvers[\ReflectionFunctionAbstract::class], $stringTypeResolver, $typeContextFactory); 84 | } 85 | } 86 | 87 | $resolversContainer = new class($resolvers) implements ContainerInterface { 88 | public function __construct( 89 | private readonly array $resolvers, 90 | ) { 91 | } 92 | 93 | public function has(string $id): bool 94 | { 95 | return isset($this->resolvers[$id]); 96 | } 97 | 98 | public function get(string $id): TypeResolverInterface 99 | { 100 | return $this->resolvers[$id]; 101 | } 102 | }; 103 | 104 | return new self($resolversContainer); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /TypeResolver/TypeResolverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\TypeInfo\TypeResolver; 13 | 14 | use Symfony\Component\TypeInfo\Exception\UnsupportedException; 15 | use Symfony\Component\TypeInfo\Type; 16 | use Symfony\Component\TypeInfo\TypeContext\TypeContext; 17 | 18 | /** 19 | * Resolves type for a given subject. 20 | * 21 | * @author Mathias Arlaud 22 | * @author Baptiste Leduc 23 | */ 24 | interface TypeResolverInterface 25 | { 26 | /** 27 | * Try to resolve a {@see Type} on a $subject. 28 | * If the resolver cannot resolve the type, it will throw a {@see UnsupportedException}. 29 | * 30 | * @throws UnsupportedException 31 | */ 32 | public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type; 33 | } 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/type-info", 3 | "type": "library", 4 | "description": "Extracts PHP types information.", 5 | "keywords": [ 6 | "type", 7 | "phpdoc", 8 | "phpstan", 9 | "symfony" 10 | ], 11 | "homepage": "https://symfony.com", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Mathias Arlaud", 16 | "email": "mathias.arlaud@gmail.com" 17 | }, 18 | { 19 | "name": "Baptiste LEDUC", 20 | "email": "baptiste.leduc@gmail.com" 21 | }, 22 | { 23 | "name": "Symfony Community", 24 | "homepage": "https://symfony.com/contributors" 25 | } 26 | ], 27 | "require": { 28 | "php": ">=8.2", 29 | "psr/container": "^1.1|^2.0", 30 | "symfony/deprecation-contracts": "^2.5|^3" 31 | }, 32 | "require-dev": { 33 | "phpstan/phpdoc-parser": "^1.30|^2.0" 34 | }, 35 | "conflict": { 36 | "phpstan/phpdoc-parser": "<1.30" 37 | }, 38 | "autoload": { 39 | "psr-4": { "Symfony\\Component\\TypeInfo\\": "" }, 40 | "exclude-from-classmap": [ 41 | "/Tests/" 42 | ] 43 | }, 44 | "minimum-stability": "dev" 45 | } 46 | --------------------------------------------------------------------------------