diff --git a/composer.json b/composer.json
index 381c2fef9c772..18787ebeadb9f 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/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index ed3b53d14ac42..fc5b36b43bf1b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========
+7.1
+---
+
+ * Register TypeInfo services
+
7.0
---
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
index 1d21c6b663688..3f90c740f330c 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
@@ -69,6 +69,7 @@ class UnusedTagsPass implements CompilerPassInterface
'mime.mime_type_guesser',
'monolog.logger',
'notifier.channel',
+ 'type_info.resolver',
'property_info.access_extractor',
'property_info.initializable_extractor',
'property_info.list_extractor',
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 883b2da90178a..cbc89e2d869ee 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -43,6 +43,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;
@@ -158,6 +159,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);
@@ -1148,6 +1150,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 d03932f8c4840..498165ec40d2d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -167,6 +167,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 +390,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);
}
@@ -1950,6 +1956,22 @@ 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)
+ ->addTag('type_info.resolver', ['priority' => -1000]);
+
+ $container->setAlias(StringTypeResolver::class, '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/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
index 25f9637867943..1234c420d1861 100644
--- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
+++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
@@ -64,6 +64,7 @@
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass;
+use Symfony\Component\TypeInfo\DependencyInjection\TypeInfoPass;
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
@@ -150,6 +151,7 @@ public function build(ContainerBuilder $container): void
$this->addCompilerPassIfExists($container, TranslationDumperPass::class);
$container->addCompilerPass(new FragmentRendererPass());
$this->addCompilerPassIfExists($container, SerializerPass::class);
+ $this->addCompilerPassIfExists($container, TypeInfoPass::class);
$this->addCompilerPassIfExists($container, PropertyInfoPass::class);
$container->addCompilerPass(new ControllerArgumentValueResolverPass());
$container->addCompilerPass(new CachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 32);
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 532cf022d3c66..30da597ee642f 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 @@
+
@@ -326,6 +327,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..164372c3d2086
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/type_info.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\DependencyInjection\Loader\Configurator;
+
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+use Symfony\Component\TypeInfo\TypeResolver\ChainTypeResolver;
+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\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', ChainTypeResolver::class)
+ ->args([[]])
+ ->alias(TypeResolverInterface::class, 'type_info.resolver')
+
+ ->set('type_info.resolver.reflection_type', ReflectionTypeResolver::class)
+ ->args([service('type_info.type_context_factory')])
+ ->tag('type_info.resolver', ['priority' => -1001])
+ ->alias(ReflectionTypeResolver::class, 'type_info.resolver.reflection_type')
+
+ ->set('type_info.resolver.reflection_parameter', ReflectionParameterTypeResolver::class)
+ ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')])
+ ->tag('type_info.resolver', ['priority' => -1002])
+ ->alias(ReflectionParameterTypeResolver::class, 'type_info.resolver.reflection_parameter')
+
+ ->set('type_info.resolver.reflection_property', ReflectionPropertyTypeResolver::class)
+ ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')])
+ ->tag('type_info.resolver', ['priority' => -1003])
+ ->alias(ReflectionPropertyTypeResolver::class, 'type_info.resolver.reflection_property')
+
+ ->set('type_info.resolver.reflection_return', ReflectionReturnTypeResolver::class)
+ ->args([service('type_info.resolver.reflection_type'), service('type_info.type_context_factory')])
+ ->tag('type_info.resolver', ['priority' => -1004])
+ ->alias(ReflectionReturnTypeResolver::class, 'type_info.resolver.reflection_return')
+ ;
+};
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index fa2de05a0d18e..3f5b95f8a15c7 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
@@ -623,6 +624,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 9036fc6d58da7..3597cd99f8e10 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
@@ -1608,6 +1608,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..de4f1cf6067bd
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TypeInfoTest.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\Bundle\FrameworkBundle\Tests\Functional;
+
+use Symfony\Component\TypeInfo\Type;
+
+class TypeInfoTest extends AbstractWebTestCase
+{
+ public function testComponent()
+ {
+ static::bootKernel(['test_case' => 'TypeInfo']);
+
+ $this->assertEquals(Type::union(Type::int(), Type::null()), static::getContainer()->get('type_info.resolver')->resolve('int|null'));
+ }
+}
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 8e879fd213adc..14842532083dc 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/Config/Resource/FileExistenceResource.php b/src/Symfony/Component/Config/Resource/FileExistenceResource.php
index e7b91ff382bb2..666866ee42f77 100644
--- a/src/Symfony/Component/Config/Resource/FileExistenceResource.php
+++ b/src/Symfony/Component/Config/Resource/FileExistenceResource.php
@@ -38,7 +38,7 @@ public function __construct(string $resource)
public function __toString(): string
{
- return $this->resource;
+ return 'existence.'.$this->resource;
}
public function getResource(): string
diff --git a/src/Symfony/Component/Config/Tests/Resource/FileExistenceResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/FileExistenceResourceTest.php
index 31fd7846d81ca..b719099f804dc 100644
--- a/src/Symfony/Component/Config/Tests/Resource/FileExistenceResourceTest.php
+++ b/src/Symfony/Component/Config/Tests/Resource/FileExistenceResourceTest.php
@@ -36,7 +36,7 @@ protected function tearDown(): void
public function testToString()
{
- $this->assertSame($this->file, (string) $this->resource);
+ $this->assertSame('existence.'.$this->file, (string) $this->resource);
}
public function testGetResource()
diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php
index d39508949215a..8d3255d4678b7 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php
@@ -268,12 +268,6 @@ private function checkController(Request $request, callable $controller): callab
$name = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $name);
}
- if (-1 === $request->attributes->get('_check_controller_is_allowed')) {
- trigger_deprecation('symfony/http-kernel', '6.4', 'Callable "%s()" is not allowed as a controller. Did you miss tagging it with "#[AsController]" or registering its type with "%s::allowControllers()"?', $name, self::class);
-
- return $controller;
- }
-
throw new BadRequestException(sprintf('Callable "%s()" is not allowed as a controller. Did you miss tagging it with "#[AsController]" or registering its type with "%s::allowControllers()"?', $name, self::class));
}
}
diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php
index c60d35e53d36d..eb2b9c85ca061 100644
--- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php
+++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php
@@ -201,7 +201,7 @@ private function getContainerDeprecationLogs(): array
private function getContainerCompilerLogs(string $compilerLogsFilepath = null): array
{
- if (!is_file($compilerLogsFilepath)) {
+ if (!$compilerLogsFilepath || !is_file($compilerLogsFilepath)) {
return [];
}
diff --git a/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php b/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php
index 562244b338b51..4aa246b6a4967 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php
@@ -70,7 +70,7 @@ public function onKernelRequest(RequestEvent $event): void
}
parse_str($request->query->get('_path', ''), $attributes);
- $attributes['_check_controller_is_allowed'] = -1; // @deprecated, switch to true in Symfony 7
+ $attributes['_check_controller_is_allowed'] = true;
$request->attributes->add($attributes);
$request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', []), $attributes));
$request->query->remove('_path');
diff --git a/src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php
index 668be81e8c5cb..14a3f3cf24133 100644
--- a/src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php
+++ b/src/Symfony/Component/HttpKernel/Fragment/AbstractSurrogateFragmentRenderer.php
@@ -59,6 +59,8 @@ public function __construct(?SurrogateInterface $surrogate, FragmentRendererInte
public function render(string|ControllerReference $uri, Request $request, array $options = []): Response
{
if (!$this->surrogate || !$this->surrogate->hasSurrogateCapability($request)) {
+ $request->attributes->set('_check_controller_is_allowed', true);
+
if ($uri instanceof ControllerReference && $this->containsNonScalars($uri->attributes)) {
throw new \InvalidArgumentException('Passing non-scalar values as part of URI attributes to the ESI and SSI rendering strategies is not supported. Use a different rendering strategy or pass scalar values.');
}
diff --git a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php
index c74200af6d408..6b815a87ba91a 100644
--- a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php
+++ b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php
@@ -130,6 +130,9 @@ protected function createSubRequest(string $uri, Request $request): Request
if ($request->attributes->has('_stateless')) {
$subRequest->attributes->set('_stateless', $request->attributes->get('_stateless'));
}
+ if ($request->attributes->has('_check_controller_is_allowed')) {
+ $subRequest->attributes->set('_check_controller_is_allowed', $request->attributes->get('_check_controller_is_allowed'));
+ }
return $subRequest;
}
diff --git a/src/Symfony/Component/HttpKernel/Log/DebugLoggerConfigurator.php b/src/Symfony/Component/HttpKernel/Log/DebugLoggerConfigurator.php
index 4271244ad97d5..537c1004083f4 100644
--- a/src/Symfony/Component/HttpKernel/Log/DebugLoggerConfigurator.php
+++ b/src/Symfony/Component/HttpKernel/Log/DebugLoggerConfigurator.php
@@ -18,12 +18,12 @@
*/
class DebugLoggerConfigurator
{
- private ?\Closure $processor = null;
+ private ?object $processor = null;
public function __construct(callable $processor, bool $enable = null)
{
if ($enable ?? !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
- $this->processor = $processor(...);
+ $this->processor = \is_object($processor) ? $processor : $processor(...);
}
}
diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php
index 58d716943712a..b18b2b3585855 100644
--- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php
+++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php
@@ -107,7 +107,7 @@ public function execute(): CollectionInterface
$this->resetPagination();
}
- throw new LdapException(sprintf('Could not complete search with dn "%s", query "%s" and filters "%s".%s.', $this->dn, $this->query, implode(',', $this->options['filter']), $ldapError));
+ throw new LdapException(sprintf('Could not complete search with dn "%s", query "%s" and filters "%s".%s.', $this->dn, $this->query, implode(',', $this->options['filter']), $ldapError), $errno);
}
$this->results[] = $search;
diff --git a/src/Symfony/Component/String/AbstractString.php b/src/Symfony/Component/String/AbstractString.php
index 3f36eefceef0d..10231a217487c 100644
--- a/src/Symfony/Component/String/AbstractString.php
+++ b/src/Symfony/Component/String/AbstractString.php
@@ -507,20 +507,14 @@ public function toByteString(string $toEncoding = null): ByteString
return $b;
}
- set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m));
-
try {
- try {
- $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8');
- } catch (InvalidArgumentException $e) {
- if (!\function_exists('iconv')) {
- throw $e;
- }
-
- $b->string = iconv('UTF-8', $toEncoding, $this->string);
+ $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8');
+ } catch (\ValueError $e) {
+ if (!\function_exists('iconv')) {
+ throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
- } finally {
- restore_error_handler();
+
+ $b->string = iconv('UTF-8', $toEncoding, $this->string);
}
return $b;
diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
index 5db9da5753a0c..b4578c77802fa 100644
--- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
+++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php
@@ -1581,4 +1581,22 @@ public static function provideWidth(): array
[17, "\u{007f}\u{007f}f\u{001b}[0moo\u{0001}bar\u{007f}cccïf\u{008e}cy\u{0005}1", false], // f[0moobarcccïfcy1
];
}
+
+ /**
+ * @dataProvider provideToByteString
+ */
+ public function testToByteString(string $origin, string $encoding)
+ {
+ $instance = static::createFromString($origin)->toByteString($encoding);
+ $this->assertInstanceOf(ByteString::class, $instance);
+ }
+
+ public static function provideToByteString(): array
+ {
+ return [
+ ['žsžsý', 'UTF-8'],
+ ['žsžsý', 'windows-1250'],
+ ['žsžsý', 'Windows-1252'],
+ ];
+ }
}
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/BuiltinType.php b/src/Symfony/Component/TypeInfo/BuiltinType.php
new file mode 100644
index 0000000000000..783d2d5b34876
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/BuiltinType.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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+enum BuiltinType: 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';
+}
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/DependencyInjection/TypeInfoPass.php b/src/Symfony/Component/TypeInfo/DependencyInjection/TypeInfoPass.php
new file mode 100644
index 0000000000000..c1e061b46a5bf
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/DependencyInjection/TypeInfoPass.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * Adds resolvers to the type_info service.
+ *
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class TypeInfoPass implements CompilerPassInterface
+{
+ use PriorityTaggedServiceTrait;
+
+ public function process(ContainerBuilder $container): void
+ {
+ if (!$container->hasDefinition('type_info.resolver')) {
+ return;
+ }
+
+ $definition = $container->getDefinition('type_info.resolver');
+ $resolvers = $this->findAndSortTaggedServices('type_info.resolver', $container);
+
+ $definition->replaceArgument(0, new IteratorArgument($resolvers));
+ }
+}
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..8e9e2e8b5fdc4
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Exception/UnsupportedException.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 UnsupportedException extends \LogicException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/TypeInfo/LICENSE b/src/Symfony/Component/TypeInfo/LICENSE
new file mode 100644
index 0000000000000..3ed9f412ce53d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2023-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..2392ce0cbfe1a
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/README.md
@@ -0,0 +1,14 @@
+TypeInfo Component
+==================
+
+The TypeInfo component extracts information about PHP types using
+metadata of popular sources.
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/components/type_info.html)
+ * [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/DependencyInjection/TypeInfoPassTest.php b/src/Symfony/Component/TypeInfo/Tests/DependencyInjection/TypeInfoPassTest.php
new file mode 100644
index 0000000000000..e11087f8a9a8b
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/DependencyInjection/TypeInfoPassTest.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\TypeInfo\Tests\DependencyInjection;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\TypeInfo\DependencyInjection\TypeInfoPass;
+
+class TypeInfoPassTest extends TestCase
+{
+ public function testInjectTypeResolvers()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('type_info.resolver')->setArguments([null]);
+
+ $container->register('second')->addTag('type_info.resolver', ['priority' => 10]);
+ $container->register('third')->addTag('type_info.resolver', ['priority' => 1]);
+ $container->register('first')->addTag('type_info.resolver', ['priority' => 100]);
+
+ (new TypeInfoPass())->process($container);
+
+ $this->assertEquals(
+ new IteratorArgument([new Reference('first'), new Reference('second'), new Reference('third')]),
+ $container->getDefinition('type_info.resolver')->getArgument(0),
+ );
+ }
+}
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..7b4c8434d6634
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/ReflectionExtractableDummy.php
@@ -0,0 +1,84 @@
+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 getVoid(): void
+ {
+ }
+
+ public function getNever(): never
+ {
+ exit(0);
+ }
+
+ 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;
+ }
+}
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..1d7f9d3700912
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php
@@ -0,0 +1,47 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+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\BackedEnumType;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+
+class BackedEnumTypeTest extends TestCase
+{
+ public function testCannotCreateWithInvalidClass()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new BackedEnumType(DummyEnum::class, new BuiltinType(BuiltinTypeEnum::INT));
+ }
+
+ public function testCannotCreateWithInvalidBackingType()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new BackedEnumType(DummyBackedEnum::class, new BuiltinType(BuiltinTypeEnum::BOOL));
+ }
+
+ public function testToString()
+ {
+ $this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, new BuiltinType(BuiltinTypeEnum::INT)));
+ }
+
+ public function testIsBuiltinType()
+ {
+ $this->assertFalse((new BackedEnumType(DummyBackedEnum::class, new BuiltinType(BuiltinTypeEnum::INT)))->isNullable());
+ $this->assertFalse((new BackedEnumType(DummyBackedEnum::class, new BuiltinType(BuiltinTypeEnum::INT)))->isBuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertTrue((new BackedEnumType(DummyBackedEnum::class, new BuiltinType(BuiltinTypeEnum::INT)))->isBuiltinType(BuiltinTypeEnum::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..19d310949b71d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.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\Tests\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+
+class BuiltinTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $this->assertSame('int', (string) new BuiltinType(BuiltinTypeEnum::INT));
+ }
+
+ public function testIsBuiltinType()
+ {
+ $this->assertFalse((new BuiltinType(BuiltinTypeEnum::INT))->isBuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertTrue((new BuiltinType(BuiltinTypeEnum::INT))->isBuiltinType(BuiltinTypeEnum::INT));
+ $this->assertFalse((new BuiltinType(BuiltinTypeEnum::INT))->isNullable());
+ $this->assertTrue((new BuiltinType(BuiltinTypeEnum::NULL))->isNullable());
+ $this->assertTrue((new BuiltinType(BuiltinTypeEnum::MIXED))->isNullable());
+ }
+}
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..1ad45ca546f89
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php
@@ -0,0 +1,84 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\CollectionType;
+use Symfony\Component\TypeInfo\Type\GenericType;
+
+class CollectionTypeTest extends TestCase
+{
+ public function testGetCollectionKeyType()
+ {
+ $type = new CollectionType(new BuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertEquals(Type::union(Type::int(), Type::string()), $type->getCollectionKeyType());
+
+ $type = new CollectionType(new GenericType(new BuiltinType(BuiltinTypeEnum::ARRAY), new BuiltinType(BuiltinTypeEnum::BOOL)));
+ $this->assertEquals(Type::int(), $type->getCollectionKeyType());
+
+ $type = new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ ));
+ $this->assertEquals(Type::string(), $type->getCollectionKeyType());
+ }
+
+ public function testGetCollectionValueType()
+ {
+ $type = new CollectionType(new BuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertEquals(Type::mixed(), $type->getCollectionValueType());
+
+ $type = new CollectionType(new GenericType(new BuiltinType(BuiltinTypeEnum::ARRAY), new BuiltinType(BuiltinTypeEnum::BOOL)));
+ $this->assertEquals(Type::bool(), $type->getCollectionValueType());
+
+ $type = new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ ));
+ $this->assertEquals(Type::bool(), $type->getCollectionValueType());
+ }
+
+ public function testToString()
+ {
+ $type = new CollectionType(new BuiltinType(BuiltinTypeEnum::ITERABLE));
+ $this->assertEquals('iterable', (string) $type);
+
+ $type = new CollectionType(new GenericType(new BuiltinType(BuiltinTypeEnum::ARRAY), new BuiltinType(BuiltinTypeEnum::BOOL)));
+ $this->assertEquals('array', (string) $type);
+
+ $type = new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ ));
+ $this->assertEquals('array', (string) $type);
+ }
+
+ public function testIsBuiltinType()
+ {
+ $type = new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ ));
+
+ $this->assertTrue($type->isBuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::STRING));
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::INT));
+ $this->assertFalse($type->isNullable());
+ }
+}
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..ba114bc31bc02
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php
@@ -0,0 +1,39 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
+use Symfony\Component\TypeInfo\Type\EnumType;
+
+class EnumTypeTest extends TestCase
+{
+ public function testCannotCreateWithInvalidClass()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ new EnumType(self::class);
+ }
+
+ public function testToString()
+ {
+ $this->assertSame(DummyEnum::class, (string) new EnumType(DummyEnum::class));
+ }
+
+ public function testIsBuiltinType()
+ {
+ $this->assertFalse((new EnumType(DummyEnum::class))->isNullable());
+ $this->assertFalse((new EnumType(DummyEnum::class))->isBuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertFalse((new EnumType(DummyEnum::class))->isBuiltinType(BuiltinTypeEnum::INT));
+ }
+}
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..59f77c0bb30fa
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.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\Type;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\GenericType;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+use Symfony\Component\TypeInfo\Type\UnionType;
+
+class GenericTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $type = new GenericType(new BuiltinType(BuiltinTypeEnum::ARRAY), new BuiltinType(BuiltinTypeEnum::BOOL));
+ $this->assertEquals('array', (string) $type);
+
+ $type = new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ );
+ $this->assertEquals('array', (string) $type);
+
+ $type = new GenericType(
+ new ObjectType(self::class),
+ new UnionType(new BuiltinType(BuiltinTypeEnum::BOOL), new BuiltinType(BuiltinTypeEnum::STRING)),
+ new BuiltinType(BuiltinTypeEnum::INT),
+ new BuiltinType(BuiltinTypeEnum::FLOAT),
+ );
+ $this->assertEquals(sprintf('%s', self::class), (string) $type);
+ }
+
+ public function testIsBuiltinType()
+ {
+ $type = new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ );
+ $this->assertFalse($type->isNullable());
+ $this->assertTrue($type->isBuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::STRING));
+
+ $type = new GenericType(
+ new ObjectType(self::class),
+ new UnionType(new BuiltinType(BuiltinTypeEnum::BOOL), new BuiltinType(BuiltinTypeEnum::STRING)),
+ new BuiltinType(BuiltinTypeEnum::INT),
+ new BuiltinType(BuiltinTypeEnum::FLOAT),
+ );
+ $this->assertFalse($type->isNullable());
+ $this->assertTrue($type->isBuiltinType(BuiltinTypeEnum::OBJECT));
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::INT));
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::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..6321b0c12331d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php
@@ -0,0 +1,77 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\IntersectionType;
+
+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 testIsBuiltinType()
+ {
+ $type = new IntersectionType(Type::int(), Type::string(), Type::float());
+ $this->assertFalse($type->isNullable());
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::ARRAY));
+
+ $type = new IntersectionType(Type::int(), Type::string(), Type::union(Type::float(), Type::bool()));
+ $this->assertFalse($type->isNullable());
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::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..ea1a128e95832
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Type\ObjectType;
+
+class ObjectTypeTest extends TestCase
+{
+ public function testToString()
+ {
+ $this->assertSame(self::class, (string) new ObjectType(self::class));
+ }
+
+ public function testIsBuiltinType()
+ {
+ $this->assertFalse((new ObjectType(self::class))->isNullable());
+ $this->assertFalse((new ObjectType(self::class))->isBuiltinType(BuiltinTypeEnum::ARRAY));
+ $this->assertTrue((new ObjectType(self::class))->isBuiltinType(BuiltinTypeEnum::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..fc1f7337e7944
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php
@@ -0,0 +1,95 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+use Symfony\Component\TypeInfo\Type\UnionType;
+
+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());
+ }
+
+ 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 testIsBuiltinType()
+ {
+ $type = new UnionType(Type::int(), Type::string(), Type::float());
+ $this->assertFalse($type->isNullable());
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::ARRAY));
+
+ $type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::float(), Type::bool()));
+ $this->assertFalse($type->isNullable());
+ $this->assertTrue($type->isBuiltinType(BuiltinTypeEnum::INT));
+ $this->assertTrue($type->isBuiltinType(BuiltinTypeEnum::STRING));
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::FLOAT));
+ $this->assertFalse($type->isBuiltinType(BuiltinTypeEnum::BOOL));
+ }
+}
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..f031b04c889ce
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeContext/TypeContextFactoryTest.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\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 testCollectClassTemplates()
+ {
+ $this->assertEquals([], $this->typeContextFactory->createFromClassName(Dummy::class)->classTemplates);
+
+ $templates = [
+ ['name' => 'T', 'type' => Type::union(Type::int(), Type::string())],
+ ['name' => 'U', 'type' => null],
+ ];
+
+ $this->assertEquals($templates, $this->typeContextFactory->createFromClassName(DummyWithTemplates::class)->classTemplates);
+
+ $this->assertEquals($templates, $this->typeContextFactory->createFromReflection(new \ReflectionClass(DummyWithTemplates::class))->classTemplates);
+ $this->assertEquals($templates, $this->typeContextFactory->createFromReflection(new \ReflectionProperty(DummyWithTemplates::class, 'price'))->classTemplates);
+ $this->assertEquals($templates, $this->typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))->classTemplates);
+ $this->assertEquals($templates, $this->typeContextFactory->createFromReflection(new \ReflectionParameter([DummyWithTemplates::class, 'getPrice'], 'inCents'))->classTemplates);
+ }
+
+ public function testDoNotCollectClassTemplatesWhenToStringTypeResolver()
+ {
+ $typeContextFactory = new TypeContextFactory();
+
+ $this->assertEquals([], $typeContextFactory->createFromClassName(DummyWithTemplates::class)->classTemplates);
+ }
+}
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..b0e7bc7ef91a1
--- /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 testResolve()
+ {
+ $typeContext = (new TypeContextFactory())->createFromClassName(DummyWithUses::class);
+
+ $this->assertSame(DummyWithUses::class, $typeContext->resolve('DummyWithUses'));
+ $this->assertSame(Type::class, $typeContext->resolve('Type'));
+ $this->assertSame(\DateTimeImmutable::class, $typeContext->resolve('DateTime'));
+ $this->assertSame('Symfony\\Component\\TypeInfo\\Tests\\Fixtures\\unknown', $typeContext->resolve('unknown'));
+ $this->assertSame('unknown', $typeContext->resolve('\\unknown'));
+
+ $typeContextWithoutNamespace = new TypeContext('Foo', 'Bar');
+ $this->assertSame('unknown', $typeContextWithoutNamespace->resolve('unknown'));
+ }
+
+ public function testResolveDeclaringClass()
+ {
+ $this->assertSame(Dummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class)->resolveDeclaringClass());
+ $this->assertSame(AbstractDummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class, AbstractDummy::class)->resolveDeclaringClass());
+ }
+
+ public function testResolveCalledClass()
+ {
+ $this->assertSame(Dummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class)->resolveCalledClass());
+ $this->assertSame(Dummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class, AbstractDummy::class)->resolveCalledClass());
+ }
+
+ public function testResolveParentClass()
+ {
+ $this->assertSame(AbstractDummy::class, (new TypeContextFactory())->createFromClassName(Dummy::class)->resolveParentClass());
+ $this->assertSame(\stdClass::class, (new TypeContextFactory())->createFromClassName(DummyExtendingStdClass::class)->resolveParentClass());
+ }
+
+ public function testCannotResolveParentClassWhenDoNotInherit()
+ {
+ $this->expectException(LogicException::class);
+ (new TypeContextFactory())->createFromClassName(AbstractDummy::class)->resolveParentClass();
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php
new file mode 100644
index 0000000000000..f9675d6b5d7bb
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php
@@ -0,0 +1,193 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+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;
+
+class TypeFactoryTest extends TestCase
+{
+ public function testCreateBuiltin()
+ {
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::INT), Type::builtin(BuiltinTypeEnum::INT));
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::INT), Type::builtin('int'));
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::INT), Type::int());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::FLOAT), Type::float());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::STRING), Type::string());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::BOOL), Type::bool());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::RESOURCE), Type::resource());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::FALSE), Type::false());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::TRUE), Type::true());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::CALLABLE), Type::callable());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::NULL), Type::null());
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::MIXED), Type::mixed());
+ }
+
+ public function testCreateArray()
+ {
+ $this->assertEquals(new CollectionType(new BuiltinType(BuiltinTypeEnum::ARRAY)), Type::array());
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING)),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ )),
+ Type::array(Type::bool()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ )),
+ Type::array(Type::bool(), Type::string()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::INT),
+ new BuiltinType(BuiltinTypeEnum::MIXED),
+ )),
+ Type::list(),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::INT),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ )),
+ Type::list(Type::bool()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::MIXED),
+ )),
+ Type::dict(),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ARRAY),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ )),
+ Type::dict(Type::bool()),
+ );
+ }
+
+ public function testCreateIterable()
+ {
+ $this->assertEquals(new CollectionType(new BuiltinType(BuiltinTypeEnum::ITERABLE)), Type::iterable());
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ITERABLE),
+ new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING)),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ )),
+ Type::iterable(Type::bool()),
+ );
+
+ $this->assertEquals(
+ new CollectionType(new GenericType(
+ new BuiltinType(BuiltinTypeEnum::ITERABLE),
+ new BuiltinType(BuiltinTypeEnum::STRING),
+ new BuiltinType(BuiltinTypeEnum::BOOL),
+ )),
+ Type::iterable(Type::bool(), Type::string()),
+ );
+ }
+
+ public function testCreateObject()
+ {
+ $this->assertEquals(new BuiltinType(BuiltinTypeEnum::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(BuiltinTypeEnum::STRING)), Type::enum(DummyBackedEnum::class));
+ $this->assertEquals(
+ new BackedEnumType(DummyBackedEnum::class, new BuiltinType(BuiltinTypeEnum::INT)),
+ Type::enum(DummyBackedEnum::class, new BuiltinType(BuiltinTypeEnum::INT)),
+ );
+ }
+
+ public function testCannotCreateUnitEnumWithBackingType()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ Type::enum(DummyEnum::class, new BuiltinType(BuiltinTypeEnum::INT));
+ }
+
+ public function testCreateGeneric()
+ {
+ $this->assertEquals(
+ new GenericType(new ObjectType(self::class), new BuiltinType(BuiltinTypeEnum::INT)),
+ Type::generic(Type::object(self::class), Type::int()),
+ );
+ }
+
+ public function testCreateTemplate()
+ {
+ $this->assertEquals(new TemplateType('T'), Type::template('T'));
+ }
+
+ public function testCreateUnion()
+ {
+ $this->assertEquals(new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new ObjectType(self::class)), Type::union(Type::int(), Type::object(self::class)));
+ $this->assertEquals(new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING)), Type::union(Type::int(), Type::string(), Type::int()));
+ $this->assertEquals(new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING)), Type::union(Type::int(), Type::union(Type::int(), Type::string())));
+ }
+
+ public function testCreateIntersection()
+ {
+ $this->assertEquals(new IntersectionType(new BuiltinType(BuiltinTypeEnum::INT), new ObjectType(self::class)), Type::intersection(Type::int(), Type::object(self::class)));
+ $this->assertEquals(new IntersectionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING)), Type::intersection(Type::int(), Type::string(), Type::int()));
+ $this->assertEquals(new IntersectionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING)), Type::intersection(Type::int(), Type::intersection(Type::int(), Type::string())));
+ }
+
+ public function testCreateNullable()
+ {
+ $this->assertEquals(new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::NULL)), Type::nullable(Type::int()));
+ $this->assertEquals(new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::NULL)), Type::nullable(Type::nullable(Type::int())));
+
+ $this->assertEquals(
+ new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING), new BuiltinType(BuiltinTypeEnum::NULL)),
+ Type::nullable(Type::union(Type::int(), Type::string())),
+ );
+ $this->assertEquals(
+ new UnionType(new BuiltinType(BuiltinTypeEnum::INT), new BuiltinType(BuiltinTypeEnum::STRING), new BuiltinType(BuiltinTypeEnum::NULL)),
+ Type::nullable(Type::union(Type::int(), Type::string(), Type::null())),
+ );
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ChainTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ChainTypeResolverTest.php
new file mode 100644
index 0000000000000..b3a0ec7c6d518
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ChainTypeResolverTest.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\Tests\TypeResolver;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeResolver\ChainTypeResolver;
+use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
+
+class ChainTypeResolverTest extends TestCase
+{
+ public function testResolve()
+ {
+ $firstResolver = $this->createMock(TypeResolverInterface::class);
+ $firstResolver->method('resolve')->willThrowException(new UnsupportedException('cannot resolve.'));
+
+ $secondResolver = $this->createMock(TypeResolverInterface::class);
+ $secondResolver->method('resolve')->willReturn(Type::int());
+
+ $thirdResolver = $this->createMock(TypeResolverInterface::class);
+ $thirdResolver->method('resolve')->willReturn(Type::string());
+
+ $resolver = new ChainTypeResolver([$firstResolver, $secondResolver, $thirdResolver]);
+
+ $this->assertEquals(Type::int(), $resolver->resolve('useless'));
+ }
+
+ public function testCannotResolveIfNoResolverCan()
+ {
+ $this->expectException(UnsupportedException::class);
+
+ $resolver = new ChainTypeResolver([]);
+ $resolver->resolve('int');
+ }
+}
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..2620b5d3e8e56
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionParameterTypeResolverTest.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\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 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..97ea248b4b029
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php
@@ -0,0 +1,120 @@
+
+ *
+ * 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 unhandledTypesDataProvider
+ */
+ public function testCannotResolveInvalidTypes(\ReflectionType $reflection)
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve($reflection);
+ }
+
+ /**
+ * @return iterable
+ */
+ public function unhandledTypesDataProvider(): iterable
+ {
+ $reflection = new \ReflectionClass(ReflectionExtractableDummy::class);
+
+ yield [$reflection->getMethod('getVoid')->getReturnType()];
+ yield [$reflection->getMethod('getNever')->getReturnType()];
+ }
+
+ /**
+ * @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..17fe49d051f7c
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
@@ -0,0 +1,180 @@
+
+ *
+ * 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\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();
+
+ // callable
+ yield [Type::callable(), 'callable(string, int): mixed'];
+
+ // array
+ yield [Type::list(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::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'), 'T'];
+
+ // 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);
+ }
+
+ /**
+ * @dataProvider unhandledTypesDataProvider
+ */
+ public function testCannotResolveInvalidTypes(string $string)
+ {
+ $this->expectException(UnsupportedException::class);
+ $this->resolver->resolve($string);
+ }
+
+ /**
+ * @return iterable
+ */
+ public function unhandledTypesDataProvider(): iterable
+ {
+ yield ['void'];
+ yield ['never'];
+ yield ['never-return'];
+ yield ['never-returns'];
+ yield ['no-return'];
+ }
+
+ public function testCannotResolveThisWithoutTypeContext()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->resolver->resolve('$this');
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php
new file mode 100644
index 0000000000000..97bd155cd56cd
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php
@@ -0,0 +1,75 @@
+
+ *
+ * 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\BuiltinType;
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+
+class TypeTest extends TestCase
+{
+ public function testIs()
+ {
+ $isInt = fn (Type $t) => BuiltinType::INT === $t->getBaseType()->getBuiltinType();
+
+ $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 testIsBuiltinType()
+ {
+ $this->assertTrue(Type::int()->isBuiltinType(BuiltinType::INT));
+ $this->assertTrue(Type::union(Type::string(), Type::int())->isBuiltinType(BuiltinType::INT));
+ $this->assertTrue(Type::generic(Type::int(), Type::string())->isBuiltinType(BuiltinType::INT));
+
+ $this->assertFalse(Type::string()->isBuiltinType(BuiltinType::INT));
+ $this->assertFalse(Type::union(Type::string(), Type::float())->isBuiltinType(BuiltinType::INT));
+ $this->assertFalse(Type::generic(Type::string(), Type::int())->isBuiltinType(BuiltinType::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::mixed(), Type::string())->isNullable());
+
+ $this->assertFalse(Type::int()->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(BuiltinType::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..61807301f9af4
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type.php
@@ -0,0 +1,83 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+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
+ {
+ if ($this instanceof UnionType) {
+ return $this->atLeastOneTypeIs($callable);
+ }
+
+ if ($this instanceof IntersectionType) {
+ return $this->everyTypeIs($callable);
+ }
+
+ return $callable($this);
+ }
+
+ public function isBuiltinType(BuiltinTypeEnum $builtinType): bool
+ {
+ return $this->is(static function (self $t) use ($builtinType): bool {
+ try {
+ $b = $t->getBaseType();
+ } catch (\LogicException) {
+ return false;
+ }
+
+ return $builtinType === $b->getBuiltinType();
+ });
+ }
+
+ public function isNullable(): bool
+ {
+ return $this->isBuiltinType(BuiltinTypeEnum::NULL) || $this->isBuiltinType(BuiltinTypeEnum::MIXED);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/BackedEnumType.php b/src/Symfony/Component/TypeInfo/Type/BackedEnumType.php
new file mode 100644
index 0000000000000..cfbd01ab75610
--- /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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class BackedEnumType extends EnumType
+{
+ /**
+ * @param class-string $className
+ */
+ public function __construct(
+ string $className,
+ private readonly BuiltinType $backingType,
+ ) {
+ if (!is_subclass_of($className, \BackedEnum::class)) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a valid backed enum.', $className));
+ }
+
+ if (!\in_array($backingType->getBuiltinType(), [BuiltinTypeEnum::INT, BuiltinTypeEnum::STRING], true)) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a valid enum backing type.', $backingType));
+ }
+
+ parent::__construct($className);
+ }
+
+ 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..dd60b51c052de
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/BuiltinType.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\Type;
+
+use Symfony\Component\TypeInfo\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class BuiltinType extends Type
+{
+ public function __construct(
+ private readonly BuiltinTypeEnum $builtinType,
+ ) {
+ }
+
+ public function getBuiltinType(): BuiltinTypeEnum
+ {
+ return $this->builtinType;
+ }
+
+ public function __toString(): string
+ {
+ return $this->builtinType->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..fff51548568f9
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/CollectionType.php
@@ -0,0 +1,76 @@
+
+ *
+ * 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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class CollectionType extends Type
+{
+ public function __construct(
+ private readonly BuiltinType|ObjectType|GenericType $type,
+ ) {
+ }
+
+ public function getType(): BuiltinType|ObjectType|GenericType
+ {
+ return $this->type;
+ }
+
+ public function getCollectionKeyType(): Type
+ {
+ $defaultCollectionKeyType = self::union(self::int(), self::string());
+
+ if ($this->type instanceof GenericType) {
+ return match (\count($this->type->getGenericTypes())) {
+ 2 => $this->type->getGenericTypes()[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->getGenericTypes())) {
+ 2 => $this->type->getGenericTypes()[1],
+ 1 => $this->type->getGenericTypes()[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..9d99436d46ba0
--- /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 (true === $callable($t)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param callable(T): bool $callable
+ */
+ public function everyTypeIs(callable $callable): bool
+ {
+ foreach ($this->types as $t) {
+ if (false === $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..41c6149fe9e4e
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/EnumType.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\Type;
+
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class EnumType extends ObjectType
+{
+ /**
+ * @param class-string $className
+ */
+ public function __construct(string $className)
+ {
+ if (!is_subclass_of($className, \UnitEnum::class)) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a valid enum.', $className));
+ }
+
+ parent::__construct($className);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/GenericType.php b/src/Symfony/Component/TypeInfo/Type/GenericType.php
new file mode 100644
index 0000000000000..61509bed7b038
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/GenericType.php
@@ -0,0 +1,70 @@
+
+ *
+ * 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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class GenericType extends Type
+{
+ /**
+ * @var list
+ */
+ private readonly array $genericTypes;
+
+ public function __construct(
+ private readonly BuiltinType|ObjectType $type,
+ Type ...$genericTypes,
+ ) {
+ $this->genericTypes = $genericTypes;
+ }
+
+ public function getType(): BuiltinType|ObjectType
+ {
+ return $this->type;
+ }
+
+ /**
+ * @return list
+ */
+ public function getGenericTypes(): array
+ {
+ return $this->genericTypes;
+ }
+
+ public function __toString(): string
+ {
+ $typeString = (string) $this->type;
+
+ $genericTypesString = '';
+ $glue = '';
+ foreach ($this->genericTypes as $t) {
+ $genericTypesString .= $glue.((string) $t);
+ $glue = ',';
+ }
+
+ return $typeString.'<'.$genericTypesString.'>';
+ }
+
+ /**
+ * 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..2ff6ba3877454
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/IntersectionType.php
@@ -0,0 +1,41 @@
+
+ *
+ * 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;
+
+/**
+ * @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;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/ObjectType.php b/src/Symfony/Component/TypeInfo/Type/ObjectType.php
new file mode 100644
index 0000000000000..cd8ce201a0d3d
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/ObjectType.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\BuiltinType;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+class ObjectType extends Type
+{
+ /**
+ * @param class-string $className
+ */
+ public function __construct(
+ private readonly string $className,
+ ) {
+ }
+
+ public function getBuiltinType(): BuiltinType
+ {
+ return BuiltinType::OBJECT;
+ }
+
+ /**
+ * @return class-string
+ */
+ public function getClassName(): string
+ {
+ return $this->className;
+ }
+
+ /**
+ * @return class-string
+ */
+ 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..72493bbdebef2
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/TemplateType.php
@@ -0,0 +1,36 @@
+
+ *
+ * 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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class TemplateType extends Type
+{
+ public function __construct(
+ private readonly string $template,
+ ) {
+ }
+
+ public function getTemplate(): string
+ {
+ return $this->template;
+ }
+
+ public function __toString(): string
+ {
+ return $this->template;
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/Type/UnionType.php b/src/Symfony/Component/TypeInfo/Type/UnionType.php
new file mode 100644
index 0000000000000..06570bafc0263
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/Type/UnionType.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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ *
+ * @template T of Type
+ */
+final class UnionType extends Type
+{
+ /**
+ * @use CompositeTypeTrait
+ */
+ use CompositeTypeTrait;
+
+ public function asNonNullable(): Type
+ {
+ $types = array_values(array_filter($this->getTypes(), fn (Type $t): bool => !$t->isBuiltinType(BuiltinTypeEnum::NULL)));
+
+ return \count($types) > 1 ? new self(...$types) : $types[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..4e0e98e8769ac
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContext.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\TypeContext;
+
+use Symfony\Component\TypeInfo\Exception\LogicException;
+use Symfony\Component\TypeInfo\Type;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class TypeContext
+{
+ /**
+ * @var array
+ */
+ private static array $classExistCache = [];
+
+ /**
+ * @param array $uses
+ * @param list $classTemplates
+ */
+ public function __construct(
+ public readonly string $calledClassName,
+ public readonly string $declaringClassName,
+ public readonly ?string $namespace = null,
+ public readonly array $uses = [],
+ public readonly array $classTemplates = [],
+ ) {
+ }
+
+ public function resolve(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 resolveDeclaringClass(): string
+ {
+ return $this->resolve($this->declaringClassName);
+ }
+
+ /**
+ * @return class-string
+ */
+ public function resolveCalledClass(): string
+ {
+ return $this->resolve($this->calledClassName);
+ }
+
+ /**
+ * @return class-string
+ */
+ public function resolveParentClass(): string
+ {
+ $declaringClassName = $this->resolveDeclaringClass();
+
+ 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->resolve(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..a01c6a3567252
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php
@@ -0,0 +1,172 @@
+
+ *
+ * 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 phpDocumentor\Reflection\Types\ContextFactory;
+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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class TypeContextFactory
+{
+ /**
+ * @var array
+ */
+ private static array $reflectionClassCache = [];
+
+ private ?ContextFactory $phpDocumentorContextFactory = null;
+ 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
+ {
+ if (!class_exists(ContextFactory::class)) {
+ throw new \LogicException(sprintf('Unable to call "%s()" as the "phpdocumentor/type-resolver" package is not installed. Try running composer require "phpdocumentor/type-resolver".', __METHOD__));
+ }
+
+ $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->collectClassTemplates($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),
+ );
+
+ return new TypeContext(
+ $typeContext->calledClassName,
+ $typeContext->declaringClassName,
+ $typeContext->namespace,
+ $typeContext->uses,
+ $this->collectClassTemplates($declaringClassReflection, $typeContext),
+ );
+ }
+
+ /**
+ * @return array
+ */
+ private function collectUses(\ReflectionClass $reflection): array
+ {
+ $fileName = $reflection->getFileName();
+ if (!\is_string($fileName) || !is_file($fileName)) {
+ return [];
+ }
+
+ if (false === $contents = @file_get_contents($fileName)) {
+ throw new RuntimeException(sprintf('Unable to read file "%s".', $fileName));
+ }
+
+ $traitUses = [];
+ foreach ($reflection->getTraits() as $traitReflection) {
+ $traitUses[] = $this->collectUses($traitReflection);
+ }
+
+ $uses = array_merge(...$traitUses);
+
+ $this->phpDocumentorContextFactory ??= new ContextFactory();
+ $context = $this->phpDocumentorContextFactory->createForNamespace($reflection->getNamespaceName(), $contents);
+
+ return array_merge($context->getNamespaceAliases(), ...$traitUses);
+ }
+
+ /**
+ * @return list
+ */
+ private function collectClassTemplates(\ReflectionClass $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 = null;
+ $typeString = ((string) $tag->value->bound) ?: null;
+
+ try {
+ if (null !== $typeString) {
+ $type = $this->stringTypeResolver->resolve($typeString);
+ }
+ } catch (UnsupportedException) {
+ }
+
+ $templates[] = ['name' => $tag->value->name, 'type' => $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..dd00e7d04d590
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php
@@ -0,0 +1,221 @@
+
+ *
+ * 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\BuiltinType as BuiltinTypeEnum;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+trait TypeFactoryTrait
+{
+ public static function builtin(BuiltinTypeEnum|string $type): BuiltinType
+ {
+ return new BuiltinType(\is_string($type) ? BuiltinTypeEnum::from($type) : $type);
+ }
+
+ public static function int(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::INT);
+ }
+
+ public static function float(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::FLOAT);
+ }
+
+ public static function string(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::STRING);
+ }
+
+ public static function bool(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::BOOL);
+ }
+
+ public static function resource(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::RESOURCE);
+ }
+
+ public static function false(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::FALSE);
+ }
+
+ public static function true(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::TRUE);
+ }
+
+ public static function callable(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::CALLABLE);
+ }
+
+ public static function mixed(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::MIXED);
+ }
+
+ public static function null(): BuiltinType
+ {
+ return self::builtin(BuiltinTypeEnum::NULL);
+ }
+
+ public static function collection(Type $type, Type $value = null, Type $key = null): CollectionType
+ {
+ if (null !== $value || null !== $key) {
+ $type = self::generic($type, $key ?? self::union(self::int(), self::string()), $value ?? self::mixed());
+ }
+
+ return new CollectionType($type);
+ }
+
+ public static function array(Type $value = null, Type $key = null): CollectionType
+ {
+ return self::collection(self::builtin(BuiltinTypeEnum::ARRAY), $value, $key);
+ }
+
+ public static function iterable(Type $value = null, Type $key = null): CollectionType
+ {
+ return self::collection(self::builtin(BuiltinTypeEnum::ITERABLE), $value, $key);
+ }
+
+ public static function list(Type $value = null): CollectionType
+ {
+ return self::array($value, self::int());
+ }
+
+ public static function dict(Type $value = null): CollectionType
+ {
+ return self::array($value, self::string());
+ }
+
+ /**
+ * @param class-string|null $className
+ */
+ public static function object(string $className = null): BuiltinType|ObjectType
+ {
+ return null !== $className ? new ObjectType($className) : new BuiltinType(BuiltinTypeEnum::OBJECT);
+ }
+
+ /**
+ * @param class-string $className
+ */
+ public static function enum(string $className, Type $backingType = null): EnumType
+ {
+ if (is_subclass_of($className, \BackedEnum::class)) {
+ if (null === $backingType) {
+ $reflectionBackingType = (new \ReflectionEnum($className))->getBackingType();
+ $builtinType = BuiltinTypeEnum::INT->value === (string) $reflectionBackingType ? BuiltinTypeEnum::INT : BuiltinTypeEnum::STRING;
+ $backingType = new BuiltinType($builtinType);
+ }
+
+ $type = new BackedEnumType($className, $backingType);
+ } else {
+ if (null !== $backingType) {
+ throw new InvalidArgumentException(sprintf('Cannot set a backing type for "%s" as it is not a backed enum.', $className));
+ }
+
+ $type = new EnumType($className);
+ }
+
+ return $type;
+ }
+
+ public static function generic(Type $mainType, Type ...$parametersType): GenericType
+ {
+ return new GenericType($mainType, ...$parametersType);
+ }
+
+ public static function template(string $template): TemplateType
+ {
+ return new TemplateType($template);
+ }
+
+ /**
+ * @param list $types
+ *
+ * @return UnionType
+ */
+ public static function union(Type ...$types): UnionType
+ {
+ $unionTypes = [];
+
+ foreach ($types as $type) {
+ if (!$type instanceof UnionType) {
+ $unionTypes[] = $type;
+
+ continue;
+ }
+
+ foreach ($type->getTypes() as $unionType) {
+ $unionTypes[] = $unionType;
+ }
+ }
+
+ return new UnionType(...$unionTypes);
+ }
+
+ /**
+ * @param list $types
+ *
+ * @return IntersectionType
+ */
+ public static function intersection(Type ...$types): IntersectionType
+ {
+ $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 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/TypeResolver/ChainTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ChainTypeResolver.php
new file mode 100644
index 0000000000000..3f7ff8e67882a
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ChainTypeResolver.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\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final readonly class ChainTypeResolver implements TypeResolverInterface
+{
+ /**
+ * @param iterable $typeResolvers
+ */
+ public function __construct(
+ private iterable $typeResolvers,
+ ) {
+ }
+
+ public function resolve(mixed $subject, TypeContext $typeContext = null): Type
+ {
+ foreach ($this->typeResolvers as $typeResolver) {
+ try {
+ return $typeResolver->resolve($subject, $typeContext);
+ } catch (UnsupportedException) {
+ }
+ }
+
+ throw new UnsupportedException('Cannot resolve type.');
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionParameterTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionParameterTypeResolver.php
new file mode 100644
index 0000000000000..10f820dddd4d2
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionParameterTypeResolver.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\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+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)));
+ }
+
+ $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), 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..7480e297d9c44
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionPropertyTypeResolver.php
@@ -0,0 +1,47 @@
+
+ *
+ * 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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+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)));
+ }
+
+ $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), 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..6d05e174bb164
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionReturnTypeResolver.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\TypeResolver;
+
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+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)));
+ }
+
+ $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), 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..f0825241f1f67
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.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 Symfony\Component\TypeInfo\BuiltinType;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Exception\UnsupportedException;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\TypeContext\TypeContext;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class ReflectionTypeResolver implements TypeResolverInterface
+{
+ /**
+ * @var array
+ */
+ private static array $reflectionEnumCache = [];
+
+ private const UNSUPPORTED_BUILTIN_TYPES = ['void', 'never'];
+
+ 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)));
+ }
+
+ $builtinTypeOrClass = $subject->getName();
+ $nullable = $subject->allowsNull();
+
+ if (\in_array($builtinTypeOrClass, self::UNSUPPORTED_BUILTIN_TYPES, true)) {
+ throw new UnsupportedException(sprintf('"%s" type is not supported.', $builtinTypeOrClass));
+ }
+
+ if (BuiltinType::ARRAY->value === $builtinTypeOrClass) {
+ $type = Type::array();
+
+ return $nullable ? Type::nullable($type) : $type;
+ }
+
+ if (BuiltinType::ITERABLE->value === $builtinTypeOrClass) {
+ $type = Type::iterable();
+
+ return $nullable ? Type::nullable($type) : $type;
+ }
+
+ if (\in_array($builtinTypeOrClass, [BuiltinType::MIXED->value, BuiltinType::NULL->value], true)) {
+ return Type::builtin($builtinTypeOrClass);
+ }
+
+ if ($subject->isBuiltin()) {
+ $type = Type::builtin(BuiltinType::from($builtinTypeOrClass));
+
+ return $nullable ? Type::nullable($type) : $type;
+ }
+
+ if (\in_array(strtolower($builtinTypeOrClass), ['self', 'static', 'parent'], true) && !$typeContext) {
+ throw new InvalidArgumentException(sprintf('A "%s" must be provided to resolve "%s".', TypeContext::class, strtolower($builtinTypeOrClass)));
+ }
+
+ /** @var class-string $className */
+ $className = match (true) {
+ 'self' === strtolower($builtinTypeOrClass) => $typeContext->resolveDeclaringClass(),
+ 'static' === strtolower($builtinTypeOrClass) => $typeContext->resolveCalledClass(),
+ 'parent' === strtolower($builtinTypeOrClass) => $typeContext->resolveParentClass(),
+ default => $builtinTypeOrClass,
+ };
+
+ 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..c4400c541a93e
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
@@ -0,0 +1,251 @@
+
+ *
+ * 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\BuiltinType;
+use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
+use Symfony\Component\TypeInfo\Exception\LogicException;
+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;
+
+/**
+ * @author Mathias Arlaud
+ * @author Baptiste Leduc
+ */
+final class StringTypeResolver implements TypeResolverInterface
+{
+ /**
+ * @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 (!class_exists(TypeParser::class)) {
+ throw new LogicException(sprintf('Unable to call "%s()" as the "phpstan/phpdoc-parser" package is not installed. Try running composer require "phpstan/phpdoc-parser".', __METHOD__));
+ }
+
+ if (!\is_string($subject)) {
+ throw new UnsupportedException(sprintf('Expected subject to be a "string", "%s" given.', get_debug_type($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), previous: $e);
+ }
+ }
+
+ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Type
+ {
+ if ($node instanceof CallableTypeNode) {
+ return Type::callable();
+ }
+
+ if ($node instanceof ArrayTypeNode) {
+ return Type::list($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->resolveCalledClass());
+ }
+
+ 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' => Type::object($typeContext->resolveDeclaringClass()),
+ 'static' => Type::object($typeContext->resolveCalledClass()),
+ 'parent' => Type::object($typeContext->resolveParentClass()),
+ 'void', 'never', 'never-return', 'never-returns', 'no-return' => throw new \DomainException(sprintf('Unhandled "%s" identifier.', $node->name)),
+ default => $this->resolveCustomIdentifier($node->name, $typeContext),
+ };
+
+ if ($type instanceof ObjectType && \in_array($type->getClassName(), [\Traversable::class, \Iterator::class, \IteratorAggregate::class], 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->isBuiltinType(BuiltinType::INT)) {
+ return $type;
+ }
+
+ $genericTypes = 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($genericTypes)) {
+ return Type::collection($type, $genericTypes[0], $keyType);
+ } elseif (2 === \count($genericTypes)) {
+ return Type::collection($type, $genericTypes[1], $genericTypes[0]);
+ }
+ }
+
+ if ($type instanceof ObjectType && \in_array($type->getClassName(), [\Traversable::class, \Iterator::class, \IteratorAggregate::class], true)) {
+ if (1 === \count($genericTypes)) {
+ return Type::collection($type, $genericTypes[0], null);
+ } elseif (2 === \count($genericTypes)) {
+ return Type::collection($type, $genericTypes[1], $genericTypes[0]);
+ }
+
+ return Type::collection($type);
+ }
+
+ return Type::generic($type, ...$genericTypes);
+ }
+
+ 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
+ {
+ $classNameOrTemplate = $typeContext ? $typeContext->resolve($identifier) : $identifier;
+
+ if (isset(self::$classExistCache[$classNameOrTemplate])) {
+ return self::$classExistCache[$classNameOrTemplate] ? Type::object($classNameOrTemplate) : Type::template($classNameOrTemplate);
+ }
+
+ if (class_exists($classNameOrTemplate) || interface_exists($classNameOrTemplate)) {
+ self::$classExistCache[$classNameOrTemplate] = true;
+
+ return Type::object($classNameOrTemplate);
+ }
+
+ try {
+ new \ReflectionClass($classNameOrTemplate);
+ self::$classExistCache[$classNameOrTemplate] = true;
+
+ return Type::object($classNameOrTemplate);
+ } catch (\Throwable) {
+ }
+
+ self::$classExistCache[$classNameOrTemplate] = false;
+
+ return Type::template($classNameOrTemplate);
+ }
+}
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolverInterface.php b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolverInterface.php
new file mode 100644
index 0000000000000..5de6f9c3c6144
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/TypeResolverInterface.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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;
+
+/**
+ * @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..98115111e8efc
--- /dev/null
+++ b/src/Symfony/Component/TypeInfo/composer.json
@@ -0,0 +1,41 @@
+{
+ "name": "symfony/type-info",
+ "type": "library",
+ "description": "Extracts information about PHP types using metadata of popular sources",
+ "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"
+ },
+ "require-dev": {
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "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
+
+
+
diff --git a/src/Symfony/Component/VarDumper/Caster/DsPairStub.php b/src/Symfony/Component/VarDumper/Caster/DsPairStub.php
index 22112af9c073d..afa2727b11b77 100644
--- a/src/Symfony/Component/VarDumper/Caster/DsPairStub.php
+++ b/src/Symfony/Component/VarDumper/Caster/DsPairStub.php
@@ -18,7 +18,7 @@
*/
class DsPairStub extends Stub
{
- public function __construct(string|int $key, mixed $value)
+ public function __construct(mixed $key, mixed $value)
{
$this->value = [
Caster::PREFIX_VIRTUAL.'key' => $key,
diff --git a/src/Symfony/Component/Webhook/Client/RequestParser.php b/src/Symfony/Component/Webhook/Client/RequestParser.php
index dc81765abf3a3..3b4b2a922cf86 100644
--- a/src/Symfony/Component/Webhook/Client/RequestParser.php
+++ b/src/Symfony/Component/Webhook/Client/RequestParser.php
@@ -18,6 +18,7 @@
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\RemoteEvent\RemoteEvent;
+use Symfony\Component\Webhook\Exception\InvalidArgumentException;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
/**
@@ -43,6 +44,10 @@ protected function getRequestMatcher(): RequestMatcherInterface
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): RemoteEvent
{
+ if (!$secret) {
+ throw new InvalidArgumentException('A non-empty secret is required.');
+ }
+
$body = $request->toArray();
foreach ([$this->signatureHeaderName, $this->eventHeaderName, $this->idHeaderName] as $header) {