diff --git a/composer.json b/composer.json
index 605e94ac8447f..e9b9ae71d3ee3 100644
--- a/composer.json
+++ b/composer.json
@@ -109,6 +109,7 @@
"symfony/translation": "self.version",
"symfony/twig-bridge": "self.version",
"symfony/twig-bundle": "self.version",
+ "symfony/type-info": "self.version",
"symfony/uid": "self.version",
"symfony/validator": "self.version",
"symfony/var-dumper": "self.version",
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 702b4f6109077..81ee3e06e0546 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -44,6 +44,7 @@
use Symfony\Component\Serializer\Encoder\JsonDecode;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
+use Symfony\Component\TypeInfo\Type;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Webhook\Controller\WebhookController;
@@ -164,6 +165,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$this->addAnnotationsSection($rootNode);
$this->addSerializerSection($rootNode, $enableIfStandalone);
$this->addPropertyAccessSection($rootNode, $willBeAvailable);
+ $this->addTypeInfoSection($rootNode, $enableIfStandalone);
$this->addPropertyInfoSection($rootNode, $enableIfStandalone);
$this->addCacheSection($rootNode, $willBeAvailable);
$this->addPhpErrorsSection($rootNode);
@@ -1162,6 +1164,18 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable
;
}
+ private function addTypeInfoSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void
+ {
+ $rootNode
+ ->children()
+ ->arrayNode('type_info')
+ ->info('Type info configuration')
+ ->{$enableIfStandalone('symfony/type-info', Type::class)}()
+ ->end()
+ ->end()
+ ;
+ }
+
private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable): void
{
$rootNode
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 9c9446fb03ce0..933c141e10fed 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -53,6 +53,7 @@
use Symfony\Component\Console\Debug\CliRequest;
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
use Symfony\Component\DependencyInjection\Alias;
+use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -167,6 +168,8 @@
use Symfony\Component\Translation\LocaleSwitcher;
use Symfony\Component\Translation\PseudoLocalizationTranslator;
use Symfony\Component\Translation\Translator;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
@@ -388,6 +391,10 @@ public function load(array $configs, ContainerBuilder $container): void
$container->removeDefinition('console.command.serializer_debug');
}
+ if ($this->readConfigEnabled('type_info', $container, $config['type_info'])) {
+ $this->registerTypeInfoConfiguration($container, $loader);
+ }
+
if ($propertyInfoEnabled) {
$this->registerPropertyInfoConfiguration($container, $loader);
}
@@ -1953,6 +1960,25 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container,
}
}
+ private function registerTypeInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void
+ {
+ if (!class_exists(Type::class)) {
+ throw new LogicException('TypeInfo support cannot be enabled as the TypeInfo component is not installed. Try running "composer require symfony/type-info".');
+ }
+
+ $loader->load('type_info.php');
+
+ if (ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/type-info'])) {
+ $container->register('type_info.resolver.string', StringTypeResolver::class);
+
+ /** @var ServiceLocatorArgument $resolversLocator */
+ $resolversLocator = $container->getDefinition('type_info.resolver')->getArgument(0);
+ $resolversLocator->setValues($resolversLocator->getValues() + [
+ 'string' => new Reference('type_info.resolver.string'),
+ ]);
+ }
+ }
+
private function registerLockConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
{
$loader->load('lock.php');
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index 9279eaf68755b..6ebcfedbf7ad8 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -27,6 +27,7 @@
+
@@ -327,6 +328,10 @@
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/type_info.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/type_info.php
new file mode 100644
index 0000000000000..71e3646a1e041
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/type_info.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionParameterTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionPropertyTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionReturnTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
+
+return static function (ContainerConfigurator $container) {
+ $container->services()
+ // type context
+ ->set('type_info.type_context_factory', TypeContextFactory::class)
+ ->args([service('type_info.resolver.string')->nullOnInvalid()])
+
+ // type resolvers
+ ->set('type_info.resolver', TypeResolver::class)
+ ->args([service_locator([
+ \ReflectionType::class => service('type_info.resolver.reflection_type'),
+ \ReflectionParameter::class => service('type_info.resolver.reflection_parameter'),
+ \ReflectionProperty::class => service('type_info.resolver.reflection_property'),
+ \ReflectionFunctionAbstract::class => service('type_info.resolver.reflection_return'),
+ ])])
+ ->alias(TypeResolverInterface::class, 'type_info.resolver')
+
+ ->set('type_info.resolver.reflection_type', ReflectionTypeResolver::class)
+ ->args([service('type_info.type_context_factory')])
+
+ ->set('type_info.resolver.reflection_parameter', ReflectionParameterTypeResolver::class)
+ ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')])
+
+ ->set('type_info.resolver.reflection_property', ReflectionPropertyTypeResolver::class)
+ ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')])
+
+ ->set('type_info.resolver.reflection_return', ReflectionReturnTypeResolver::class)
+ ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')])
+ ;
+};
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index d56cfa90d7f48..5a4005bbf2722 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -28,6 +28,7 @@
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory;
use Symfony\Component\Serializer\Encoder\JsonDecode;
+use Symfony\Component\TypeInfo\Type;
use Symfony\Component\Uid\Factory\UuidFactory;
class ConfigurationTest extends TestCase
@@ -624,6 +625,9 @@ protected static function getBundleDefaultConfig()
'throw_exception_on_invalid_index' => false,
'throw_exception_on_invalid_property_path' => true,
],
+ 'type_info' => [
+ 'enabled' => !class_exists(FullStack::class) && class_exists(Type::class),
+ ],
'property_info' => [
'enabled' => !class_exists(FullStack::class),
],
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php
index b5d8061e4d0af..4fbf72a9f6eea 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php
@@ -70,6 +70,7 @@
'default_context' => ['enable_max_depth' => true],
],
'property_info' => true,
+ 'type_info' => true,
'ide' => 'file%%link%%format',
'request' => [
'formats' => [
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/type_info.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/type_info.php
new file mode 100644
index 0000000000000..0e7dcbae0e1da
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/type_info.php
@@ -0,0 +1,11 @@
+loadFromExtension('framework', [
+ 'annotations' => false,
+ 'http_method_override' => false,
+ 'handle_all_throwables' => true,
+ 'php_errors' => ['log' => true],
+ 'type_info' => [
+ 'enabled' => true,
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml
index 92e4405a003fd..fd5d52e1c5de5 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml
@@ -40,5 +40,6 @@
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/type_info.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/type_info.xml
new file mode 100644
index 0000000000000..0fe4d525d1d5c
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/type_info.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml
index 883e9d6c20ebb..96001f1d2dc88 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml
@@ -59,6 +59,7 @@ framework:
max_depth_handler: my.max.depth.handler
default_context:
enable_max_depth: true
+ type_info: ~
property_info: ~
ide: file%%link%%format
request:
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/type_info.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/type_info.yml
new file mode 100644
index 0000000000000..4d6b405b28821
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/type_info.yml
@@ -0,0 +1,8 @@
+framework:
+ annotations: false
+ http_method_override: false
+ handle_all_throwables: true
+ php_errors:
+ log: true
+ type_info:
+ enabled: true
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
index 0d97dcf62fae3..8c4711c641c91 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
@@ -1616,6 +1616,12 @@ public function testSerializerServiceIsNotRegisteredWhenDisabled()
$this->assertFalse($container->hasDefinition('serializer'));
}
+ public function testTypeInfoEnabled()
+ {
+ $container = $this->createContainerFromFile('type_info');
+ $this->assertTrue($container->has('type_info.resolver'));
+ }
+
public function testPropertyInfoEnabled()
{
$container = $this->createContainerFromFile('property_info');
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TypeInfoTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TypeInfoTest.php
new file mode 100644
index 0000000000000..6acdb9c814548
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TypeInfoTest.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
+
+use PHPStan\PhpDocParser\Parser\PhpDocParser;
+use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\TypeInfo\Dummy;
+use Symfony\Component\TypeInfo\Type;
+
+class TypeInfoTest extends AbstractWebTestCase
+{
+ public function testComponent()
+ {
+ static::bootKernel(['test_case' => 'TypeInfo']);
+
+ $this->assertEquals(Type::string(), static::getContainer()->get('type_info.resolver')->resolve(new \ReflectionProperty(Dummy::class, 'name')));
+
+ if (!class_exists(PhpDocParser::class)) {
+ $this->markTestSkipped('"phpstan/phpdoc-parser" dependency is required.');
+ }
+
+ $this->assertEquals(Type::int(), static::getContainer()->get('type_info.resolver')->resolve('int'));
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/Dummy.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/Dummy.php
new file mode 100644
index 0000000000000..0f517df5139d0
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/Dummy.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\TypeInfo;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class Dummy
+{
+ public string $name;
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/bundles.php
new file mode 100644
index 0000000000000..15ff182c6fed5
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/bundles.php
@@ -0,0 +1,18 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
+use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle;
+
+return [
+ new FrameworkBundle(),
+ new TestBundle(),
+];
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/config.yml
new file mode 100644
index 0000000000000..35c7bb4c46c09
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TypeInfo/config.yml
@@ -0,0 +1,11 @@
+imports:
+ - { resource: ../config/default.yml }
+
+framework:
+ http_method_override: false
+ type_info: true
+
+services:
+ type_info.resolver.alias:
+ alias: type_info.resolver
+ public: true
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 9a50605a21071..766bc80641e09 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -64,6 +64,7 @@
"symfony/string": "^6.4|^7.0",
"symfony/translation": "^6.4|^7.0",
"symfony/twig-bundle": "^6.4|^7.0",
+ "symfony/type-info": "^7.1",
"symfony/validator": "^6.4|^7.0",
"symfony/workflow": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0",
diff --git a/src/Symfony/Component/TypeInfo/.gitattributes b/src/Symfony/Component/TypeInfo/.gitattributes
new file mode 100644
index 0000000000000..84c7add058fb5
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/.gitattributes
@@ -0,0 +1,4 @@
+/Tests export-ignore
+/phpunit.xml.dist export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
diff --git a/src/Symfony/Component/TypeInfo/.gitignore b/src/Symfony/Component/TypeInfo/.gitignore
new file mode 100644
index 0000000000000..c49a5d8df5c65
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/.gitignore
@@ -0,0 +1,3 @@
+vendor/
+composer.lock
+phpunit.xml
diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md
new file mode 100644
index 0000000000000..5f941ae21a99a
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md
@@ -0,0 +1,7 @@
+CHANGELOG
+=========
+
+7.1
+---
+
+ * Add the component
diff --git a/src/Symfony/Component/TypeInfo/Exception/ExceptionInterface.php b/src/Symfony/Component/TypeInfo/Exception/ExceptionInterface.php
new file mode 100644
index 0000000000000..fee0c3bd94978
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Exception/ExceptionInterface.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Exception;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+interface ExceptionInterface extends \Throwable
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/Exception/InvalidArgumentException.php b/src/Symfony/Component/TypeInfo/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000000000..8baae82917683
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Exception/InvalidArgumentException.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Exception;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/Exception/LogicException.php b/src/Symfony/Component/TypeInfo/Exception/LogicException.php
new file mode 100644
index 0000000000000..06be3c9eb1653
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Exception/LogicException.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Exception;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class LogicException extends \LogicException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/Exception/RuntimeException.php b/src/Symfony/Component/TypeInfo/Exception/RuntimeException.php
new file mode 100644
index 0000000000000..143e18ef4ace0
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Exception/RuntimeException.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Exception;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/Exception/UnsupportedException.php b/src/Symfony/Component/TypeInfo/Exception/UnsupportedException.php
new file mode 100644
index 0000000000000..2c2e45f091a4b
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Exception/UnsupportedException.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Exception;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class UnsupportedException extends \LogicException implements ExceptionInterface
+{
+ public function __construct(
+ string $message,
+ public readonly mixed $subject,
+ int $code = 0,
+ \Throwable $previous = null,
+ ) {
+ parent::__construct($message, $code, $previous);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/LICENSE b/src/Symfony/Component/TypeInfo/LICENSE
new file mode 100644
index 0000000000000..e374a5c8339d3
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2024-present Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/Symfony/Component/TypeInfo/README.md b/src/Symfony/Component/TypeInfo/README.md
new file mode 100644
index 0000000000000..6643ed41dd325
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/README.md
@@ -0,0 +1,42 @@
+TypeInfo Component
+==================
+
+The TypeInfo component extracts PHP types information.
+
+Getting Started
+---------------
+
+```bash
+composer require symfony/type-info
+composer require phpstan/phpdoc-parser # to support raw string resolving
+```
+
+```php
+resolve(new \ReflectionProperty(Dummy::class, 'id')); // returns an "int" Type instance
+$typeResolver->resolve('bool'); // returns a "bool" Type instance
+
+// Types can be instantiated thanks to static factories
+$type = Type::list(Type::nullable(Type::bool()));
+
+// Type instances have several helper methods
+$type->getBaseType() // returns an "array" Type instance
+$type->getCollectionKeyType(); // returns an "int" Type instance
+$type->getCollectionValueType()->isNullable(); // returns true
+```
+
+Resources
+---------
+
+ * [Contributing](https://symfony.com/doc/current/contributing/index.html)
+ * [Report issues](https://github.com/symfony/symfony/issues) and
+ [send Pull Requests](https://github.com/symfony/symfony/pulls)
+ in the [main Symfony repository](https://github.com/symfony/symfony)
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/AbstractDummy.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/AbstractDummy.php
new file mode 100644
index 0000000000000..9dd5a2dc28b54
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/AbstractDummy.php
@@ -0,0 +1,7 @@
+id;
+ }
+
+ public function setId(int $id): void
+ {
+ $this->id = $id;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyBackedEnum.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyBackedEnum.php
new file mode 100644
index 0000000000000..2348415910314
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyBackedEnum.php
@@ -0,0 +1,9 @@
+price : $this->price / 100;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUses.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUses.php
new file mode 100644
index 0000000000000..58517a4bd0428
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyWithUses.php
@@ -0,0 +1,22 @@
+createdAt = $createdAt;
+ }
+
+ public function getType(): Type
+ {
+ throw new \LogicException('Should not be called.');
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php
new file mode 100644
index 0000000000000..46979ce822361
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php
@@ -0,0 +1,79 @@
+builtin;
+ }
+
+ public function getSelf(): self
+ {
+ return $this->self;
+ }
+
+ public function getStatic(): static
+ {
+ return $this;
+ }
+
+ public function getNullableStatic(): ?static
+ {
+ return null;
+ }
+
+ public function getNothing()
+ {
+ return $this->nothing;
+ }
+
+ public function setBuiltin(int $builtin): void
+ {
+ $this->builtin = $builtin;
+ }
+
+ public function setSelf(self $self): void
+ {
+ $this->self = $self;
+ }
+
+ public function setNothing($nothing): void
+ {
+ $this->nothing = $nothing;
+ }
+
+ public function setOptional(int $optional = null): void
+ {
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php
new file mode 100644
index 0000000000000..be3fb65678e3e
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BackedEnumType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class BackedEnumTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int()));
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isNullable());
+ }
+
+ public function testAsNonNullable()
+ {
+ $type = new BackedEnumType(DummyBackedEnum::class, Type::int());
+
+ $this->assertSame($type, $type->asNonNullable());
+ }
+
+ public function testIsA()
+ {
+ $this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::ARRAY));
+ $this->assertTrue((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::OBJECT));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php
new file mode 100644
index 0000000000000..c417e71ec58fa
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php
@@ -0,0 +1,65 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class BuiltinTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $this->assertSame('int', (string) new BuiltinType(TypeIdentifier::INT));
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new BuiltinType(TypeIdentifier::INT))->isNullable());
+ $this->assertTrue((new BuiltinType(TypeIdentifier::NULL))->isNullable());
+ $this->assertTrue((new BuiltinType(TypeIdentifier::MIXED))->isNullable());
+ }
+
+ public function testAsNonNullable()
+ {
+ $type = new BuiltinType(TypeIdentifier::INT);
+
+ $this->assertSame($type, $type->asNonNullable());
+ $this->assertEquals(
+ Type::union(
+ new BuiltinType(TypeIdentifier::OBJECT),
+ new BuiltinType(TypeIdentifier::RESOURCE),
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new BuiltinType(TypeIdentifier::STRING),
+ new BuiltinType(TypeIdentifier::FLOAT),
+ new BuiltinType(TypeIdentifier::INT),
+ new BuiltinType(TypeIdentifier::BOOL),
+ ),
+ Type::nullable(new BuiltinType(TypeIdentifier::MIXED))->asNonNullable()
+ );
+ }
+
+ public function testCannotTurnNullAsNonNullable()
+ {
+ $this->expectException(LogicException::class);
+
+ (new BuiltinType(TypeIdentifier::NULL))->asNonNullable();
+ }
+
+ public function testIsA()
+ {
+ $this->assertFalse((new BuiltinType(TypeIdentifier::INT))->isA(TypeIdentifier::ARRAY));
+ $this->assertTrue((new BuiltinType(TypeIdentifier::INT))->isA(TypeIdentifier::INT));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php
new file mode 100644
index 0000000000000..5faee73d95ea4
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\GenericType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class CollectionTypeTest extends TestCase
+{
+ public function testCanOnlyConstructListWithIntKeyType()
+ {
+ new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::int(), Type::bool()), isList: true);
+ $this->addToAssertionCount(1);
+
+ $this->expectException(InvalidArgumentException::class);
+ new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()), isList: true);
+ }
+
+ public function testIsList()
+ {
+ $type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::bool()));
+ $this->assertFalse($type->isList());
+
+ $type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::bool()), isList: true);
+ $this->assertTrue($type->isList());
+ }
+
+ public function testGetCollectionKeyType()
+ {
+ $type = new CollectionType(Type::builtin(TypeIdentifier::ARRAY));
+ $this->assertEquals(Type::union(Type::int(), Type::string()), $type->getCollectionKeyType());
+
+ $type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::bool()));
+ $this->assertEquals(Type::int(), $type->getCollectionKeyType());
+
+ $type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()));
+ $this->assertEquals(Type::string(), $type->getCollectionKeyType());
+ }
+
+ public function testGetCollectionValueType()
+ {
+ $type = new CollectionType(Type::builtin(TypeIdentifier::ARRAY));
+ $this->assertEquals(Type::mixed(), $type->getCollectionValueType());
+
+ $type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::bool()));
+ $this->assertEquals(Type::bool(), $type->getCollectionValueType());
+
+ $type = new CollectionType(new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()));
+ $this->assertEquals(Type::bool(), $type->getCollectionValueType());
+ }
+
+ public function testToString()
+ {
+ $type = new CollectionType(Type::builtin(TypeIdentifier::ITERABLE));
+ $this->assertEquals('iterable', (string) $type);
+
+ $type = new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::bool()));
+ $this->assertEquals('array', (string) $type);
+
+ $type = new CollectionType(new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()));
+ $this->assertEquals('array', (string) $type);
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::int())))->isNullable());
+ $this->assertTrue((new CollectionType(Type::generic(Type::null(), Type::int())))->isNullable());
+ $this->assertTrue((new CollectionType(Type::generic(Type::mixed(), Type::int())))->isNullable());
+ }
+
+ public function testAsNonNullable()
+ {
+ $type = new CollectionType(Type::builtin(TypeIdentifier::ITERABLE));
+
+ $this->assertSame($type, $type->asNonNullable());
+ }
+
+ public function testIsA()
+ {
+ $type = new CollectionType(new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()));
+
+ $this->assertTrue($type->isA(TypeIdentifier::ARRAY));
+ $this->assertFalse($type->isA(TypeIdentifier::STRING));
+ $this->assertFalse($type->isA(TypeIdentifier::INT));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php
new file mode 100644
index 0000000000000..fdf7bafb3805a
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
+use Symfony\Component\TypeInfo\Type\EnumType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class EnumTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $this->assertSame(DummyEnum::class, (string) new EnumType(DummyEnum::class));
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new EnumType(DummyEnum::class))->isNullable());
+ }
+
+ public function testAsNonNullable()
+ {
+ $type = new EnumType(DummyEnum::class);
+
+ $this->assertSame($type, $type->asNonNullable());
+ }
+
+ public function testIsA()
+ {
+ $this->assertFalse((new EnumType(DummyEnum::class))->isA(TypeIdentifier::ARRAY));
+ $this->assertTrue((new EnumType(DummyEnum::class))->isA(TypeIdentifier::OBJECT));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php
new file mode 100644
index 0000000000000..bdaf7e8654834
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php
@@ -0,0 +1,58 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\GenericType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class GenericTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $type = new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::bool());
+ $this->assertEquals('array', (string) $type);
+
+ $type = new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool());
+ $this->assertEquals('array', (string) $type);
+
+ $type = new GenericType(Type::object(self::class), Type::union(Type::bool(), Type::string()), Type::int(), Type::float());
+ $this->assertEquals(sprintf('%s', self::class), (string) $type);
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::int()))->isNullable());
+ $this->assertTrue((new GenericType(Type::null(), Type::int()))->isNullable());
+ $this->assertTrue((new GenericType(Type::mixed(), Type::int()))->isNullable());
+ }
+
+ public function testAsNonNullable()
+ {
+ $type = new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::int());
+
+ $this->assertSame($type, $type->asNonNullable());
+ }
+
+ public function testIsA()
+ {
+ $type = new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool());
+ $this->assertTrue($type->isA(TypeIdentifier::ARRAY));
+ $this->assertFalse($type->isA(TypeIdentifier::STRING));
+
+ $type = new GenericType(Type::object(self::class), Type::union(Type::bool(), Type::string()), Type::int(), Type::float());
+ $this->assertTrue($type->isA(TypeIdentifier::OBJECT));
+ $this->assertFalse($type->isA(TypeIdentifier::INT));
+ $this->assertFalse($type->isA(TypeIdentifier::STRING));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php
new file mode 100644
index 0000000000000..a6db32f126234
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\IntersectionType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class IntersectionTypeTest extends TestCase
+{
+ public function testCannotCreateWithOnlyOneType()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new IntersectionType(Type::int());
+ }
+
+ public function testCannotCreateWithIntersectionTypeParts()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new IntersectionType(Type::int(), new IntersectionType());
+ }
+
+ public function testSortTypesOnCreation()
+ {
+ $type = new IntersectionType(Type::int(), Type::string(), Type::bool());
+ $this->assertEquals([Type::bool(), Type::int(), Type::string()], $type->getTypes());
+ }
+
+ public function testAtLeastOneTypeIs()
+ {
+ $type = new IntersectionType(Type::int(), Type::string(), Type::bool());
+
+ $this->assertTrue($type->atLeastOneTypeIs(fn (Type $t) => 'int' === (string) $t));
+ $this->assertFalse($type->atLeastOneTypeIs(fn (Type $t) => 'float' === (string) $t));
+ }
+
+ public function testEveryTypeIs()
+ {
+ $type = new IntersectionType(Type::int(), Type::string(), Type::bool());
+ $this->assertTrue($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType));
+
+ $type = new IntersectionType(Type::int(), Type::string(), Type::template('T'));
+ $this->assertFalse($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType));
+ }
+
+ public function testToString()
+ {
+ $type = new IntersectionType(Type::int(), Type::string(), Type::float());
+ $this->assertSame('float&int&string', (string) $type);
+
+ $type = new IntersectionType(Type::int(), Type::string(), Type::union(Type::float(), Type::bool()));
+ $this->assertSame('(bool|float)&int&string', (string) $type);
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new IntersectionType(Type::int(), Type::string(), Type::float()))->isNullable());
+ $this->assertTrue((new IntersectionType(Type::null(), Type::union(Type::int(), Type::mixed())))->isNullable());
+ }
+
+ public function testAsNonNullable()
+ {
+ $type = new IntersectionType(Type::int(), Type::string(), Type::float());
+
+ $this->assertSame($type, $type->asNonNullable());
+ }
+
+ public function testCannotTurnNullIntersectionAsNonNullable()
+ {
+ $this->expectException(LogicException::class);
+
+ $type = (new IntersectionType(Type::null(), Type::mixed()))->asNonNullable();
+ }
+
+ public function testIsA()
+ {
+ $type = new IntersectionType(Type::int(), Type::string(), Type::float());
+ $this->assertFalse($type->isA(TypeIdentifier::ARRAY));
+
+ $type = new IntersectionType(Type::int(), Type::string(), Type::union(Type::float(), Type::bool()));
+ $this->assertFalse($type->isA(TypeIdentifier::INT));
+
+ $type = new IntersectionType(Type::int(), Type::union(Type::int(), Type::int()));
+ $this->assertTrue($type->isA(TypeIdentifier::INT));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php
new file mode 100644
index 0000000000000..c9e3399361a20
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class ObjectTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $this->assertSame(self::class, (string) new ObjectType(self::class));
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new ObjectType(self::class))->isNullable());
+ }
+
+ public function testIsA()
+ {
+ $this->assertFalse((new ObjectType(self::class))->isA(TypeIdentifier::ARRAY));
+ $this->assertTrue((new ObjectType(self::class))->isA(TypeIdentifier::OBJECT));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php
new file mode 100644
index 0000000000000..827793ff71c2e
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php
@@ -0,0 +1,117 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\UnionType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class UnionTypeTest extends TestCase
+{
+ public function testCannotCreateWithOnlyOneType()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new UnionType(Type::int());
+ }
+
+ public function testCannotCreateWithUnionTypeParts()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new UnionType(Type::int(), new UnionType());
+ }
+
+ public function testSortTypesOnCreation()
+ {
+ $type = new UnionType(Type::int(), Type::string(), Type::bool());
+ $this->assertEquals([Type::bool(), Type::int(), Type::string()], $type->getTypes());
+ }
+
+ public function testAsNonNullable()
+ {
+ $type = new UnionType(Type::int(), Type::string(), Type::bool());
+ $this->assertInstanceOf(UnionType::class, $type->asNonNullable());
+ $this->assertEquals([Type::bool(), Type::int(), Type::string()], $type->asNonNullable()->getTypes());
+
+ $type = new UnionType(Type::int(), Type::string(), Type::null());
+ $this->assertInstanceOf(UnionType::class, $type->asNonNullable());
+ $this->assertEquals([Type::int(), Type::string()], $type->asNonNullable()->getTypes());
+
+ $type = new UnionType(Type::int(), Type::null());
+ $this->assertInstanceOf(BuiltinType::class, $type->asNonNullable());
+ $this->assertEquals(Type::int(), $type->asNonNullable());
+
+ $type = new UnionType(Type::int(), Type::object(\stdClass::class), Type::mixed());
+ $this->assertInstanceOf(UnionType::class, $type->asNonNullable());
+ $this->assertEquals([
+ Type::builtin(TypeIdentifier::ARRAY),
+ Type::bool(),
+ Type::float(),
+ Type::int(),
+ Type::object(),
+ Type::resource(),
+ Type::object(\stdClass::class),
+ Type::string(),
+ ], $type->asNonNullable()->getTypes());
+ }
+
+ public function testAtLeastOneTypeIs()
+ {
+ $type = new UnionType(Type::int(), Type::string(), Type::bool());
+
+ $this->assertTrue($type->atLeastOneTypeIs(fn (Type $t) => 'int' === (string) $t));
+ $this->assertFalse($type->atLeastOneTypeIs(fn (Type $t) => 'float' === (string) $t));
+ }
+
+ public function testEveryTypeIs()
+ {
+ $type = new UnionType(Type::int(), Type::string(), Type::bool());
+ $this->assertTrue($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType));
+
+ $type = new UnionType(Type::int(), Type::string(), Type::template('T'));
+ $this->assertFalse($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType));
+ }
+
+ public function testToString()
+ {
+ $type = new UnionType(Type::int(), Type::string(), Type::float());
+ $this->assertSame('float|int|string', (string) $type);
+
+ $type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::float(), Type::bool()));
+ $this->assertSame('(bool&float)|int|string', (string) $type);
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertFalse((new UnionType(Type::int(), Type::intersection(Type::float(), Type::int())))->isNullable());
+ $this->assertTrue((new UnionType(Type::int(), Type::null()))->isNullable());
+ $this->assertTrue((new UnionType(Type::int(), Type::mixed()))->isNullable());
+ }
+
+ public function testIsA()
+ {
+ $type = new UnionType(Type::int(), Type::string(), Type::float());
+ $this->assertFalse($type->isNullable());
+ $this->assertFalse($type->isA(TypeIdentifier::ARRAY));
+
+ $type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::float(), Type::bool()));
+ $this->assertTrue($type->isA(TypeIdentifier::INT));
+ $this->assertTrue($type->isA(TypeIdentifier::STRING));
+ $this->assertFalse($type->isA(TypeIdentifier::FLOAT));
+ $this->assertFalse($type->isA(TypeIdentifier::BOOL));
+
+ $type = new UnionType(Type::string(), Type::intersection(Type::int(), Type::int()));
+ $this->assertTrue($type->isA(TypeIdentifier::INT));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
new file mode 100644
index 0000000000000..1de82676b0334
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.php
@@ -0,0 +1,122 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeContext;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
+
+class TypeContextFactoryTest extends TestCase
+{
+ private TypeContextFactory $typeContextFactory;
+
+ protected function setUp(): void
+ {
+ $this->typeContextFactory = new TypeContextFactory(new StringTypeResolver());
+ }
+
+ public function testCollectClassNames()
+ {
+ $typeContext = $this->typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class);
+ $this->assertSame('Dummy', $typeContext->calledClassName);
+ $this->assertSame('AbstractDummy', $typeContext->declaringClassName);
+
+ $typeContext = $this->typeContextFactory->createFromReflection(new \ReflectionClass(Dummy::class));
+ $this->assertSame('Dummy', $typeContext->calledClassName);
+ $this->assertSame('Dummy', $typeContext->declaringClassName);
+
+ $typeContext = $this->typeContextFactory->createFromReflection(new \ReflectionProperty(Dummy::class, 'id'));
+ $this->assertSame('Dummy', $typeContext->calledClassName);
+ $this->assertSame('Dummy', $typeContext->declaringClassName);
+
+ $typeContext = $this->typeContextFactory->createFromReflection(new \ReflectionMethod(Dummy::class, 'getId'));
+ $this->assertSame('Dummy', $typeContext->calledClassName);
+ $this->assertSame('Dummy', $typeContext->declaringClassName);
+
+ $typeContext = $this->typeContextFactory->createFromReflection(new \ReflectionParameter([Dummy::class, 'setId'], 'id'));
+ $this->assertSame('Dummy', $typeContext->calledClassName);
+ $this->assertSame('Dummy', $typeContext->declaringClassName);
+ }
+
+ public function testCollectNamespace()
+ {
+ $namespace = 'Symfony\\Component\\TypeInfo\\Tests\\Fixtures';
+
+ $this->assertSame($namespace, $this->typeContextFactory->createFromClassName(Dummy::class)->namespace);
+
+ $this->assertEquals($namespace, $this->typeContextFactory->createFromReflection(new \ReflectionClass(Dummy::class))->namespace);
+ $this->assertEquals($namespace, $this->typeContextFactory->createFromReflection(new \ReflectionProperty(Dummy::class, 'id'))->namespace);
+ $this->assertEquals($namespace, $this->typeContextFactory->createFromReflection(new \ReflectionMethod(Dummy::class, 'getId'))->namespace);
+ $this->assertEquals($namespace, $this->typeContextFactory->createFromReflection(new \ReflectionParameter([Dummy::class, 'setId'], 'id'))->namespace);
+ }
+
+ public function testCollectUses()
+ {
+ $this->assertSame([], $this->typeContextFactory->createFromClassName(Dummy::class)->uses);
+
+ $uses = [
+ 'Type' => Type::class,
+ \DateTimeInterface::class => '\\'.\DateTimeInterface::class,
+ 'DateTime' => '\\'.\DateTimeImmutable::class,
+ ];
+
+ $this->assertSame($uses, $this->typeContextFactory->createFromClassName(DummyWithUses::class)->uses);
+
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithUses::class))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithUses::class, 'createdAt'))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithUses::class, 'setCreatedAt'))->uses);
+ $this->assertEquals($uses, $this->typeContextFactory->createFromReflection(new \ReflectionParameter([DummyWithUses::class, 'setCreatedAt'], 'createdAt'))->uses);
+ }
+
+ public function testCollectTemplates()
+ {
+ $this->assertEquals([], $this->typeContextFactory->createFromClassName(Dummy::class)->templates);
+ $this->assertEquals([
+ 'T' => Type::union(Type::int(), Type::string()),
+ 'U' => Type::mixed(),
+ ], $this->typeContextFactory->createFromClassName(DummyWithTemplates::class)->templates);
+
+ $this->assertEquals([
+ 'T' => Type::union(Type::int(), Type::string()),
+ 'U' => Type::mixed(),
+ ], $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTemplates::class))->templates);
+
+ $this->assertEquals([
+ 'T' => Type::union(Type::int(), Type::string()),
+ 'U' => Type::mixed(),
+ ], $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTemplates::class, 'price'))->templates);
+
+ $this->assertEquals([
+ 'T' => Type::union(Type::int(), Type::float()),
+ 'U' => Type::mixed(),
+ 'V' => Type::mixed(),
+ ], $this->typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))->templates);
+
+ $this->assertEquals([
+ 'T' => Type::union(Type::int(), Type::float()),
+ 'U' => Type::mixed(),
+ 'V' => Type::mixed(),
+ ], $this->typeContextFactory->createFromReflection(new \ReflectionParameter([DummyWithTemplates::class, 'getPrice'], 'inCents'))->templates);
+ }
+
+ public function testDoNotCollectTemplatesWhenToStringTypeResolver()
+ {
+ $typeContextFactory = new TypeContextFactory();
+
+ $this->assertEquals([], $typeContextFactory->createFromClassName(DummyWithTemplates::class)->templates);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextTest.php
new file mode 100644
index 0000000000000..e12f0c4b7b83b
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextTest.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeContext;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyExtendingStdClass;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithUses;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+
+class TypeContextTest extends TestCase
+{
+ public function testNormalize()
+ {
+ $typeContext = (new TypeContextFactory())->createFromClassName(DummyWithUses::class);
+
+ $this->assertSame(DummyWithUses::class, $typeContext->normalize('DummyWithUses'));
+ $this->assertSame(Type::class, $typeContext->normalize('Type'));
+ $this->assertSame('\\'.\DateTimeImmutable::class, $typeContext->normalize('DateTime'));
+ $this->assertSame('Symfony\\Component\\TypeInfo\\Tests\\Fixtures\\unknown', $typeContext->normalize('unknown'));
+ $this->assertSame('unknown', $typeContext->normalize('\\unknown'));
+
+ $typeContextWithoutNamespace = new TypeContext('Foo', 'Bar');
+ $this->assertSame('unknown', $typeContextWithoutNamespace->normalize('unknown'));
+ }
+
+ public function testGetDeclaringClass()
+ {
+ $this->assertSame(Dummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class)->getDeclaringClass());
+ $this->assertSame(AbstractDummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class, AbstractDummy::class)->getDeclaringClass());
+ }
+
+ public function testGetCalledClass()
+ {
+ $this->assertSame(Dummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class)->getCalledClass());
+ $this->assertSame(Dummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class, AbstractDummy::class)->getCalledClass());
+ }
+
+ public function testGetParentClass()
+ {
+ $this->assertSame(AbstractDummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class)->getParentClass());
+ $this->assertSame(\stdClass::class, (new TypeContextFactory())->createFromClassName(DummyExtendingStdClass::class)->getParentClass());
+ }
+
+ public function testCannotGetParentClassWhenDoNotInherit()
+ {
+ $this->expectException(LogicException::class);
+ (new TypeContextFactory())->createFromClassName(AbstractDummy::class)->getParentClass();
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php
new file mode 100644
index 0000000000000..1d22d22a9008b
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php
@@ -0,0 +1,208 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BackedEnumType;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\EnumType;
+use Symfony\Component\TypeInfo\Type\GenericType;
+use Symfony\Component\TypeInfo\Type\IntersectionType;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\Type\TemplateType;
+use Symfony\Component\TypeInfo\Type\UnionType;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class TypeFactoryTest extends TestCase
+{
+ public function testCreateBuiltin()
+ {
+ $this->assertEquals(new BuiltinType(TypeIdentifier::INT), Type::builtin(TypeIdentifier::INT));
+ $this->assertEquals(new BuiltinType(TypeIdentifier::INT), Type::builtin('int'));
+ $this->assertEquals(new BuiltinType(TypeIdentifier::INT), Type::int());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::FLOAT), Type::float());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::STRING), Type::string());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::BOOL), Type::bool());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::RESOURCE), Type::resource());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::FALSE), Type::false());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::TRUE), Type::true());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::CALLABLE), Type::callable());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::NULL), Type::null());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::MIXED), Type::mixed());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::VOID), Type::void());
+ $this->assertEquals(new BuiltinType(TypeIdentifier::NEVER), Type::never());
+ }
+
+ public function testCreateArray()
+ {
+ $this->assertEquals(new CollectionType(new BuiltinType(TypeIdentifier::ARRAY)), Type::array());
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)),
+ new BuiltinType(TypeIdentifier::BOOL),
+ )),
+ Type::array(Type::bool()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new BuiltinType(TypeIdentifier::STRING),
+ new BuiltinType(TypeIdentifier::BOOL),
+ )),
+ Type::array(Type::bool(), Type::string()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new BuiltinType(TypeIdentifier::INT),
+ new BuiltinType(TypeIdentifier::BOOL),
+ ), isList: true),
+ Type::array(Type::bool(), Type::int(), true),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new BuiltinType(TypeIdentifier::INT),
+ new BuiltinType(TypeIdentifier::MIXED),
+ ), isList: true),
+ Type::list(),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new BuiltinType(TypeIdentifier::INT),
+ new BuiltinType(TypeIdentifier::BOOL),
+ ), isList: true),
+ Type::list(Type::bool()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new BuiltinType(TypeIdentifier::STRING),
+ new BuiltinType(TypeIdentifier::MIXED),
+ )),
+ Type::dict(),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ARRAY),
+ new BuiltinType(TypeIdentifier::STRING),
+ new BuiltinType(TypeIdentifier::BOOL),
+ )),
+ Type::dict(Type::bool()),
+ );
+ }
+
+ public function testCreateIterable()
+ {
+ $this->assertEquals(new CollectionType(new BuiltinType(TypeIdentifier::ITERABLE)), Type::iterable());
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ITERABLE),
+ new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)),
+ new BuiltinType(TypeIdentifier::BOOL),
+ )),
+ Type::iterable(Type::bool()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ITERABLE),
+ new BuiltinType(TypeIdentifier::STRING),
+ new BuiltinType(TypeIdentifier::BOOL),
+ )),
+ Type::iterable(Type::bool(), Type::string()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(TypeIdentifier::ITERABLE),
+ new BuiltinType(TypeIdentifier::INT),
+ new BuiltinType(TypeIdentifier::BOOL),
+ ), isList: true),
+ Type::iterable(Type::bool(), Type::int(), true),
+ );
+ }
+
+ public function testCreateObject()
+ {
+ $this->assertEquals(new BuiltinType(TypeIdentifier::OBJECT), Type::object());
+ $this->assertEquals(new ObjectType(self::class), Type::object(self::class));
+ }
+
+ public function testCreateEnum()
+ {
+ $this->assertEquals(new EnumType(DummyEnum::class), Type::enum(DummyEnum::class));
+ $this->assertEquals(new BackedEnumType(DummyBackedEnum::class, new BuiltinType(TypeIdentifier::STRING)), Type::enum(DummyBackedEnum::class));
+ $this->assertEquals(
+ new BackedEnumType(DummyBackedEnum::class, new BuiltinType(TypeIdentifier::INT)),
+ Type::enum(DummyBackedEnum::class, new BuiltinType(TypeIdentifier::INT)),
+ );
+ }
+
+ public function testCreateGeneric()
+ {
+ $this->assertEquals(
+ new GenericType(new ObjectType(self::class), new BuiltinType(TypeIdentifier::INT)),
+ Type::generic(Type::object(self::class), Type::int()),
+ );
+ }
+
+ public function testCreateTemplate()
+ {
+ $this->assertEquals(new TemplateType('T', new BuiltinType(TypeIdentifier::INT)), Type::template('T', Type::int()));
+ $this->assertEquals(new TemplateType('T', Type::mixed()), Type::template('T'));
+ }
+
+ public function testCreateUnion()
+ {
+ $this->assertEquals(new UnionType(new BuiltinType(TypeIdentifier::INT), new ObjectType(self::class)), Type::union(Type::int(), Type::object(self::class)));
+ $this->assertEquals(new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::union(Type::int(), Type::string(), Type::int()));
+ $this->assertEquals(new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::union(Type::int(), Type::union(Type::int(), Type::string())));
+ }
+
+ public function testCreateIntersection()
+ {
+ $this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new ObjectType(self::class)), Type::intersection(Type::int(), Type::object(self::class)));
+ $this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::intersection(Type::int(), Type::string(), Type::int()));
+ $this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::intersection(Type::int(), Type::intersection(Type::int(), Type::string())));
+ }
+
+ public function testCreateNullable()
+ {
+ $this->assertEquals(new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::NULL)), Type::nullable(Type::int()));
+ $this->assertEquals(new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::NULL)), Type::nullable(Type::nullable(Type::int())));
+
+ $this->assertEquals(
+ new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING), new BuiltinType(TypeIdentifier::NULL)),
+ Type::nullable(Type::union(Type::int(), Type::string())),
+ );
+ $this->assertEquals(
+ new UnionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING), new BuiltinType(TypeIdentifier::NULL)),
+ Type::nullable(Type::union(Type::int(), Type::string(), Type::null())),
+ );
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.php
new file mode 100644
index 0000000000000..333fd7c812492
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.php
@@ -0,0 +1,74 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummy;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionParameterTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver;
+
+class ReflectionParameterTypeResolverTest extends TestCase
+{
+ private ReflectionParameterTypeResolver $resolver;
+
+ protected function setUp(): void
+ {
+ $this->resolver = new ReflectionParameterTypeResolver(new ReflectionTypeResolver(), new TypeContextFactory());
+ }
+
+ public function testCannotResolveNonReflectionParameter()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve(123);
+ }
+
+ public function testCannotResolveReflectionParameterWithoutType()
+ {
+ $this->expectException(UnsupportedException::class);
+
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionParameter = $reflectionClass->getMethod('setNothing')->getParameters()[0];
+
+ $this->resolver->resolve($reflectionParameter);
+ }
+
+ public function testResolve()
+ {
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionParameter = $reflectionClass->getMethod('setBuiltin')->getParameters()[0];
+
+ $this->assertEquals(Type::int(), $this->resolver->resolve($reflectionParameter));
+ }
+
+ public function testResolveOptionalParameter()
+ {
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionParameter = $reflectionClass->getMethod('setOptional')->getParameters()[0];
+
+ $this->assertEquals(Type::nullable(Type::int()), $this->resolver->resolve($reflectionParameter));
+ }
+
+ public function testCreateTypeContextOrUseProvided()
+ {
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionParameter = $reflectionClass->getMethod('setSelf')->getParameters()[0];
+
+ $this->assertEquals(Type::object(ReflectionExtractableDummy::class), $this->resolver->resolve($reflectionParameter));
+
+ $typeContext = (new TypeContextFactory())->createFromClassName(self::class);
+
+ $this->assertEquals(Type::object(self::class), $this->resolver->resolve($reflectionParameter, $typeContext));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionPropertyTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionPropertyTypeResolverTest.php
new file mode 100644
index 0000000000000..6935f818b6f17
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionPropertyTypeResolverTest.php
@@ -0,0 +1,66 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummy;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionPropertyTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver;
+
+class ReflectionPropertyTypeResolverTest extends TestCase
+{
+ private ReflectionPropertyTypeResolver $resolver;
+
+ protected function setUp(): void
+ {
+ $this->resolver = new ReflectionPropertyTypeResolver(new ReflectionTypeResolver(), new TypeContextFactory());
+ }
+
+ public function testCannotResolveNonReflectionProperty()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve(123);
+ }
+
+ public function testCannotResolveReflectionPropertyWithoutType()
+ {
+ $this->expectException(UnsupportedException::class);
+
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionProperty = $reflectionClass->getProperty('nothing');
+
+ $this->resolver->resolve($reflectionProperty);
+ }
+
+ public function testResolve()
+ {
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionProperty = $reflectionClass->getProperty('builtin');
+
+ $this->assertEquals(Type::int(), $this->resolver->resolve($reflectionProperty));
+ }
+
+ public function testCreateTypeContextOrUseProvided()
+ {
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionProperty = $reflectionClass->getProperty('self');
+
+ $this->assertEquals(Type::object(ReflectionExtractableDummy::class), $this->resolver->resolve($reflectionProperty));
+
+ $typeContext = (new TypeContextFactory())->createFromClassName(self::class);
+
+ $this->assertEquals(Type::object(self::class), $this->resolver->resolve($reflectionProperty, $typeContext));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionReturnTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionReturnTypeResolverTest.php
new file mode 100644
index 0000000000000..56d4fdd821e35
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionReturnTypeResolverTest.php
@@ -0,0 +1,66 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummy;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionReturnTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver;
+
+class ReflectionReturnTypeResolverTest extends TestCase
+{
+ private ReflectionReturnTypeResolver $resolver;
+
+ protected function setUp(): void
+ {
+ $this->resolver = new ReflectionReturnTypeResolver(new ReflectionTypeResolver(), new TypeContextFactory());
+ }
+
+ public function testCannotResolveNonReflectionFunctionAbstract()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve(123);
+ }
+
+ public function testCannotResolveReflectionFunctionAbstractWithoutType()
+ {
+ $this->expectException(UnsupportedException::class);
+
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionFunction = $reflectionClass->getMethod('getNothing');
+
+ $this->resolver->resolve($reflectionFunction);
+ }
+
+ public function testResolve()
+ {
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionFunction = $reflectionClass->getMethod('getBuiltin');
+
+ $this->assertEquals(Type::int(), $this->resolver->resolve($reflectionFunction));
+ }
+
+ public function testCreateTypeContextOrUseProvided()
+ {
+ $reflectionClass = new \ReflectionClass(ReflectionExtractableDummy::class);
+ $reflectionFunction = $reflectionClass->getMethod('getSelf');
+
+ $this->assertEquals(Type::object(ReflectionExtractableDummy::class), $this->resolver->resolve($reflectionFunction));
+
+ $typeContext = (new TypeContextFactory())->createFromClassName(self::class);
+
+ $this->assertEquals(Type::object(self::class), $this->resolver->resolve($reflectionFunction, $typeContext));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php
new file mode 100644
index 0000000000000..89d80ffd03212
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
+use Symfony\Component\TypeInfo\Tests\Fixtures\ReflectionExtractableDummy;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver;
+
+class ReflectionTypeResolverTest extends TestCase
+{
+ private ReflectionTypeResolver $resolver;
+
+ protected function setUp(): void
+ {
+ $this->resolver = new ReflectionTypeResolver();
+ }
+
+ /**
+ * @dataProvider resolveDataProvider
+ */
+ public function testResolve(Type $expectedType, \ReflectionType $reflection, TypeContext $typeContext = null)
+ {
+ $this->assertEquals($expectedType, $this->resolver->resolve($reflection, $typeContext));
+ }
+
+ /**
+ * @return iterable
+ */
+ public function resolveDataProvider(): iterable
+ {
+ $typeContext = (new TypeContextFactory())->createFromClassName(ReflectionExtractableDummy::class);
+ $reflection = new \ReflectionClass(ReflectionExtractableDummy::class);
+
+ yield [Type::int(), $reflection->getProperty('builtin')->getType()];
+ yield [Type::nullable(Type::int()), $reflection->getProperty('nullableBuiltin')->getType()];
+ yield [Type::array(), $reflection->getProperty('array')->getType()];
+ yield [Type::nullable(Type::array()), $reflection->getProperty('nullableArray')->getType()];
+ yield [Type::iterable(), $reflection->getProperty('iterable')->getType()];
+ yield [Type::nullable(Type::iterable()), $reflection->getProperty('nullableIterable')->getType()];
+ yield [Type::object(Dummy::class), $reflection->getProperty('class')->getType()];
+ yield [Type::nullable(Type::object(Dummy::class)), $reflection->getProperty('nullableClass')->getType()];
+ yield [Type::object(ReflectionExtractableDummy::class), $reflection->getProperty('self')->getType(), $typeContext];
+ yield [Type::nullable(Type::object(ReflectionExtractableDummy::class)), $reflection->getProperty('nullableSelf')->getType(), $typeContext];
+ yield [Type::object(ReflectionExtractableDummy::class), $reflection->getMethod('getStatic')->getReturnType(), $typeContext];
+ yield [Type::nullable(Type::object(ReflectionExtractableDummy::class)), $reflection->getMethod('getNullableStatic')->getReturnType(), $typeContext];
+ yield [Type::object(AbstractDummy::class), $reflection->getProperty('parent')->getType(), $typeContext];
+ yield [Type::nullable(Type::object(AbstractDummy::class)), $reflection->getProperty('nullableParent')->getType(), $typeContext];
+ yield [Type::enum(DummyEnum::class), $reflection->getProperty('enum')->getType()];
+ yield [Type::nullable(Type::enum(DummyEnum::class)), $reflection->getProperty('nullableEnum')->getType()];
+ yield [Type::enum(DummyBackedEnum::class), $reflection->getProperty('backedEnum')->getType()];
+ yield [Type::nullable(Type::enum(DummyBackedEnum::class)), $reflection->getProperty('nullableBackedEnum')->getType()];
+ yield [Type::union(Type::int(), Type::string()), $reflection->getProperty('union')->getType()];
+ yield [Type::intersection(Type::object(\Traversable::class), Type::object(\Stringable::class)), $reflection->getProperty('intersection')->getType()];
+ }
+
+ public function testCannotResolveNonProperReflectionType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve(new \ReflectionClass(self::class));
+ }
+
+ /**
+ * @dataProvider classKeywordsTypesDataProvider
+ */
+ public function testCannotResolveClassKeywordsWithoutTypeContext(\ReflectionType $reflection)
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->resolver->resolve($reflection);
+ }
+
+ /**
+ * @return iterable
+ */
+ public function classKeywordsTypesDataProvider(): iterable
+ {
+ $reflection = new \ReflectionClass(ReflectionExtractableDummy::class);
+
+ yield [$reflection->getProperty('self')->getType()];
+ yield [$reflection->getMethod('getStatic')->getReturnType()];
+ yield [$reflection->getProperty('parent')->getType()];
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
new file mode 100644
index 0000000000000..f348baed5f6df
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
@@ -0,0 +1,190 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
+
+class StringTypeResolverTest extends TestCase
+{
+ private StringTypeResolver $resolver;
+
+ protected function setUp(): void
+ {
+ $this->resolver = new StringTypeResolver();
+ }
+
+ /**
+ * @dataProvider resolveDataProvider
+ */
+ public function testResolve(Type $expectedType, string $string, TypeContext $typeContext = null)
+ {
+ $this->assertEquals($expectedType, $this->resolver->resolve($string, $typeContext));
+ }
+
+ /**
+ * @return iterable
+ */
+ public function resolveDataProvider(): iterable
+ {
+ $typeContextFactory = new TypeContextFactory(new StringTypeResolver());
+
+ // callable
+ yield [Type::callable(), 'callable(string, int): mixed'];
+
+ // array
+ yield [Type::array(Type::bool()), 'bool[]'];
+
+ // array shape
+ yield [Type::array(), 'array{0: true, 1: false}'];
+
+ // object shape
+ yield [Type::object(), 'object{foo: true, bar: false}'];
+
+ // this
+ yield [Type::object(Dummy::class), '$this', $typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class)];
+
+ // const
+ yield [Type::array(), 'array[1, 2, 3]'];
+ yield [Type::false(), 'false'];
+ yield [Type::float(), '1.23'];
+ yield [Type::int(), '1'];
+ yield [Type::null(), 'null'];
+ yield [Type::string(), '"string"'];
+ yield [Type::true(), 'true'];
+
+ // identifiers
+ yield [Type::bool(), 'bool'];
+ yield [Type::bool(), 'boolean'];
+ yield [Type::true(), 'true'];
+ yield [Type::false(), 'false'];
+ yield [Type::int(), 'int'];
+ yield [Type::int(), 'integer'];
+ yield [Type::int(), 'positive-int'];
+ yield [Type::int(), 'negative-int'];
+ yield [Type::int(), 'non-positive-int'];
+ yield [Type::int(), 'non-negative-int'];
+ yield [Type::int(), 'non-zero-int'];
+ yield [Type::float(), 'float'];
+ yield [Type::float(), 'double'];
+ yield [Type::string(), 'string'];
+ yield [Type::string(), 'class-string'];
+ yield [Type::string(), 'trait-string'];
+ yield [Type::string(), 'interface-string'];
+ yield [Type::string(), 'callable-string'];
+ yield [Type::string(), 'numeric-string'];
+ yield [Type::string(), 'lowercase-string'];
+ yield [Type::string(), 'non-empty-lowercase-string'];
+ yield [Type::string(), 'non-empty-string'];
+ yield [Type::string(), 'non-falsy-string'];
+ yield [Type::string(), 'truthy-string'];
+ yield [Type::string(), 'literal-string'];
+ yield [Type::string(), 'html-escaped-string'];
+ yield [Type::resource(), 'resource'];
+ yield [Type::object(), 'object'];
+ yield [Type::callable(), 'callable'];
+ yield [Type::array(), 'array'];
+ yield [Type::array(), 'non-empty-array'];
+ yield [Type::list(), 'list'];
+ yield [Type::list(), 'non-empty-list'];
+ yield [Type::iterable(), 'iterable'];
+ yield [Type::mixed(), 'mixed'];
+ yield [Type::null(), 'null'];
+ yield [Type::void(), 'void'];
+ yield [Type::never(), 'never'];
+ yield [Type::never(), 'never-return'];
+ yield [Type::never(), 'never-returns'];
+ yield [Type::never(), 'no-return'];
+ yield [Type::union(Type::int(), Type::string()), 'array-key'];
+ yield [Type::union(Type::int(), Type::float(), Type::string(), Type::bool()), 'scalar'];
+ yield [Type::union(Type::int(), Type::float()), 'number'];
+ yield [Type::union(Type::int(), Type::float(), Type::string()), 'numeric'];
+ yield [Type::object(AbstractDummy::class), 'self', $typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class)];
+ yield [Type::object(Dummy::class), 'static', $typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class)];
+ yield [Type::object(AbstractDummy::class), 'parent', $typeContextFactory->createFromClassName(Dummy::class)];
+ yield [Type::object(Dummy::class), 'Dummy', $typeContextFactory->createFromClassName(Dummy::class)];
+ yield [Type::template('T', Type::union(Type::int(), Type::string())), 'T', $typeContextFactory->createFromClassName(DummyWithTemplates::class)];
+ yield [Type::template('V'), 'V', $typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))];
+
+ // nullable
+ yield [Type::nullable(Type::int()), '?int'];
+
+ // generic
+ yield [Type::generic(Type::object(), Type::string(), Type::bool()), 'object'];
+ yield [Type::generic(Type::object(), Type::generic(Type::string(), Type::bool())), 'object>'];
+ yield [Type::int(), 'int<0, 100>'];
+
+ // union
+ yield [Type::union(Type::int(), Type::string()), 'int|string'];
+
+ // intersection
+ yield [Type::intersection(Type::int(), Type::string()), 'int&string'];
+
+ // DNF
+ yield [Type::union(Type::int(), Type::intersection(Type::string(), Type::bool())), 'int|(string&bool)'];
+
+ // collection objects
+ yield [Type::collection(Type::object(\Traversable::class)), \Traversable::class];
+ yield [Type::collection(Type::object(\Traversable::class), Type::string()), \Traversable::class.''];
+ yield [Type::collection(Type::object(\Traversable::class), Type::bool(), Type::string()), \Traversable::class.''];
+ yield [Type::collection(Type::object(\Iterator::class)), \Iterator::class];
+ yield [Type::collection(Type::object(\Iterator::class), Type::string()), \Iterator::class.''];
+ yield [Type::collection(Type::object(\Iterator::class), Type::bool(), Type::string()), \Iterator::class.''];
+ yield [Type::collection(Type::object(\IteratorAggregate::class)), \IteratorAggregate::class];
+ yield [Type::collection(Type::object(\IteratorAggregate::class), Type::string()), \IteratorAggregate::class.''];
+ yield [Type::collection(Type::object(\IteratorAggregate::class), Type::bool(), Type::string()), \IteratorAggregate::class.''];
+ }
+
+ public function testCannotResolveNonStringType()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve(123);
+ }
+
+ public function testCannotResolveThisWithoutTypeContext()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->resolver->resolve('$this');
+ }
+
+ public function testCannotResolveSelfWithoutTypeContext()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->resolver->resolve('self');
+ }
+
+ public function testCannotResolveStaticWithoutTypeContext()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->resolver->resolve('static');
+ }
+
+ public function testCannotResolveParentWithoutTypeContext()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->resolver->resolve('parent');
+ }
+
+ public function testCannotUnknownIdentifier()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve('unknown');
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php
new file mode 100644
index 0000000000000..3b778ab71c88d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/TypeResolverTest.php
@@ -0,0 +1,92 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\ServiceLocator;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
+
+class TypeResolverTest extends TestCase
+{
+ public function testResolve()
+ {
+ $resolver = TypeResolver::create();
+
+ $this->assertEquals(Type::bool(), $resolver->resolve('bool'));
+ $this->assertEquals(Type::int(), $resolver->resolve((new \ReflectionProperty(Dummy::class, 'id'))->getType()));
+ $this->assertEquals(Type::int(), $resolver->resolve((new \ReflectionMethod(Dummy::class, 'setId'))->getParameters()[0]));
+ $this->assertEquals(Type::int(), $resolver->resolve(new \ReflectionProperty(Dummy::class, 'id')));
+ $this->assertEquals(Type::void(), $resolver->resolve(new \ReflectionMethod(Dummy::class, 'setId')));
+ $this->assertEquals(Type::string(), $resolver->resolve(new \ReflectionFunction(strtoupper(...))));
+ }
+
+ public function testCannotFindResolver()
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->expectExceptionMessage('Cannot find any resolver for "int" type.');
+
+ $resolver = new TypeResolver(new ServiceLocator([]));
+ $resolver->resolve(1);
+ }
+
+ public function testUseProperResolver()
+ {
+ $stringResolver = $this->createMock(TypeResolverInterface::class);
+ $stringResolver->method('resolve')->willReturn(Type::template('STRING'));
+
+ $reflectionTypeResolver = $this->createMock(TypeResolverInterface::class);
+ $reflectionTypeResolver->method('resolve')->willReturn(Type::template('REFLECTION_TYPE'));
+
+ $reflectionParameterResolver = $this->createMock(TypeResolverInterface::class);
+ $reflectionParameterResolver->method('resolve')->willReturn(Type::template('REFLECTION_PARAMETER'));
+
+ $reflectionPropertyResolver = $this->createMock(TypeResolverInterface::class);
+ $reflectionPropertyResolver->method('resolve')->willReturn(Type::template('REFLECTION_PROPERTY'));
+
+ $reflectionReturnTypeResolver = $this->createMock(TypeResolverInterface::class);
+ $reflectionReturnTypeResolver->method('resolve')->willReturn(Type::template('REFLECTION_RETURN_TYPE'));
+
+ $resolver = new TypeResolver(new ServiceLocator([
+ 'string' => fn () => $stringResolver,
+ \ReflectionType::class => fn () => $reflectionTypeResolver,
+ \ReflectionParameter::class => fn () => $reflectionParameterResolver,
+ \ReflectionProperty::class => fn () => $reflectionPropertyResolver,
+ \ReflectionFunctionAbstract::class => fn () => $reflectionReturnTypeResolver,
+ ]));
+
+ $this->assertEquals(Type::template('STRING'), $resolver->resolve('foo'));
+ $this->assertEquals(
+ Type::template('REFLECTION_TYPE'),
+ $resolver->resolve((new \ReflectionProperty(Dummy::class, 'id'))->getType()),
+ );
+ $this->assertEquals(
+ Type::template('REFLECTION_PARAMETER'),
+ $resolver->resolve((new \ReflectionMethod(Dummy::class, 'setId'))->getParameters()[0]),
+ );
+ $this->assertEquals(
+ Type::template('REFLECTION_PROPERTY'),
+ $resolver->resolve(new \ReflectionProperty(Dummy::class, 'id')),
+ );
+ $this->assertEquals(
+ Type::template('REFLECTION_RETURN_TYPE'),
+ $resolver->resolve(new \ReflectionMethod(Dummy::class, 'setId')),
+ );
+ $this->assertEquals(
+ Type::template('REFLECTION_RETURN_TYPE'),
+ $resolver->resolve(new \ReflectionFunction(strtoupper(...))),
+ );
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php
new file mode 100644
index 0000000000000..2f0ad858ed3e9
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php
@@ -0,0 +1,74 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+class TypeTest extends TestCase
+{
+ public function testIs()
+ {
+ $isInt = fn (Type $t) => TypeIdentifier::INT === $t->getBaseType()->getTypeIdentifier();
+
+ $this->assertTrue(Type::int()->is($isInt));
+ $this->assertTrue(Type::union(Type::string(), Type::int())->is($isInt));
+ $this->assertTrue(Type::generic(Type::int(), Type::string())->is($isInt));
+
+ $this->assertFalse(Type::string()->is($isInt));
+ $this->assertFalse(Type::union(Type::string(), Type::float())->is($isInt));
+ $this->assertFalse(Type::generic(Type::string(), Type::int())->is($isInt));
+ }
+
+ public function testIsA()
+ {
+ $this->assertTrue(Type::int()->isA(TypeIdentifier::INT));
+ $this->assertTrue(Type::union(Type::string(), Type::int())->isA(TypeIdentifier::INT));
+ $this->assertTrue(Type::generic(Type::int(), Type::string())->isA(TypeIdentifier::INT));
+
+ $this->assertFalse(Type::string()->isA(TypeIdentifier::INT));
+ $this->assertFalse(Type::union(Type::string(), Type::float())->isA(TypeIdentifier::INT));
+ $this->assertFalse(Type::generic(Type::string(), Type::int())->isA(TypeIdentifier::INT));
+ }
+
+ public function testIsNullable()
+ {
+ $this->assertTrue(Type::null()->isNullable());
+ $this->assertTrue(Type::mixed()->isNullable());
+ $this->assertTrue(Type::nullable(Type::int())->isNullable());
+ $this->assertTrue(Type::union(Type::int(), Type::null())->isNullable());
+ $this->assertTrue(Type::union(Type::int(), Type::mixed())->isNullable());
+ $this->assertTrue(Type::generic(Type::null(), Type::string())->isNullable());
+
+ $this->assertFalse(Type::int()->isNullable());
+ $this->assertFalse(Type::union(Type::int(), Type::string())->isNullable());
+ $this->assertFalse(Type::generic(Type::int(), Type::nullable(Type::string()))->isNullable());
+ $this->assertFalse(Type::generic(Type::int(), Type::mixed())->isNullable());
+ }
+
+ public function testGetBaseType()
+ {
+ $this->assertEquals(Type::string(), Type::string()->getBaseType());
+ $this->assertEquals(Type::object(self::class), Type::object(self::class)->getBaseType());
+ $this->assertEquals(Type::object(), Type::generic(Type::object(), Type::int())->getBaseType());
+ $this->assertEquals(Type::builtin(TypeIdentifier::ARRAY), Type::list()->getBaseType());
+ $this->assertEquals(Type::int(), Type::collection(Type::generic(Type::int(), Type::string()))->getBaseType());
+ }
+
+ public function testCannotGetBaseTypeOnCompoundType()
+ {
+ $this->expectException(LogicException::class);
+ Type::union(Type::int(), Type::string())->getBaseType();
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type.php b/src/Symfony/Component/TypeInfo/Type.php
new file mode 100644
index 0000000000000..5a81f7e2ecc29
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo;
+
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\GenericType;
+use Symfony\Component\TypeInfo\Type\IntersectionType;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\Type\UnionType;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+abstract class Type implements \Stringable
+{
+ use TypeFactoryTrait;
+
+ public function getBaseType(): BuiltinType|ObjectType
+ {
+ if ($this instanceof UnionType || $this instanceof IntersectionType) {
+ throw new LogicException(sprintf('Cannot get base type on "%s" compound type.', (string) $this));
+ }
+
+ $baseType = $this;
+
+ if ($baseType instanceof CollectionType) {
+ $baseType = $baseType->getType();
+ }
+
+ if ($baseType instanceof GenericType) {
+ $baseType = $baseType->getType();
+ }
+
+ return $baseType;
+ }
+
+ /**
+ * @param callable(Type): bool $callable
+ */
+ public function is(callable $callable): bool
+ {
+ return match(true) {
+ $this instanceof UnionType => $this->atLeastOneTypeIs($callable),
+ $this instanceof IntersectionType => $this->everyTypeIs($callable),
+ default => $callable($this),
+ };
+ }
+
+ public function isA(TypeIdentifier $typeIdentifier): bool
+ {
+ return $this->testIdentifier(fn (TypeIdentifier $i): bool => $typeIdentifier === $i);
+ }
+
+ public function isNullable(): bool
+ {
+ return $this->testIdentifier(fn (TypeIdentifier $i): bool => TypeIdentifier::NULL === $i || TypeIdentifier::MIXED === $i);
+ }
+
+ abstract public function asNonNullable(): self;
+
+ /**
+ * @param callable(TypeIdentifier): bool $test
+ */
+ private function testIdentifier(callable $test): bool
+ {
+ $callable = function (self $t) use ($test, &$callable): bool {
+ // unwrap compound type to forward type identifier check
+ if ($t instanceof UnionType || $t instanceof IntersectionType) {
+ return $t->is($callable);
+ }
+
+ return $test($t->getBaseType()->getTypeIdentifier());
+ };
+
+ return $this->is($callable);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/BackedEnumType.php b/src/Symfony/Component/TypeInfo/Type/BackedEnumType.php
new file mode 100644
index 0000000000000..c83017cedf38c
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/BackedEnumType.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of class-string<\BackedEnum>
+ * @template U of BuiltinType|BuiltinType
+ *
+ * @extends EnumType
+ */
+final class BackedEnumType extends EnumType
+{
+ /**
+ * @param T $className
+ * @param U $backingType
+ */
+ public function __construct(
+ string $className,
+ private readonly BuiltinType $backingType,
+ ) {
+ parent::__construct($className);
+ }
+
+ /**
+ * @return U
+ */
+ public function getBackingType(): BuiltinType
+ {
+ return $this->backingType;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/BuiltinType.php b/src/Symfony/Component/TypeInfo/Type/BuiltinType.php
new file mode 100644
index 0000000000000..9a8cc13e25837
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/BuiltinType.php
@@ -0,0 +1,72 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of TypeIdentifier
+ */
+final class BuiltinType extends Type
+{
+ /**
+ * @param T $typeIdentifier
+ */
+ public function __construct(
+ private readonly TypeIdentifier $typeIdentifier,
+ ) {
+ }
+
+ /**
+ * @return T
+ */
+ public function getTypeIdentifier(): TypeIdentifier
+ {
+ return $this->typeIdentifier;
+ }
+
+ /**
+ * @return self|UnionType|BuiltinType|BuiltinType|BuiltinType|BuiltinType|BuiltinType|BuiltinType>
+ */
+ public function asNonNullable(): self|UnionType
+ {
+ if (TypeIdentifier::NULL === $this->typeIdentifier) {
+ throw new LogicException('"null" cannot be turned as non nullable.');
+ }
+
+ // "mixed" is an alias of "object|resource|array|string|float|int|bool|null"
+ // therefore, its non-nullable version is "object|resource|array|string|float|int|bool"
+ if (TypeIdentifier::MIXED === $this->typeIdentifier) {
+ return new UnionType(
+ new self(TypeIdentifier::OBJECT),
+ new self(TypeIdentifier::RESOURCE),
+ new self(TypeIdentifier::ARRAY),
+ new self(TypeIdentifier::STRING),
+ new self(TypeIdentifier::FLOAT),
+ new self(TypeIdentifier::INT),
+ new self(TypeIdentifier::BOOL),
+ );
+ }
+
+ return $this;
+ }
+
+ public function __toString(): string
+ {
+ return $this->typeIdentifier->value;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/CollectionType.php b/src/Symfony/Component/TypeInfo/Type/CollectionType.php
new file mode 100644
index 0000000000000..6b99d1325eaab
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/CollectionType.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * Represents a key/value collection type.
+ *
+ * It proxies every method to the main type and adds methods related to key and value types.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of BuiltinType|BuiltinType|ObjectType|GenericType
+ */
+final class CollectionType extends Type
+{
+ /**
+ * @param T $type
+ */
+ public function __construct(
+ private readonly BuiltinType|ObjectType|GenericType $type,
+ private readonly bool $isList = false,
+ ) {
+ if ($this->isList()) {
+ $keyType = $this->getCollectionKeyType();
+
+ if (!$keyType instanceof BuiltinType || TypeIdentifier::INT !== $keyType->getTypeIdentifier()) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a valid list key type.', (string) $keyType));
+ }
+ }
+ }
+
+ /**
+ * @return T
+ */
+ public function getType(): BuiltinType|ObjectType|GenericType
+ {
+ return $this->type;
+ }
+
+ public function isList(): bool
+ {
+ return $this->isList;
+ }
+
+ public function asNonNullable(): self
+ {
+ return $this;
+ }
+
+ public function getCollectionKeyType(): Type
+ {
+ $defaultCollectionKeyType = self::union(self::int(), self::string());
+
+ if ($this->type instanceof GenericType) {
+ return match (\count($this->type->getVariableTypes())) {
+ 2 => $this->type->getVariableTypes()[0],
+ 1 => self::int(),
+ default => $defaultCollectionKeyType,
+ };
+ }
+
+ return $defaultCollectionKeyType;
+ }
+
+ public function getCollectionValueType(): Type
+ {
+ $defaultCollectionValueType = self::mixed();
+
+ if ($this->type instanceof GenericType) {
+ return match (\count($this->type->getVariableTypes())) {
+ 2 => $this->type->getVariableTypes()[1],
+ 1 => $this->type->getVariableTypes()[0],
+ default => $defaultCollectionValueType,
+ };
+ }
+
+ return $defaultCollectionValueType;
+ }
+
+ public function __toString(): string
+ {
+ return (string) $this->type;
+ }
+
+ /**
+ * Proxies all method calls to the original type.
+ *
+ * @param list $arguments
+ */
+ public function __call(string $method, array $arguments): mixed
+ {
+ return $this->type->{$method}(...$arguments);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/CompositeTypeTrait.php b/src/Symfony/Component/TypeInfo/Type/CompositeTypeTrait.php
new file mode 100644
index 0000000000000..9da43daab924a
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/CompositeTypeTrait.php
@@ -0,0 +1,86 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @internal
+ *
+ * @template T of Type
+ */
+trait CompositeTypeTrait
+{
+ /**
+ * @var list
+ */
+ private readonly array $types;
+
+ /**
+ * @param list $types
+ */
+ public function __construct(Type ...$types)
+ {
+ if (\count($types) < 2) {
+ throw new InvalidArgumentException(sprintf('"%s" expects at least 2 types.', self::class));
+ }
+
+ foreach ($types as $t) {
+ if ($t instanceof self) {
+ throw new InvalidArgumentException(sprintf('Cannot set "%s" as a "%1$s" part.', self::class));
+ }
+ }
+
+ usort($types, fn (Type $a, Type $b): int => (string) $a <=> (string) $b);
+ $this->types = array_values(array_unique($types));
+ }
+
+ /**
+ * @return list
+ */
+ public function getTypes(): array
+ {
+ return $this->types;
+ }
+
+ /**
+ * @param callable(T): bool $callable
+ */
+ public function atLeastOneTypeIs(callable $callable): bool
+ {
+ foreach ($this->types as $t) {
+ if ($callable($t)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param callable(T): bool $callable
+ */
+ public function everyTypeIs(callable $callable): bool
+ {
+ foreach ($this->types as $t) {
+ if (!$callable($t)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/EnumType.php b/src/Symfony/Component/TypeInfo/Type/EnumType.php
new file mode 100644
index 0000000000000..95665921d1590
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/EnumType.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of class-string<\UnitEnum>
+ *
+ * @extends ObjectType
+ */
+class EnumType extends ObjectType
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/GenericType.php b/src/Symfony/Component/TypeInfo/Type/GenericType.php
new file mode 100644
index 0000000000000..dc6a845278da8
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/GenericType.php
@@ -0,0 +1,88 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * Represents a generic type, which is a type that holds variable parts.
+ *
+ * It proxies every method to the main type and adds methods related to variable types.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of BuiltinType|BuiltinType|ObjectType
+ */
+final class GenericType extends Type
+{
+ /**
+ * @var list
+ */
+ private readonly array $variableTypes;
+
+ /**
+ * @param T $type
+ */
+ public function __construct(
+ private readonly BuiltinType|ObjectType $type,
+ Type ...$variableTypes,
+ ) {
+ $this->variableTypes = $variableTypes;
+ }
+
+ /**
+ * @return T
+ */
+ public function getType(): BuiltinType|ObjectType
+ {
+ return $this->type;
+ }
+
+ public function asNonNullable(): self
+ {
+ return $this;
+ }
+
+ /**
+ * @return list
+ */
+ public function getVariableTypes(): array
+ {
+ return $this->variableTypes;
+ }
+
+ public function __toString(): string
+ {
+ $typeString = (string) $this->type;
+
+ $variableTypesString = '';
+ $glue = '';
+ foreach ($this->variableTypes as $t) {
+ $variableTypesString .= $glue.((string) $t);
+ $glue = ',';
+ }
+
+ return $typeString.'<'.$variableTypesString.'>';
+ }
+
+ /**
+ * Proxies all method calls to the original type.
+ *
+ * @param list $arguments
+ */
+ public function __call(string $method, array $arguments): mixed
+ {
+ return $this->type->{$method}(...$arguments);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/IntersectionType.php b/src/Symfony/Component/TypeInfo/Type/IntersectionType.php
new file mode 100644
index 0000000000000..c8a0bd03d501d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/IntersectionType.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of Type
+ */
+final class IntersectionType extends Type
+{
+ /**
+ * @use CompositeTypeTrait
+ */
+ use CompositeTypeTrait;
+
+ public function __toString(): string
+ {
+ $string = '';
+ $glue = '';
+
+ foreach ($this->types as $t) {
+ $string .= $glue.($t instanceof UnionType ? '('.((string) $t).')' : ((string) $t));
+ $glue = '&';
+ }
+
+ return $string;
+ }
+
+ public function asNonNullable(): self
+ {
+ if ($this->isNullable()) {
+ throw new LogicException(sprintf('"%s cannot be turned as non nullable.', (string) $this));
+ }
+
+ return $this;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/ObjectType.php b/src/Symfony/Component/TypeInfo/Type/ObjectType.php
new file mode 100644
index 0000000000000..e10d90bd16612
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/ObjectType.php
@@ -0,0 +1,55 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of class-string
+ */
+class ObjectType extends Type
+{
+ /**
+ * @param T $className
+ */
+ public function __construct(
+ private readonly string $className,
+ ) {
+ }
+
+ public function getTypeIdentifier(): TypeIdentifier
+ {
+ return TypeIdentifier::OBJECT;
+ }
+
+ /**
+ * @return T
+ */
+ public function getClassName(): string
+ {
+ return $this->className;
+ }
+
+ public function asNonNullable(): static
+ {
+ return $this;
+ }
+
+ public function __toString(): string
+ {
+ return $this->className;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/TemplateType.php b/src/Symfony/Component/TypeInfo/Type/TemplateType.php
new file mode 100644
index 0000000000000..a96b61293ec75
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/TemplateType.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * Represents a template placeholder, such as "T" in "Collection".
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class TemplateType extends Type
+{
+ public function __construct(
+ private readonly string $name,
+ private readonly Type $bound,
+ ) {
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getBound(): Type
+ {
+ return $this->bound;
+ }
+
+ public function asNonNullable(): self
+ {
+ return $this;
+ }
+
+ public function __toString(): string
+ {
+ return $this->name;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/UnionType.php b/src/Symfony/Component/TypeInfo/Type/UnionType.php
new file mode 100644
index 0000000000000..02ace46087d6a
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/UnionType.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Type;
+
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of Type
+ */
+final class UnionType extends Type
+{
+ /**
+ * @use CompositeTypeTrait
+ */
+ use CompositeTypeTrait;
+
+ public function asNonNullable(): Type
+ {
+ $nonNullableTypes = [];
+ foreach ($this->getTypes() as $type) {
+ if ($type->isA(TypeIdentifier::NULL)) {
+ continue;
+ }
+
+ $nonNullableType = $type->asNonNullable();
+ $nonNullableTypes = [
+ ...$nonNullableTypes,
+ ...($nonNullableType instanceof self ? $nonNullableType->getTypes() : [$nonNullableType]),
+ ];
+ }
+
+ return \count($nonNullableTypes) > 1 ? new self(...$nonNullableTypes) : $nonNullableTypes[0];
+ }
+
+ public function __toString(): string
+ {
+ $string = '';
+ $glue = '';
+
+ foreach ($this->types as $t) {
+ $string .= $glue.($t instanceof IntersectionType ? '('.((string) $t).')' : ((string) $t));
+ $glue = '|';
+ }
+
+ return $string;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php b/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php
new file mode 100644
index 0000000000000..60638b95616e8
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php
@@ -0,0 +1,116 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeContext;
+
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * Type resolving context.
+ *
+ * Helps to retrieve declaring class, called class, parent class, templates
+ * and normalize classes according to the current namespace and uses.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class TypeContext
+{
+ /**
+ * @var array
+ */
+ private static array $classExistCache = [];
+
+ /**
+ * @param array $uses
+ * @param array $templates
+ */
+ public function __construct(
+ public readonly string $calledClassName,
+ public readonly string $declaringClassName,
+ public readonly ?string $namespace = null,
+ public readonly array $uses = [],
+ public readonly array $templates = [],
+ ) {
+ }
+
+ /**
+ * Normalize class name according to current namespace and uses.
+ */
+ public function normalize(string $name): string
+ {
+ if (str_starts_with($name, '\\')) {
+ return ltrim($name, '\\');
+ }
+
+ $nameParts = explode('\\', $name);
+ $firstNamePart = $nameParts[0];
+ if (isset($this->uses[$firstNamePart])) {
+ if (1 === \count($nameParts)) {
+ return $this->uses[$firstNamePart];
+ }
+ array_shift($nameParts);
+
+ return sprintf('%s\\%s', $this->uses[$firstNamePart], implode('\\', $nameParts));
+ }
+
+ if (null !== $this->namespace) {
+ return sprintf('%s\\%s', $this->namespace, $name);
+ }
+
+ return $name;
+ }
+
+ /**
+ * @return class-string
+ */
+ public function getDeclaringClass(): string
+ {
+ return $this->normalize($this->declaringClassName);
+ }
+
+ /**
+ * @return class-string
+ */
+ public function getCalledClass(): string
+ {
+ return $this->normalize($this->calledClassName);
+ }
+
+ /**
+ * @return class-string
+ */
+ public function getParentClass(): string
+ {
+ $declaringClassName = $this->getDeclaringClass();
+
+ if (false === $parentClass = get_parent_class($declaringClassName)) {
+ throw new LogicException(sprintf('"%s" do not extend any class.', $declaringClassName));
+ }
+
+ if (!isset(self::$classExistCache[$parentClass])) {
+ self::$classExistCache[$parentClass] = false;
+
+ if (class_exists($parentClass)) {
+ self::$classExistCache[$parentClass] = true;
+ } else {
+ try {
+ new \ReflectionClass($parentClass);
+ self::$classExistCache[$parentClass] = true;
+ } catch (\Throwable) {
+ }
+ }
+ }
+
+ return self::$classExistCache[$parentClass] ? $parentClass : $this->normalize(str_replace($this->namespace.'\\', '', $parentClass));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
new file mode 100644
index 0000000000000..e5d17e0a30801
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
@@ -0,0 +1,184 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeContext;
+
+use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
+use PHPStan\PhpDocParser\Lexer\Lexer;
+use PHPStan\PhpDocParser\Parser\ConstExprParser;
+use PHPStan\PhpDocParser\Parser\PhpDocParser;
+use PHPStan\PhpDocParser\Parser\TokenIterator;
+use PHPStan\PhpDocParser\Parser\TypeParser;
+use Symfony\Component\TypeInfo\Exception\RuntimeException;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
+
+/**
+ * Creates a type resolving context.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class TypeContextFactory
+{
+ /**
+ * @var array
+ */
+ private static array $reflectionClassCache = [];
+
+ private ?Lexer $phpstanLexer = null;
+ private ?PhpDocParser $phpstanParser = null;
+
+ public function __construct(
+ private readonly ?StringTypeResolver $stringTypeResolver = null,
+ ) {
+ }
+
+ public function createFromClassName(string $calledClassName, string $declaringClassName = null): TypeContext
+ {
+ $declaringClassName ??= $calledClassName;
+
+ $calledClassPath = explode('\\', $calledClassName);
+ $declaringClassPath = explode('\\', $declaringClassName);
+
+ $declaringClassReflection = (self::$reflectionClassCache[$declaringClassName] ??= new \ReflectionClass($declaringClassName));
+
+ $typeContext = new TypeContext(
+ array_pop($calledClassPath),
+ array_pop($declaringClassPath),
+ trim($declaringClassReflection->getNamespaceName(), '\\'),
+ $this->collectUses($declaringClassReflection),
+ );
+
+ return new TypeContext(
+ $typeContext->calledClassName,
+ $typeContext->declaringClassName,
+ $typeContext->namespace,
+ $typeContext->uses,
+ $this->collectTemplates($declaringClassReflection, $typeContext),
+ );
+ }
+
+ public function createFromReflection(\Reflector $reflection): ?TypeContext
+ {
+ $declaringClassReflection = match (true) {
+ $reflection instanceof \ReflectionClass => $reflection,
+ $reflection instanceof \ReflectionMethod => $reflection->getDeclaringClass(),
+ $reflection instanceof \ReflectionProperty => $reflection->getDeclaringClass(),
+ $reflection instanceof \ReflectionParameter => $reflection->getDeclaringClass(),
+ $reflection instanceof \ReflectionFunctionAbstract => $reflection->getClosureScopeClass(),
+ default => null,
+ };
+
+ if (null === $declaringClassReflection) {
+ return null;
+ }
+
+ $typeContext = new TypeContext(
+ $declaringClassReflection->getShortName(),
+ $declaringClassReflection->getShortName(),
+ $declaringClassReflection->getNamespaceName(),
+ $this->collectUses($declaringClassReflection),
+ );
+
+ $templates = match (true) {
+ $reflection instanceof \ReflectionFunctionAbstract => $this->collectTemplates($reflection, $typeContext) + $this->collectTemplates($declaringClassReflection, $typeContext),
+ $reflection instanceof \ReflectionParameter => $this->collectTemplates($reflection->getDeclaringFunction(), $typeContext) + $this->collectTemplates($declaringClassReflection, $typeContext),
+ default => $this->collectTemplates($declaringClassReflection, $typeContext),
+ };
+
+ return new TypeContext(
+ $typeContext->calledClassName,
+ $typeContext->declaringClassName,
+ $typeContext->namespace,
+ $typeContext->uses,
+ $templates,
+ );
+ }
+
+ /**
+ * @return array
+ */
+ private function collectUses(\ReflectionClass $reflection): array
+ {
+ $fileName = $reflection->getFileName();
+ if (!\is_string($fileName) || !is_file($fileName)) {
+ return [];
+ }
+
+ if (false === $lines = @file($fileName)) {
+ throw new RuntimeException(sprintf('Unable to read file "%s".', $fileName));
+ }
+
+ $uses = [];
+ $inUseSection = false;
+
+ foreach ($lines as $line) {
+ if (str_starts_with($line, 'use ')) {
+ $inUseSection = true;
+ $use = explode(' as ', substr($line, 4, -2), 2);
+
+ $alias = 1 === \count($use) ? substr($use[0], false !== ($p = strrpos($use[0], '\\')) ? 1 + $p : 0) : $use[1];
+ $uses[$alias] = $use[0];
+ } elseif ($inUseSection) {
+ break;
+ }
+ }
+
+ $traitUses = [];
+ foreach ($reflection->getTraits() as $traitReflection) {
+ $traitUses[] = $this->collectUses($traitReflection);
+ }
+
+ return array_merge($uses, ...$traitUses);
+ }
+
+ /**
+ * @return array
+ */
+ private function collectTemplates(\ReflectionClass|\ReflectionFunctionAbstract $reflection, TypeContext $typeContext): array
+ {
+ if (!$this->stringTypeResolver || !class_exists(PhpDocParser::class)) {
+ return [];
+ }
+
+ if (!$rawDocNode = $reflection->getDocComment()) {
+ return [];
+ }
+
+ $this->phpstanLexer ??= new Lexer();
+ $this->phpstanParser ??= new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
+
+ $tokens = new TokenIterator($this->phpstanLexer->tokenize($rawDocNode));
+
+ $templates = [];
+ foreach ($this->phpstanParser->parse($tokens)->getTagsByName('@template') as $tag) {
+ if (!$tag->value instanceof TemplateTagValueNode) {
+ continue;
+ }
+
+ $type = Type::mixed();
+ $typeString = ((string) $tag->value->bound) ?: null;
+
+ try {
+ if (null !== $typeString) {
+ $type = $this->stringTypeResolver->resolve($typeString, $typeContext);
+ }
+ } catch (UnsupportedException) {
+ }
+
+ $templates[$tag->value->name] = $type;
+ }
+
+ return $templates;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php
new file mode 100644
index 0000000000000..f56c17b16c789
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php
@@ -0,0 +1,319 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo;
+
+use Symfony\Component\TypeInfo\Type\BackedEnumType;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\EnumType;
+use Symfony\Component\TypeInfo\Type\GenericType;
+use Symfony\Component\TypeInfo\Type\IntersectionType;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\Type\TemplateType;
+use Symfony\Component\TypeInfo\Type\UnionType;
+
+/**
+ * Helper trait to create any type easily.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+trait TypeFactoryTrait
+{
+ /**
+ * @template T of TypeIdentifier
+ * @template U value-of
+ *
+ * @param T|U $identifier
+ *
+ * @return BuiltinType
+ */
+ public static function builtin(TypeIdentifier|string $identifier): BuiltinType
+ {
+ /** @var T $identifier */
+ $identifier = \is_string($identifier) ? TypeIdentifier::from($identifier) : $identifier;
+
+ return new BuiltinType($identifier);
+ }
+
+ public static function foo(): BackedEnumType
+ {
+ return new BackedEnumType(\BackedEnum::class, Type::int());
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function int(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::INT);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function float(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::FLOAT);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function string(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::STRING);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function bool(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::BOOL);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function resource(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::RESOURCE);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function false(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::FALSE);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function true(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::TRUE);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function callable(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::CALLABLE);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function mixed(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::MIXED);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function null(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::NULL);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function void(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::VOID);
+ }
+
+ /**
+ * @return BuiltinType
+ */
+ public static function never(): BuiltinType
+ {
+ return self::builtin(TypeIdentifier::NEVER);
+ }
+
+ /**
+ * @template T of BuiltinType|BuiltinType|ObjectType|GenericType
+ *
+ * @param T $type
+ *
+ * @return CollectionType
+ */
+ public static function collection(BuiltinType|ObjectType|GenericType $type, Type $value = null, Type $key = null, bool $asList = false): CollectionType
+ {
+ if (!$type instanceof GenericType && (null !== $value || null !== $key)) {
+ $type = self::generic($type, $key ?? self::union(self::int(), self::string()), $value ?? self::mixed());
+ }
+
+ return new CollectionType($type, $asList);
+ }
+
+ /**
+ * @return CollectionType>
+ */
+ public static function array(Type $value = null, Type $key = null, bool $asList = false): CollectionType
+ {
+ return self::collection(self::builtin(TypeIdentifier::ARRAY), $value, $key, $asList);
+ }
+
+ /**
+ * @return CollectionType>
+ */
+ public static function iterable(Type $value = null, Type $key = null, bool $asList = false): CollectionType
+ {
+ return self::collection(self::builtin(TypeIdentifier::ITERABLE), $value, $key, $asList);
+ }
+
+ /**
+ * @return CollectionType>
+ */
+ public static function list(Type $value = null): CollectionType
+ {
+ return self::array($value, self::int(), asList: true);
+ }
+
+ /**
+ * @return CollectionType>
+ */
+ public static function dict(Type $value = null): CollectionType
+ {
+ return self::array($value, self::string());
+ }
+
+ /**
+ * @template T of class-string
+ *
+ * @param T|null $className
+ *
+ * @return ($className is class-string ? ObjectType : BuiltinType)
+ */
+ public static function object(string $className = null): BuiltinType|ObjectType
+ {
+ return null !== $className ? new ObjectType($className) : new BuiltinType(TypeIdentifier::OBJECT);
+ }
+
+ /**
+ * @template T of class-string<\UnitEnum>|class-string<\BackedEnum>
+ * @template U of BuiltinType|BuiltinType
+ *
+ * @param T $className
+ * @param U|null $backingType
+ *
+ * @return ($className is class-string<\BackedEnum> ? ($backingType is U ? BackedEnumType : BackedEnumType|BuiltinType>) : EnumType))
+ */
+ public static function enum(string $className, BuiltinType $backingType = null): EnumType
+ {
+ if (is_subclass_of($className, \BackedEnum::class)) {
+ if (null === $backingType) {
+ $reflectionBackingType = (new \ReflectionEnum($className))->getBackingType();
+ $typeIdentifier = TypeIdentifier::INT->value === (string) $reflectionBackingType ? TypeIdentifier::INT : TypeIdentifier::STRING;
+ $backingType = new BuiltinType($typeIdentifier);
+ }
+
+ return new BackedEnumType($className, $backingType);
+ }
+
+ return new EnumType($className);
+ }
+
+ /**
+ * @template T of BuiltinType|BuiltinType|ObjectType
+ *
+ * @param T $mainType
+ *
+ * @return GenericType
+ */
+ public static function generic(Type $mainType, Type ...$variableTypes): GenericType
+ {
+ return new GenericType($mainType, ...$variableTypes);
+ }
+
+ public static function template(string $name, Type $bound = null): TemplateType
+ {
+ return new TemplateType($name, $bound ?? Type::mixed());
+ }
+
+ /**
+ * @template T of Type
+ *
+ * @param list $types
+ *
+ * @return UnionType
+ */
+ public static function union(Type ...$types): UnionType
+ {
+ /** @var list $unionTypes */
+ $unionTypes = [];
+
+ foreach ($types as $type) {
+ if (!$type instanceof UnionType) {
+ $unionTypes[] = $type;
+
+ continue;
+ }
+
+ foreach ($type->getTypes() as $unionType) {
+ $unionTypes[] = $unionType;
+ }
+ }
+
+ return new UnionType(...$unionTypes);
+ }
+
+ /**
+ * @template T of Type
+ *
+ * @param list $types
+ *
+ * @return IntersectionType
+ */
+ public static function intersection(Type ...$types): IntersectionType
+ {
+ /** @var list $intersectionTypes */
+ $intersectionTypes = [];
+
+ foreach ($types as $type) {
+ if (!$type instanceof IntersectionType) {
+ $intersectionTypes[] = $type;
+
+ continue;
+ }
+
+ foreach ($type->getTypes() as $intersectionType) {
+ $intersectionTypes[] = $intersectionType;
+ }
+ }
+
+ return new IntersectionType(...$intersectionTypes);
+ }
+
+ /**
+ * @template T of Type
+ *
+ * @param T $type
+ *
+ * @return (T is UnionType ? T : UnionType>)
+ */
+ public static function nullable(Type $type): UnionType
+ {
+ if ($type instanceof UnionType) {
+ return Type::union(Type::null(), ...$type->getTypes());
+ }
+
+ return Type::union($type, Type::null());
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeIdentifier.php b/src/Symfony/Component/TypeInfo/TypeIdentifier.php
new file mode 100644
index 0000000000000..5326262a8562d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeIdentifier.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo;
+
+/**
+ * Identifier of a PHP native type.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+enum TypeIdentifier: string
+{
+ case ARRAY = 'array';
+ case BOOL = 'bool';
+ case CALLABLE = 'callable';
+ case FALSE = 'false';
+ case FLOAT = 'float';
+ case INT = 'int';
+ case ITERABLE = 'iterable';
+ case MIXED = 'mixed';
+ case NULL = 'null';
+ case OBJECT = 'object';
+ case RESOURCE = 'resource';
+ case STRING = 'string';
+ case TRUE = 'true';
+ case NEVER = 'never';
+ case VOID = 'void';
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionParameterTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionParameterTypeResolver.php
new file mode 100644
index 0000000000000..83d94f90b6fa6
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionParameterTypeResolver.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+
+/**
+ * Resolves type for a given parameter reflection.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @internal
+ */
+final readonly class ReflectionParameterTypeResolver implements TypeResolverInterface
+{
+ public function __construct(
+ private ReflectionTypeResolver $reflectionTypeResolver,
+ private TypeContextFactory $typeContextFactory,
+ ) {
+ }
+
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type
+ {
+ if (!$subject instanceof \ReflectionParameter) {
+ throw new UnsupportedException(sprintf('Expected subject to be a "ReflectionParameter", "%s" given.', get_debug_type($subject)), $subject);
+ }
+
+ $typeContext ??= $this->typeContextFactory->createFromReflection($subject);
+
+ try {
+ return $this->reflectionTypeResolver->resolve($subject->getType(), $typeContext);
+ } catch (UnsupportedException $e) {
+ $path = null !== $typeContext
+ ? sprintf('%s::%s($%s)', $typeContext->calledClassName, $subject->getDeclaringFunction()->getName(), $subject->getName())
+ : sprintf('%s($%s)', $subject->getDeclaringFunction()->getName(), $subject->getName());
+
+ throw new UnsupportedException(sprintf('Cannot resolve type for "%s".', $path), $subject, previous: $e);
+ }
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionPropertyTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionPropertyTypeResolver.php
new file mode 100644
index 0000000000000..a6c55a411b5ac
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionPropertyTypeResolver.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+
+/**
+ * Resolves type for a given property reflection.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @internal
+ */
+final readonly class ReflectionPropertyTypeResolver implements TypeResolverInterface
+{
+ public function __construct(
+ private ReflectionTypeResolver $reflectionTypeResolver,
+ private TypeContextFactory $typeContextFactory,
+ ) {
+ }
+
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type
+ {
+ if (!$subject instanceof \ReflectionProperty) {
+ throw new UnsupportedException(sprintf('Expected subject to be a "ReflectionProperty", "%s" given.', get_debug_type($subject)), $subject);
+ }
+
+ $typeContext ??= $this->typeContextFactory->createFromReflection($subject);
+
+ try {
+ return $this->reflectionTypeResolver->resolve($subject->getType(), $typeContext);
+ } catch (UnsupportedException $e) {
+ $path = sprintf('%s::$%s', $subject->getDeclaringClass()->getName(), $subject->getName());
+
+ throw new UnsupportedException(sprintf('Cannot resolve type for "%s".', $path), $subject, previous: $e);
+ }
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionReturnTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionReturnTypeResolver.php
new file mode 100644
index 0000000000000..feab41730b0e6
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionReturnTypeResolver.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+
+/**
+ * Resolves return type for a given function reflection.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @internal
+ */
+final readonly class ReflectionReturnTypeResolver implements TypeResolverInterface
+{
+ public function __construct(
+ private ReflectionTypeResolver $reflectionTypeResolver,
+ private TypeContextFactory $typeContextFactory,
+ ) {
+ }
+
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type
+ {
+ if (!$subject instanceof \ReflectionFunctionAbstract) {
+ throw new UnsupportedException(sprintf('Expected subject to be a "ReflectionFunctionAbstract", "%s" given.', get_debug_type($subject)), $subject);
+ }
+
+ $typeContext ??= $this->typeContextFactory->createFromReflection($subject);
+
+ try {
+ return $this->reflectionTypeResolver->resolve($subject->getReturnType(), $typeContext);
+ } catch (UnsupportedException $e) {
+ $path = null !== $typeContext
+ ? sprintf('%s::%s()', $typeContext->calledClassName, $subject->getName())
+ : sprintf('%s()', $subject->getName());
+
+ throw new UnsupportedException(sprintf('Cannot resolve type for "%s".', $path), $subject, previous: $e);
+ }
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
new file mode 100644
index 0000000000000..193086aef1017
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * Resolves type for a given type reflection.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @internal
+ */
+final class ReflectionTypeResolver implements TypeResolverInterface
+{
+ /**
+ * @var array
+ */
+ private static array $reflectionEnumCache = [];
+
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type
+ {
+ if ($subject instanceof \ReflectionUnionType) {
+ return Type::union(...array_map(fn (mixed $t): Type => $this->resolve($t, $typeContext), $subject->getTypes()));
+ }
+
+ if ($subject instanceof \ReflectionIntersectionType) {
+ return Type::intersection(...array_map(fn (mixed $t): Type => $this->resolve($t, $typeContext), $subject->getTypes()));
+ }
+
+ if (!$subject instanceof \ReflectionNamedType) {
+ throw new UnsupportedException(sprintf('Expected subject to be a "ReflectionNamedType", a "ReflectionUnionType" or a "ReflectionIntersectionType", "%s" given.', get_debug_type($subject)), $subject);
+ }
+
+ $identifier = $subject->getName();
+ $nullable = $subject->allowsNull();
+
+ if (TypeIdentifier::ARRAY->value === $identifier) {
+ $type = Type::array();
+
+ return $nullable ? Type::nullable($type) : $type;
+ }
+
+ if (TypeIdentifier::ITERABLE->value === $identifier) {
+ $type = Type::iterable();
+
+ return $nullable ? Type::nullable($type) : $type;
+ }
+
+ if (TypeIdentifier::NULL->value === $identifier || TypeIdentifier::MIXED->value === $identifier) {
+ return Type::builtin($identifier);
+ }
+
+ if ($subject->isBuiltin()) {
+ $type = Type::builtin(TypeIdentifier::from($identifier));
+
+ return $nullable ? Type::nullable($type) : $type;
+ }
+
+ if (\in_array(strtolower($identifier), ['self', 'static', 'parent'], true) && !$typeContext) {
+ throw new InvalidArgumentException(sprintf('A "%s" must be provided to resolve "%s".', TypeContext::class, strtolower($identifier)));
+ }
+
+ /** @var class-string $className */
+ $className = match (true) {
+ 'self' === strtolower($identifier) => $typeContext->getDeclaringClass(),
+ 'static' === strtolower($identifier) => $typeContext->getCalledClass(),
+ 'parent' === strtolower($identifier) => $typeContext->getParentClass(),
+ default => $identifier,
+ };
+
+ if (is_subclass_of($className, \BackedEnum::class)) {
+ $reflectionEnum = (self::$reflectionEnumCache[$className] ??= new \ReflectionEnum($className));
+ $backingType = $this->resolve($reflectionEnum->getBackingType(), $typeContext);
+ $type = Type::enum($className, $backingType);
+ } elseif (is_subclass_of($className, \UnitEnum::class)) {
+ $type = Type::enum($className);
+ } else {
+ $type = Type::object($className);
+ }
+
+ return $nullable ? Type::nullable($type) : $type;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
new file mode 100644
index 0000000000000..de90a5073fc60
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
@@ -0,0 +1,255 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeResolver;
+
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
+use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprTrueNode;
+use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
+use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
+use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
+use PHPStan\PhpDocParser\Ast\Type\TypeNode;
+use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
+use PHPStan\PhpDocParser\Lexer\Lexer;
+use PHPStan\PhpDocParser\Parser\ConstExprParser;
+use PHPStan\PhpDocParser\Parser\TokenIterator;
+use PHPStan\PhpDocParser\Parser\TypeParser;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\GenericType;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeIdentifier;
+
+/**
+ * Resolves type for a given string.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @internal
+ */
+final class StringTypeResolver implements TypeResolverInterface
+{
+ private const COLLECTION_CLASS_NAMES = [\Traversable::class, \Iterator::class, \IteratorAggregate::class, \ArrayAccess::class, \Generator::class];
+
+ /**
+ * @var array
+ */
+ private static array $classExistCache = [];
+
+ private readonly Lexer $lexer;
+ private readonly TypeParser $parser;
+
+ public function __construct()
+ {
+ $this->lexer = new Lexer();
+ $this->parser = new TypeParser(new ConstExprParser());
+ }
+
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type
+ {
+ if (!\is_string($subject)) {
+ throw new UnsupportedException(sprintf('Expected subject to be a "string", "%s" given.', get_debug_type($subject)), $subject);
+ }
+
+ try {
+ $tokens = new TokenIterator($this->lexer->tokenize($subject));
+ $node = $this->parser->parse($tokens);
+
+ return $this->getTypeFromNode($node, $typeContext);
+ } catch (\DomainException $e) {
+ throw new UnsupportedException(sprintf('Cannot resolve "%s".', $subject), $subject, previous: $e);
+ }
+ }
+
+ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Type
+ {
+ if ($node instanceof CallableTypeNode) {
+ return Type::callable();
+ }
+
+ if ($node instanceof ArrayTypeNode) {
+ return Type::array($this->getTypeFromNode($node->type, $typeContext));
+ }
+
+ if ($node instanceof ArrayShapeNode) {
+ return Type::array();
+ }
+
+ if ($node instanceof ObjectShapeNode) {
+ return Type::object();
+ }
+
+ if ($node instanceof ThisTypeNode) {
+ if (null === $typeContext) {
+ throw new InvalidArgumentException(sprintf('A "%s" must be provided to resolve "$this".', TypeContext::class));
+ }
+
+ return Type::object($typeContext->getCalledClass());
+ }
+
+ if ($node instanceof ConstTypeNode) {
+ return match ($node->constExpr::class) {
+ ConstExprArrayNode::class => Type::array(),
+ ConstExprFalseNode::class => Type::false(),
+ ConstExprFloatNode::class => Type::float(),
+ ConstExprIntegerNode::class => Type::int(),
+ ConstExprNullNode::class => Type::null(),
+ ConstExprStringNode::class => Type::string(),
+ ConstExprTrueNode::class => Type::true(),
+ default => throw new \DomainException(sprintf('Unhandled "%s" constant expression.', $node->constExpr::class)),
+ };
+ }
+
+ if ($node instanceof IdentifierTypeNode) {
+ $type = match ($node->name) {
+ 'bool', 'boolean' => Type::bool(),
+ 'true' => Type::true(),
+ 'false' => Type::false(),
+ 'int', 'integer', 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int' => Type::int(),
+ 'float', 'double' => Type::float(),
+ 'string',
+ 'class-string',
+ 'trait-string',
+ 'interface-string',
+ 'callable-string',
+ 'numeric-string',
+ 'lowercase-string',
+ 'non-empty-lowercase-string',
+ 'non-empty-string',
+ 'non-falsy-string',
+ 'truthy-string',
+ 'literal-string',
+ 'html-escaped-string' => Type::string(),
+ 'resource' => Type::resource(),
+ 'object' => Type::object(),
+ 'callable' => Type::callable(),
+ 'array', 'non-empty-array' => Type::array(),
+ 'list', 'non-empty-list' => Type::list(),
+ 'iterable' => Type::iterable(),
+ 'mixed' => Type::mixed(),
+ 'null' => Type::null(),
+ 'array-key' => Type::union(Type::int(), Type::string()),
+ 'scalar' => Type::union(Type::int(), Type::float(), Type::string(), Type::bool()),
+ 'number' => Type::union(Type::int(), Type::float()),
+ 'numeric' => Type::union(Type::int(), Type::float(), Type::string()),
+ 'self' => $typeContext ? Type::object($typeContext->getDeclaringClass()) : throw new InvalidArgumentException(sprintf('A "%s" must be provided to resolve "self".', TypeContext::class)),
+ 'static' => $typeContext ? Type::object($typeContext->getCalledClass()) : throw new InvalidArgumentException(sprintf('A "%s" must be provided to resolve "static".', TypeContext::class)),
+ 'parent' => $typeContext ? Type::object($typeContext->getParentClass()) : throw new InvalidArgumentException(sprintf('A "%s" must be provided to resolve "parent".', TypeContext::class)),
+ 'void' => Type::void(),
+ 'never', 'never-return', 'never-returns', 'no-return' => Type::never(),
+ default => $this->resolveCustomIdentifier($node->name, $typeContext),
+ };
+
+ if ($type instanceof ObjectType && \in_array($type->getClassName(), self::COLLECTION_CLASS_NAMES, true)) {
+ return Type::collection($type);
+ }
+
+ return $type;
+ }
+
+ if ($node instanceof NullableTypeNode) {
+ return Type::nullable($this->getTypeFromNode($node->type, $typeContext));
+ }
+
+ if ($node instanceof GenericTypeNode) {
+ $type = $this->getTypeFromNode($node->type, $typeContext);
+
+ // handle integer ranges as simple integers
+ if ($type->isA(TypeIdentifier::INT)) {
+ return $type;
+ }
+
+ $variableTypes = array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->genericTypes);
+
+ if ($type instanceof CollectionType) {
+ $keyType = $type->getCollectionKeyType();
+
+ $type = $type->getType();
+ if ($type instanceof GenericType) {
+ $type = $type->getType();
+ }
+
+ if (1 === \count($variableTypes)) {
+ return Type::collection($type, $variableTypes[0], $keyType);
+ } elseif (2 === \count($variableTypes)) {
+ return Type::collection($type, $variableTypes[1], $variableTypes[0]);
+ }
+ }
+
+ if ($type instanceof ObjectType && \in_array($type->getClassName(), self::COLLECTION_CLASS_NAMES, true)) {
+ return match (\count($variableTypes)) {
+ 1 => Type::collection($type, $variableTypes[0]),
+ 2 => Type::collection($type, $variableTypes[1], $variableTypes[0]),
+ default => Type::collection($type),
+ };
+ }
+
+ return Type::generic($type, ...$variableTypes);
+ }
+
+ if ($node instanceof UnionTypeNode) {
+ return Type::union(...array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->types));
+ }
+
+ if ($node instanceof IntersectionTypeNode) {
+ return Type::intersection(...array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->types));
+ }
+
+ throw new \DomainException(sprintf('Unhandled "%s" node.', $node::class));
+ }
+
+ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeContext): Type
+ {
+ $className = $typeContext ? $typeContext->normalize($identifier) : $identifier;
+
+ if (!isset(self::$classExistCache[$className])) {
+ self::$classExistCache[$className] = false;
+
+ if (class_exists($className) || interface_exists($className)) {
+ self::$classExistCache[$className] = true;
+ } else {
+ try {
+ new \ReflectionClass($className);
+ self::$classExistCache[$className] = true;
+
+ return Type::object($className);
+ } catch (\Throwable) {
+ }
+ }
+ }
+
+ if (self::$classExistCache[$className]) {
+ return Type::object($className);
+ }
+
+ if (isset($typeContext?->templates[$identifier])) {
+ return Type::template($identifier, $typeContext->templates[$identifier]);
+ }
+
+ throw new \DomainException(sprintf('Unhandled "%s" identifier.', $identifier));
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php
new file mode 100644
index 0000000000000..173dc937381ed
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolver.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeResolver;
+
+use PHPStan\PhpDocParser\Parser\PhpDocParser;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+
+/**
+ * Resolves type for a given subject by delegating resolving to nested type resolvers.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final readonly class TypeResolver implements TypeResolverInterface
+{
+ /**
+ * @param ContainerInterface $resolvers Locator of type resolvers, keyed by supported subject type
+ */
+ public function __construct(
+ private ContainerInterface $resolvers,
+ ) {
+ }
+
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type
+ {
+ $subjectType = match (\is_object($subject)) {
+ true => match (true) {
+ is_subclass_of($subject::class, \ReflectionType::class) => \ReflectionType::class,
+ is_subclass_of($subject::class, \ReflectionFunctionAbstract::class) => \ReflectionFunctionAbstract::class,
+ default => $subject::class,
+ },
+ false => get_debug_type($subject),
+ };
+
+ if (!$this->resolvers->has($subjectType)) {
+ if ('string' === $subjectType) {
+ throw new UnsupportedException('Cannot find any resolver for "string" type. Try running "composer require phpstan/phpdoc-parser".', $subject);
+ }
+
+ throw new UnsupportedException(sprintf('Cannot find any resolver for "%s" type.', $subjectType), $subject);
+ }
+
+ /** @param TypeResolverInterface $resolver */
+ $resolver = $this->resolvers->get($subjectType);
+
+ return $resolver->resolve($subject, $typeContext);
+ }
+
+ public static function create(): self
+ {
+ $resolvers = new class() implements ContainerInterface {
+ private readonly array $resolvers;
+
+ public function __construct()
+ {
+ $stringTypeResolver = class_exists(PhpDocParser::class) ? new StringTypeResolver() : null;
+ $typeContextFactory = new TypeContextFactory($stringTypeResolver);
+ $reflectionTypeResolver = new ReflectionTypeResolver();
+
+ $resolvers = [
+ \ReflectionType::class => $reflectionTypeResolver,
+ \ReflectionParameter::class => new ReflectionParameterTypeResolver($reflectionTypeResolver, $typeContextFactory),
+ \ReflectionProperty::class => new ReflectionPropertyTypeResolver($reflectionTypeResolver, $typeContextFactory),
+ \ReflectionFunctionAbstract::class => new ReflectionReturnTypeResolver($reflectionTypeResolver, $typeContextFactory),
+ ];
+
+ if (null !== $stringTypeResolver) {
+ $resolvers['string'] = $stringTypeResolver;
+ }
+
+ $this->resolvers = $resolvers;
+ }
+
+ public function has(string $id): bool
+ {
+ return isset($this->resolvers[$id]);
+ }
+
+ public function get(string $id): TypeResolverInterface
+ {
+ return $this->resolvers[$id];
+ }
+ };
+
+ return new self($resolvers);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolverInterface.php b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolverInterface.php
new file mode 100644
index 0000000000000..aca3f26623491
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolverInterface.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+
+/**
+ * Resolves type for a given subject.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+interface TypeResolverInterface
+{
+ /**
+ * Try to resolve a {@see Type} on a $subject.
+ * If the resolver cannot resolve the type, it will throw a {@see UnsupportedException}.
+ *
+ * @throws UnsupportedException
+ */
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type;
+}
diff --git a/src/Symfony/Component/TypeInfo/composer.json b/src/Symfony/Component/TypeInfo/composer.json
new file mode 100644
index 0000000000000..2967e0d1ed79d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "symfony/type-info",
+ "type": "library",
+ "description": "Extracts PHP types information.",
+ "keywords": [
+ "type",
+ "phpdoc",
+ "phpstan",
+ "symfony"
+ ],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Mathias Arlaud",
+ "email": "mathias.arlaud@gmail.com"
+ },
+ {
+ "name": "Baptiste LEDUC",
+ "email": "baptiste.leduc@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=8.2",
+ "psr/container": "^1.1|^2.0"
+ },
+ "require-dev": {
+ "symfony/dependency-injection": "^7.1",
+ "phpstan/phpdoc-parser": "^1.0"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\TypeInfo\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev"
+}
diff --git a/src/Symfony/Component/TypeInfo/phpunit.xml.dist b/src/Symfony/Component/TypeInfo/phpunit.xml.dist
new file mode 100644
index 0000000000000..11b4d18ad464c
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/phpunit.xml.dist
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+ ./Tests/
+
+
+
+
+
+ ./
+
+
+ ./Resources
+ ./Tests
+ ./vendor
+
+
+