diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 2f2df85356e02..1d942f6841240 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -101,6 +101,7 @@ use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ControllerValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; @@ -246,12 +247,14 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('validator.translation_domain', 'validators'); } + $loader->load('argument_resolver.php'); $loader->load('web.php'); $loader->load('services.php'); $loader->load('fragment_renderer.php'); $loader->load('error_renderer.php'); if (!ContainerBuilder::willBeAvailable('symfony/clock', ClockInterface::class, ['symfony/framework-bundle'])) { + $container->removeDefinition('clock'); $container->removeDefinition('clock'); $container->removeAlias(ClockInterface::class); $container->removeAlias(PsrClockInterface::class); @@ -425,6 +428,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerSerializerConfiguration($config['serializer'], $container, $loader); } else { + // @deprecated since Symfony 7.3 $container->getDefinition('argument_resolver.request_payload') ->setArguments([]) ->addError('You can neither use "#[MapRequestPayload]" nor "#[MapQueryString]" since the Serializer component is not ' @@ -433,6 +437,14 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('container.error') ->clearTag('kernel.event_subscriber'); + /*$container->getDefinition('argument_resolver.controller.request_payload') + ->setArguments([]) + ->addError('You can neither use "#[MapRequestPayload]" nor "#[MapQueryString]" since the Serializer component is not ' + .(class_exists(Serializer::class) ? 'enabled. Try setting "framework.serializer.enabled" to true.' : 'installed. Try running "composer require symfony/serializer-pack".') + ) + ->addTag('container.error') + ->clearTag('kernel.event_subscriber'); + */ $container->removeDefinition('console.command.serializer_debug'); } @@ -483,7 +495,8 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerUidConfiguration($config['uid'], $container, $loader); } else { - $container->removeDefinition('argument_resolver.uid'); + $container->removeDefinition('argument_resolver.uid'); // @deprecated since Symfony 7.3 + $container->removeDefinition('argument_resolver.controller.uid'); } // register cache before session so both can share the connection services @@ -644,6 +657,9 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('container.service_locator'); $container->registerForAutoconfiguration(ServiceSubscriberInterface::class) ->addTag('container.service_subscriber'); + $container->registerForAutoconfiguration(ControllerValueResolverInterface::class) + ->addTag('controller.argument_value_resolver'); + // @deprecated since Symfony 7.3 $container->registerForAutoconfiguration(ValueResolverInterface::class) ->addTag('controller.argument_value_resolver'); $container->registerForAutoconfiguration(AbstractController::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/argument_resolver.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/argument_resolver.php new file mode 100644 index 0000000000000..341ee45c891ab --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/argument_resolver.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadataFactory; +use Symfony\Component\ArgumentResolver\ValueResolver\DefaultValueResolver; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('argument_resolver.argument_metadata_factory', ArgumentMetadataFactory::class) + + ->set('argument_resolver.default', DefaultValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => -100, 'name' => DefaultValueResolver::class]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php index 5c426653daeca..25d9679d08959 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php @@ -37,7 +37,7 @@ ]) ->set('debug.argument_resolver', TraceableArgumentResolver::class) - ->decorate('argument_resolver') + ->decorate('controller.argument_resolver') ->args([ service('debug.argument_resolver.inner'), service('debug.stopwatch'), diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 558c2b6d52334..5b024c50110db 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -91,7 +91,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] service('event_dispatcher'), service('controller_resolver'), service('request_stack'), - service('argument_resolver'), + service('controller.argument_resolver'), false, ]) ->tag('container.hot_path') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 6f8358fb0c7b8..47fd539305d12 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -14,18 +14,29 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\UidValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\DefaultValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ServiceValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\UidValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver as LegacyBackedEnumValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver as LegacyDateTimeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver as LegacyDefaultValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver as LegacyQueryParameterValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver as LegacyRequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver as LegacyRequestPayloadValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver as LegacyRequestValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver as LegacyServiceValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver as LegacySessionValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\UidValueResolver as LegacyUidValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\BackedEnumValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\DateTimeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\QueryParameterValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\RequestPayloadValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\RequestValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\SessionValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ValueResolver\VariadicValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver as LegacyVariadicValueResolver; +use Symfony\Component\HttpKernel\Controller\ControllerArgumentResolver; use Symfony\Component\HttpKernel\Controller\ErrorController; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\EventListener\CacheAttributeListener; @@ -45,28 +56,97 @@ ->call('allowControllers', [[AbstractController::class, TemplateController::class]]) ->tag('monolog.logger', ['channel' => 'request']) + # Deprecated Argument Resolver and Value Resolvers ->set('argument_metadata_factory', ArgumentMetadataFactory::class) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.argument_metadata_factory" instead.') - ->set('argument_resolver', ArgumentResolver::class) + ->set('argument_resolver', \Symfony\Component\HttpKernel\Controller\ArgumentResolver::class) ->args([ service('argument_metadata_factory'), abstract_arg('argument value resolvers'), abstract_arg('targeted value resolvers'), ]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller" instead.') - ->set('argument_resolver.backed_enum_resolver', BackedEnumValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => BackedEnumValueResolver::class]) + ->set('argument_resolver.backed_enum_resolver', LegacyBackedEnumValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => LegacyBackedEnumValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.backed_enum" instead.') - ->set('argument_resolver.uid', UidValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => UidValueResolver::class]) + ->set('argument_resolver.uid', LegacyUidValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => LegacyUidValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.uid" instead.') - ->set('argument_resolver.datetime', DateTimeValueResolver::class) + ->set('argument_resolver.datetime', LegacyDateTimeValueResolver::class) ->args([ service('clock')->nullOnInvalid(), ]) - ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => DateTimeValueResolver::class]) + ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => LegacyDateTimeValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.datetime" instead.') - ->set('argument_resolver.request_payload', RequestPayloadValueResolver::class) + ->set('argument_resolver.request_payload', LegacyRequestPayloadValueResolver::class) + ->args([ + service('serializer'), + service('validator')->nullOnInvalid(), + service('translator')->nullOnInvalid(), + param('validator.translation_domain'), + ]) + ->tag('controller.targeted_value_resolver', ['name' => LegacyRequestPayloadValueResolver::class]) + ->tag('kernel.event_subscriber') + ->lazy() + + ->set('argument_resolver.request_attribute', LegacyRequestAttributeValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => LegacyRequestAttributeValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.request_attribute" instead.') + + ->set('argument_resolver.request', LegacyRequestValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => LegacyRequestValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.request" instead.') + + ->set('argument_resolver.session', LegacySessionValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => LegacySessionValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.session" instead.') + + ->set('argument_resolver.service', LegacyServiceValueResolver::class) + ->args([ + abstract_arg('service locator, set in RegisterControllerArgumentLocatorsPass'), + ]) + ->tag('controller.argument_value_resolver', ['priority' => -50, 'name' => LegacyServiceValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.service" instead.') + + ->set('argument_resolver.default', LegacyDefaultValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => -100, 'name' => LegacyDefaultValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.default" instead.') + + ->set('argument_resolver.variadic', LegacyVariadicValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => -150, 'name' => LegacyVariadicValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.variadic" instead.') + + ->set('argument_resolver.query_parameter_value_resolver', LegacyQueryParameterValueResolver::class) + ->tag('controller.targeted_value_resolver', ['name' => LegacyQueryParameterValueResolver::class]) + ->deprecate('symfony/framework-bundle', '7.3', 'The "%service_id%" service is deprecated, use "argument_resolver.controller.query_parameter" instead.') + + # Controller Argument Resolver and Value Resolvers + ->set('controller.argument_resolver', ControllerArgumentResolver::class) + ->args([ + service('argument_resolver.argument_metadata_factory'), + abstract_arg('argument value resolvers'), + abstract_arg('targeted value resolvers'), + ]) + + /* + ->set('argument_resolver.controller.backed_enum', BackedEnumValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => 100, 'name' => BackedEnumValueResolver::class]) + + ->set('argument_resolver.controller.uid', UidValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => 100, 'name' => UidValueResolver::class]) + + ->set('argument_resolver.controller.datetime', DateTimeValueResolver::class) + ->args([ + service('clock')->nullOnInvalid(), + ]) + ->tag('argument_resolver.controller.value_resolver', ['priority' => 100, 'name' => DateTimeValueResolver::class]) + + ->set('argument_resolver.controller.request_payload', RequestPayloadValueResolver::class) ->args([ service('serializer'), service('validator')->nullOnInvalid(), @@ -77,29 +157,30 @@ ->tag('kernel.event_subscriber') ->lazy() - ->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => RequestAttributeValueResolver::class]) + ->set('argument_resolver.controller.request_attribute', RequestAttributeValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => 100, 'name' => RequestAttributeValueResolver::class]) - ->set('argument_resolver.request', RequestValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => RequestValueResolver::class]) + ->set('argument_resolver.controller.request', RequestValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => 50, 'name' => RequestValueResolver::class]) - ->set('argument_resolver.session', SessionValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => SessionValueResolver::class]) + ->set('argument_resolver.controller.session', SessionValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => 50, 'name' => SessionValueResolver::class]) - ->set('argument_resolver.service', ServiceValueResolver::class) + ->set('argument_resolver.controller.service', ServiceValueResolver::class) ->args([ abstract_arg('service locator, set in RegisterControllerArgumentLocatorsPass'), ]) - ->tag('controller.argument_value_resolver', ['priority' => -50, 'name' => ServiceValueResolver::class]) + ->tag('argument_resolver.controller.value_resolver', ['priority' => -50, 'name' => ServiceValueResolver::class]) - ->set('argument_resolver.default', DefaultValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => -100, 'name' => DefaultValueResolver::class]) + ->set('argument_resolver.controller.default', DefaultValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => -100, 'name' => DefaultValueResolver::class]) - ->set('argument_resolver.variadic', VariadicValueResolver::class) - ->tag('controller.argument_value_resolver', ['priority' => -150, 'name' => VariadicValueResolver::class]) + ->set('argument_resolver.controller.variadic', VariadicValueResolver::class) + ->tag('argument_resolver.controller.value_resolver', ['priority' => -150, 'name' => VariadicValueResolver::class]) - ->set('argument_resolver.query_parameter_value_resolver', QueryParameterValueResolver::class) + ->set('argument_resolver.controller.query_parameter_value_resolver', QueryParameterValueResolver::class) ->tag('controller.targeted_value_resolver', ['name' => QueryParameterValueResolver::class]) + */ ->set('response_listener', ResponseListener::class) ->args([ diff --git a/src/Symfony/Component/ArgumentResolver/.gitattributes b/src/Symfony/Component/ArgumentResolver/.gitattributes new file mode 100644 index 0000000000000..14c3c35940427 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.git* export-ignore diff --git a/src/Symfony/Component/ArgumentResolver/.github/PULL_REQUEST_TEMPLATE.md b/src/Symfony/Component/ArgumentResolver/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000..4689c4dad430e --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/src/Symfony/Component/ArgumentResolver/.github/workflows/close-pull-request.yml b/src/Symfony/Component/ArgumentResolver/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000000..e55b47817e69a --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! diff --git a/src/Symfony/Component/ArgumentResolver/.gitignore b/src/Symfony/Component/ArgumentResolver/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadata.php b/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadata.php new file mode 100644 index 0000000000000..976c63783b696 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadata.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ArgumentMetadata; + +/** + * Responsible for storing metadata of an argument. + * + * @author Iltar van der Berg + * + * @final + */ +class ArgumentMetadata +{ + public const IS_INSTANCEOF = 2; + + /** + * @param object[] $attributes + */ + public function __construct( + private string $name, + private ?string $type, + private bool $isVariadic, + private bool $hasDefaultValue, + private mixed $defaultValue, + private bool $isNullable = false, + private array $attributes = [], + private string $callableName = 'n/a', + ) { + $this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue); + } + + /** + * Returns the name as given in PHP, $foo would yield "foo". + */ + public function getName(): string + { + return $this->name; + } + + /** + * Returns the type of the argument. + * + * The type is the PHP class in 5.5+ and additionally the basic type in PHP 7.0+. + */ + public function getType(): ?string + { + return $this->type; + } + + /** + * Returns whether the argument is defined as "...$variadic". + */ + public function isVariadic(): bool + { + return $this->isVariadic; + } + + /** + * Returns whether the argument has a default value. + * + * Implies whether an argument is optional. + */ + public function hasDefaultValue(): bool + { + return $this->hasDefaultValue; + } + + /** + * Returns whether the argument accepts null values. + */ + public function isNullable(): bool + { + return $this->isNullable; + } + + /** + * Returns the default value of the argument. + * + * @throws \LogicException if no default value is present; {@see self::hasDefaultValue()} + */ + public function getDefaultValue(): mixed + { + if (!$this->hasDefaultValue) { + throw new \LogicException(\sprintf('Argument $%s does not have a default value. Use "%s::hasDefaultValue()" to avoid this exception.', $this->name, __CLASS__)); + } + + return $this->defaultValue; + } + + /** + * @param class-string $name + * @param self::IS_INSTANCEOF|0 $flags + * + * @return array + */ + public function getAttributes(?string $name = null, int $flags = 0): array + { + if (!$name) { + return $this->attributes; + } + + return $this->getAttributesOfType($name, $flags); + } + + /** + * @template T of object + * + * @param class-string $name + * @param self::IS_INSTANCEOF|0 $flags + * + * @return array + */ + public function getAttributesOfType(string $name, int $flags = 0): array + { + $attributes = []; + if ($flags & self::IS_INSTANCEOF) { + foreach ($this->attributes as $attribute) { + if ($attribute instanceof $name) { + $attributes[] = $attribute; + } + } + } else { + foreach ($this->attributes as $attribute) { + if ($attribute::class === $name) { + $attributes[] = $attribute; + } + } + } + + return $attributes; + } + + public function getCallableName(): string + { + return $this->callableName; + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadataFactory.php new file mode 100644 index 0000000000000..afd77a90da253 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadataFactory.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ArgumentMetadata; + +/** + * Builds {@see ArgumentMetadata} objects based on the given Controller. + * + * @author Iltar van der Berg + */ +final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface +{ + public function createArgumentMetadata(string|object|array $callable, ?\ReflectionFunctionAbstract $reflector = null): array + { + $arguments = []; + $reflector ??= new \ReflectionFunction($callable(...)); + $callableName = $this->getPrettyName($reflector); + + foreach ($reflector->getParameters() as $param) { + $attributes = []; + foreach ($param->getAttributes() as $reflectionAttribute) { + if (class_exists($reflectionAttribute->getName())) { + $attributes[] = $reflectionAttribute->newInstance(); + } + } + + $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attributes, $callableName); + } + + return $arguments; + } + + /** + * Returns an associated type to the given parameter if available. + */ + private function getType(\ReflectionParameter $parameter): ?string + { + if (!$type = $parameter->getType()) { + return null; + } + $name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; + + return match (strtolower($name)) { + 'self' => $parameter->getDeclaringClass()?->name, + 'parent' => get_parent_class($parameter->getDeclaringClass()?->name ?? '') ?: null, + default => $name, + }; + } + + private function getPrettyName(\ReflectionFunctionAbstract $r): string + { + $name = $r->name; + + if ($r instanceof \ReflectionMethod) { + return $r->class.'::'.$name; + } + + if ($r->isAnonymous() || !$class = $r->getClosureCalledClass()) { + return $name; + } + + return $class->name.'::'.$name; + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadataFactoryInterface.php b/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadataFactoryInterface.php new file mode 100644 index 0000000000000..c190cf3e09263 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ArgumentMetadata/ArgumentMetadataFactoryInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ArgumentMetadata; + +/** + * Builds method argument data. + * + * @author Iltar van der Berg + */ +interface ArgumentMetadataFactoryInterface +{ + /** + * @return ArgumentMetadata[] + */ + public function createArgumentMetadata(string|object|array $callable, ?\ReflectionFunctionAbstract $reflector = null): array; +} diff --git a/src/Symfony/Component/ArgumentResolver/ArgumentResolver.php b/src/Symfony/Component/ArgumentResolver/ArgumentResolver.php new file mode 100644 index 0000000000000..616944baff26e --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ArgumentResolver.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver; + +use Psr\Container\ContainerInterface; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadataFactory; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadataFactoryInterface; +use Symfony\Component\ArgumentResolver\Exception\InvalidArgumentException; +use Symfony\Component\ArgumentResolver\Exception\LogicException; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; +use Symfony\Component\ArgumentResolver\Exception\RuntimeException; +use Symfony\Component\ArgumentResolver\ValueResolver\DefaultValueResolver; +use Symfony\Component\ArgumentResolver\Attribute\ValueResolver; +use Symfony\Component\ArgumentResolver\Exception\ResolverNotFoundException; +use Symfony\Contracts\Service\ServiceProviderInterface; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; + +/** + * Responsible for resolving the arguments passed to a callable. + * + * @author Iltar van der Berg + * @author Robin Chalas + */ +class ArgumentResolver implements ArgumentResolverInterface +{ + private readonly ArgumentMetadataFactoryInterface $argumentMetadataFactory; + + /** + * @param iterable $argumentValueResolvers + */ + public function __construct( + ?ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, + private iterable $argumentValueResolvers = [], + private readonly ?ContainerInterface $namedResolvers = null, + ) { + $this->argumentMetadataFactory = $argumentMetadataFactory ?? new ArgumentMetadataFactory(); + $this->argumentValueResolvers = $argumentValueResolvers ?: static::getDefaultValueResolvers(); + } + + public function getArguments(mixed $input, callable $callable, ?\ReflectionFunctionAbstract $reflector = null): array + { + $arguments = []; + + foreach ($this->argumentMetadataFactory->createArgumentMetadata($callable, $reflector) as $metadata) { + $argumentValueResolvers = $this->argumentValueResolvers; + $disabledResolvers = []; + + if ($this->namedResolvers && $attributes = $metadata->getAttributesOfType(ValueResolver::class, $metadata::IS_INSTANCEOF)) { + $resolverName = null; + foreach ($attributes as $attribute) { + if ($attribute->disabled) { + $disabledResolvers[$attribute->resolver] = true; + } elseif ($resolverName) { + throw new LogicException(\sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $metadata->getName(), $metadata->getCallableName())); + } else { + $resolverName = $attribute->resolver; + } + } + + if ($resolverName) { + if (!$this->namedResolvers->has($resolverName)) { + throw new ResolverNotFoundException($resolverName, $this->namedResolvers instanceof ServiceProviderInterface ? array_keys($this->namedResolvers->getProvidedServices()) : []); + } + + $argumentValueResolvers = [$this->namedResolvers->get($resolverName), ...$this->getExtraValueResolversForNamed()]; + } + } + + $valueResolverExceptions = []; + foreach ($argumentValueResolvers as $name => $resolver) { + if (isset($disabledResolvers[\is_int($name) ? $resolver::class : $name])) { + continue; + } + + try { + $count = 0; + foreach ($this->callResolver($resolver, $metadata, $input) as $argument) { + ++$count; + $arguments[] = $argument; + } + } catch (NearMissValueResolverException $e) { + $valueResolverExceptions[] = $e; + } + + if (1 < $count && !$metadata->isVariadic()) { + throw new InvalidArgumentException(\sprintf('"%s::resolve()" must yield at most one value for non-variadic arguments.', get_debug_type($resolver))); + } + + if ($count) { + // continue to the next controller argument + continue 2; + } + } + + $reasons = array_map(static fn (NearMissValueResolverException $e) => $e->getMessage(), $valueResolverExceptions); + if (!$reasons) { + $reasons[] = 'Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.'; + } + + $reasonCounter = 1; + if (\count($reasons) > 1) { + foreach ($reasons as $i => $reason) { + $reasons[$i] = $reasonCounter.') '.$reason; + ++$reasonCounter; + } + } + + throw new RuntimeException(\sprintf('Controller "%s" requires the "$%s" argument that could not be resolved. '.($reasonCounter > 1 ? 'Possible reasons: ' : '').'%s', $metadata->getCallableName(), $metadata->getName(), implode(' ', $reasons))); + } + + return $arguments; + } + + protected static function getDefaultValueResolvers(): iterable + { + return [ + new DefaultValueResolver(), + ]; + } + + protected static function getExtraValueResolversForNamed(): array + { + return [ + new DefaultValueResolver(), + ]; + } + + /** + * @param ValueResolverInterface $resolver + */ + protected function callResolver($resolver, ArgumentMetadata $metadata, mixed $input): iterable + { + return $resolver->resolveArgument($metadata, $input); + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ArgumentResolverInterface.php b/src/Symfony/Component/ArgumentResolver/ArgumentResolverInterface.php new file mode 100644 index 0000000000000..8a7846138643c --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ArgumentResolverInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver; + + +use Symfony\Component\ArgumentResolver\ArgumentValueSource\ValueSourceInterface; + +/** + * An ArgumentResolverInterface instance knows how to determine the + * arguments for a specific function. + * + * @author Robin Chalas + * @author Fabien Potencier + */ +interface ArgumentResolverInterface +{ + /** + * Returns the arguments to pass to the callable. + * + * @param mixed $input The source from which raw values should be found e.g. an HTTP request + * + * @throws \RuntimeException When no value could be provided for a required argument + */ + public function getArguments(ValueSourceInterface $source, callable $callable, ?\ReflectionFunctionAbstract $reflector = null): array; +} diff --git a/src/Symfony/Component/ArgumentResolver/Attribute/AsTargetedValueResolver.php b/src/Symfony/Component/ArgumentResolver/Attribute/AsTargetedValueResolver.php new file mode 100644 index 0000000000000..b62cf809d821c --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Attribute/AsTargetedValueResolver.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Attribute; + +/** + * Service tag to autoconfigure targeted value resolvers. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsTargetedValueResolver +{ + /** + * @param string|null $name The name with which the resolver can be targeted + */ + public function __construct(public readonly ?string $name = null) + { + } +} diff --git a/src/Symfony/Component/ArgumentResolver/Attribute/MapDateTime.php b/src/Symfony/Component/ArgumentResolver/Attribute/MapDateTime.php new file mode 100644 index 0000000000000..8c3f90c67c4fb --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Attribute/MapDateTime.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Attribute; + +use Symfony\Component\ArgumentResolver\ValueResolver\DateTimeValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; + +/** + * Controller parameter tag to configure DateTime arguments. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class MapDateTime extends ValueResolver +{ + /** + * @param string|null $format The DateTime format to use, @see https://php.net/datetime.format + * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases + * @param class-string|string $resolver The name of the resolver to use + */ + public function __construct( + public ?string $format = null, + bool $disabled = false, + string $resolver = DateTimeValueResolver::class, + ) { + parent::__construct($resolver, $disabled); + } +} diff --git a/src/Symfony/Component/ArgumentResolver/Attribute/ValueResolver.php b/src/Symfony/Component/ArgumentResolver/Attribute/ValueResolver.php new file mode 100644 index 0000000000000..f2e1ec575668d --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Attribute/ValueResolver.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\ArgumentResolver\Attribute; + +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; + +/** + * Defines which value resolver should be used for a given parameter. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] +class ValueResolver +{ + /** + * @param class-string|string $resolver The class name of the resolver to use + * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases + */ + public function __construct( + public string $resolver, + public bool $disabled = false, + ) { + } +} diff --git a/src/Symfony/Component/ArgumentResolver/CHANGELOG.md b/src/Symfony/Component/ArgumentResolver/CHANGELOG.md new file mode 100644 index 0000000000000..1584a6b2955f7 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.3 +--- + + * Added the component diff --git a/src/Symfony/Component/ArgumentResolver/Exception/ExceptionInterface.php b/src/Symfony/Component/ArgumentResolver/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..d2729313ff33c --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Exception; + +/** + * Interface for exceptions thrown by the argument-resolver component. + * + * @author Robin Chalas + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/src/Symfony/Component/ArgumentResolver/Exception/InvalidArgumentException.php b/src/Symfony/Component/ArgumentResolver/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..b179e1b5dabaa --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Exception; + +/** + * @author Robin Chalas + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/ArgumentResolver/Exception/InvalidSourceValueException.php b/src/Symfony/Component/ArgumentResolver/Exception/InvalidSourceValueException.php new file mode 100644 index 0000000000000..fc2bf305a62f4 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Exception/InvalidSourceValueException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Exception; + +/** + * @author Robin Chalas + */ +class InvalidSourceValueException extends RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/ArgumentResolver/Exception/LogicException.php b/src/Symfony/Component/ArgumentResolver/Exception/LogicException.php new file mode 100644 index 0000000000000..e306bc38f445c --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Exception; + +/** + * @author Robin Chalas + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/ArgumentResolver/Exception/NearMissValueResolverException.php b/src/Symfony/Component/ArgumentResolver/Exception/NearMissValueResolverException.php new file mode 100644 index 0000000000000..88ea3540b7392 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Exception/NearMissValueResolverException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Exception; + +/** + * Lets value resolvers tell when an argument could be under their watch but failed to be resolved. + * + * Throwing this exception inside `ValueResolverInterface::resolve` does not interrupt the value resolvers chain. + */ +class NearMissValueResolverException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/ArgumentResolver/Exception/ResolverNotFoundException.php b/src/Symfony/Component/ArgumentResolver/Exception/ResolverNotFoundException.php new file mode 100644 index 0000000000000..a4ce5dfc147b1 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Exception/ResolverNotFoundException.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\ArgumentResolver\Exception; + +class ResolverNotFoundException extends \RuntimeException +{ + /** + * @param string[] $alternatives + */ + public function __construct(string $name, array $alternatives = []) + { + $msg = \sprintf('You have requested a non-existent resolver "%s".', $name); + if ($alternatives) { + if (1 === \count($alternatives)) { + $msg .= ' Did you mean this: "'; + } else { + $msg .= ' Did you mean one of these: "'; + } + $msg .= implode('", "', $alternatives).'"?'; + } + + parent::__construct($msg); + } +} diff --git a/src/Symfony/Component/ArgumentResolver/Exception/RuntimeException.php b/src/Symfony/Component/ArgumentResolver/Exception/RuntimeException.php new file mode 100644 index 0000000000000..1c0544a1c1ba0 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\Exception; + +/** + * @author Robin Chalas + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/ArgumentResolver/LICENSE b/src/Symfony/Component/ArgumentResolver/LICENSE new file mode 100644 index 0000000000000..bc38d714ef697 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025-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/ArgumentResolver/README.md b/src/Symfony/Component/ArgumentResolver/README.md new file mode 100644 index 0000000000000..14afb4a3eca22 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/README.md @@ -0,0 +1,53 @@ +ArgumentResolver Component +======================== + +The ArgumentResolver component provides a system that resolves the arguments of a callable based on their metadata and a given input source (e.g. an HTTP Request) at runtime. + +```php +getArguments($ca); + + + +``` + +Getting Started +--------------- + +```bash +composer require symfony/argument-resolver +``` + +```php +``` + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/argument-resolver.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/ArgumentResolver/Source/InputSourceInterface.php b/src/Symfony/Component/ArgumentResolver/Source/InputSourceInterface.php new file mode 100644 index 0000000000000..2b4fb31f7e21b --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Source/InputSourceInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ArgumentValueSource; + +/** + * Exposes a context-specific input source from which arguments' values can be extracted then resolved. + * + * @author Robin Chalas + */ +interface InputSourceInterface +{ + /** + * @returns mixed The context-specific source from which to extract arguments' source values e.g. an HTTP request or a CLI input + */ + public function getSource(): mixed; +} diff --git a/src/Symfony/Component/ArgumentResolver/Source/SourceValue.php b/src/Symfony/Component/ArgumentResolver/Source/SourceValue.php new file mode 100644 index 0000000000000..019e7a3ced21f --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/Source/SourceValue.php @@ -0,0 +1,27 @@ + + */ +final readonly class SourceValue +{ + const NOT_FOUND = 'notfound'; + + public function __construct(private mixed $value) + { + } + + public function get(): mixed + { + return $this->value; + } + + public static function notFound(): self + { + return new self(static::NOT_FOUND); + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/BackedEnumValueResolver.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/BackedEnumValueResolver.php new file mode 100644 index 0000000000000..a1e18233d4aee --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/BackedEnumValueResolver.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ValueResolver; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\InvalidSourceValueException; +use Symfony\Component\ArgumentResolver\ValueAccessor\RawValueAccessorInterface; + +/** + * Attempts to resolve backed enum cases from request attributes, for a route path parameter, + * leading to a 404 Not Found if the attribute value isn't a valid backing value for the enum type. + * + * @author Maxime Steinhausser + * @author Robin Chalas + */ +final readonly class BackedEnumValueResolver implements ValueResolverInterface +{ + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + if (!is_subclass_of($argument->getType(), \BackedEnum::class)) { + return []; + } + + $value = $value->get(); + + if ($argument->isVariadic() || SourceValue::NOT_FOUND === $value) { + // not supported + return []; + } + + if (null === $value) { + return [null]; + } + + if ($value instanceof \BackedEnum) { + return [$value]; + } + + if (!\is_int($value) && !\is_string($value)) { + throw new \LogicException(\sprintf('Could not resolve the "%s $%s" argument: expecting an int or string, got "%s".', $argument->getType(), $argument->getName(), get_debug_type($value))); + } + + /** @var class-string<\BackedEnum> $enumType */ + $enumType = $argument->getType(); + + try { + return [$enumType::from($value)]; + } catch (\ValueError|\TypeError $e) { + throw new InvalidSourceValueException(\sprintf('Could not resolve the "%s $%s" argument: ', $argument->getType(), $argument->getName()).$e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/DateTimeValueResolver.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/DateTimeValueResolver.php new file mode 100644 index 0000000000000..dae4b9abe499d --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/DateTimeValueResolver.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ValueResolver; + +use Psr\Clock\ClockInterface; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Attribute\MapDateTime; +use Symfony\Component\ArgumentResolver\Exception\InvalidSourceValueException; + +/** + * Convert DateTime instances from request attribute variable. + * + * @author Benjamin Eberlei + * @author Tim Goudriaan + * @author Robin Chalas + */ +final readonly class DateTimeValueResolver implements ValueResolverInterface +{ + public function __construct( + private readonly ?ClockInterface $clock = null, + ) { + } + + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + if (!is_a($argument->getType(), \DateTimeInterface::class, true)) { + return []; + } + + $value = $value->get(); + + if (SourceValue::NOT_FOUND === $value) { + return []; + } + + $class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType(); + + if (!$value) { + if ($argument->isNullable()) { + return [null]; + } + if (!$this->clock) { + return [new $class()]; + } + $value = $this->clock->now(); + } + + if ($value instanceof \DateTimeInterface) { + return [$value instanceof $class ? $value : $class::createFromInterface($value)]; + } + + $format = null; + + if ($attributes = $argument->getAttributes(MapDateTime::class, ArgumentMetadata::IS_INSTANCEOF)) { + $attribute = $attributes[0]; + $format = $attribute->format; + } + + if (null !== $format) { + $date = $class::createFromFormat($format, $value, $this->clock?->now()->getTimeZone()); + + if (($class::getLastErrors() ?: ['warning_count' => 0])['warning_count']) { + $date = false; + } + } else { + if (false !== filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) { + $value = '@'.$value; + } + try { + $date = new $class($value, $this->clock?->now()->getTimeZone()); + } catch (\Exception) { + $date = false; + } + } + + if (!$date) { + throw new InvalidSourceValueException(\sprintf('Invalid date given for parameter "%s".', $argument->getName())); + } + + return [$date]; + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/DefaultValueResolver.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/DefaultValueResolver.php new file mode 100644 index 0000000000000..fcd3e2f33f1ab --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/DefaultValueResolver.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\ArgumentResolver\ValueResolver; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; + +/** + * Yields the default value defined in the action signature when no value has been given. + * + * @author Iltar van der Berg + */ +final class DefaultValueResolver implements ValueResolverInterface +{ + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + if ($argument->hasDefaultValue()) { + return [$argument->getDefaultValue()]; + } + + if (null !== $argument->getType() && $argument->isNullable() && !$argument->isVariadic()) { + return [null]; + } + + return []; + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/NotTaggedServiceValueResolver.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/NotTaggedServiceValueResolver.php new file mode 100644 index 0000000000000..fa54d93bbf7ed --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/NotTaggedServiceValueResolver.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ValueResolver; + +use Psr\Container\ContainerInterface; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; + +/** + * Provides an intuitive error message when controller fails because it is not registered as a service. + * + * @author Simeon Kolev + */ +final readonly class NotTaggedServiceValueResolver implements ValueResolverInterface +{ + public function __construct( + private ContainerInterface $container, + ) { + } + + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + $callable = $value->get(); + + if (\is_array($callable) && \is_callable($callable, true) && \is_string($callable[0])) { + $callable = $callable[0].'::'.$callable[1]; + } elseif (!\is_string($callable) || '' === $callable || SourceValue::NOT_FOUND === $callable) { + return []; + } + + if ('\\' === $callable[0]) { + $callable = ltrim($callable, '\\'); + } + + if (!$this->container->has($callable)) { + $callable = (false !== $i = strrpos($callable, ':')) + ? substr($callable, 0, $i).strtolower(substr($callable, $i)) + : $callable.'::__invoke'; + } + + if ($this->container->has($callable)) { + return []; + } + + throw new RuntimeException(\sprintf('Could not resolve argument $%s of "%s()", maybe you forgot to register it as a service?', $argument->getName(), $callable)); + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/ServiceValueResolver.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/ServiceValueResolver.php new file mode 100644 index 0000000000000..64dba7f6dbecf --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/ServiceValueResolver.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ValueResolver; + +use Psr\Container\ContainerInterface; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; + +/** + * Yields a service keyed by callable name and argument name. + * + * @author Nicolas Grekas + * @author Robin Chalas + */ +final readonly class ServiceValueResolver implements ValueResolverInterface +{ + public function __construct( + private ContainerInterface $container, + ) { + } + + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + $callable = $value->get(); + + if (SourceValue::NOT_FOUND === $callable) { + return []; + } + + if (\is_array($callable) && \is_callable($callable, true) && \is_string($callable[0])) { + $callable = $callable[0].'::'.$callable[1]; + } elseif (!\is_string($callable) || '' === $callable) { + return []; + } + + if ('\\' === $callable[0]) { + $callable = ltrim($callable, '\\'); + } + + if (!$this->container->has($callable) && false !== $i = strrpos($callable, ':')) { + $callable = substr($callable, 0, $i).strtolower(substr($callable, $i)); + } + + if (!$this->container->has($callable) || !$this->container->get($callable)->has($argument->getName())) { + return []; + } + + try { + return [$this->container->get($callable)->get($argument->getName())]; + } catch (RuntimeException $e) { + $what = 'argument $'.$argument->getName(); + $message = str_replace(\sprintf('service "%s"', $argument->getName()), $what, $e->getMessage()); + $what .= \sprintf(' of "%s()"', $callable); + $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message); + + if ($e->getMessage() === $message) { + $message = \sprintf('Cannot resolve %s: %s', $what, $message); + } + + throw new NearMissValueResolverException($message, $e->getCode(), $e); + } + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/UidValueResolver.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/UidValueResolver.php new file mode 100644 index 0000000000000..d03bef1b6159a --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/UidValueResolver.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\ArgumentResolver\ValueResolver; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\InvalidSourceValueException; +use Symfony\Component\Uid\AbstractUid; + +/** + * @author Robin Chalas + */ +final readonly class UidValueResolver implements ValueResolverInterface +{ + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + $value = $value->get(); + + if ($argument->isVariadic() + || SourceValue::NOT_FOUND === $value + || !\is_string($value) + || null === ($uidClass = $argument->getType()) + || !is_subclass_of($uidClass, AbstractUid::class, true) + ) { + return []; + } + + try { + return [$uidClass::fromString($value)]; + } catch (\InvalidArgumentException $e) { + throw new InvalidSourceValueException(\sprintf('The uid for the "%s" parameter is invalid.', $argument->getName()), $e->getCode(), $e); + } + } +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/ValueResolverInterface.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/ValueResolverInterface.php new file mode 100644 index 0000000000000..ae8802a6dd8ab --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/ValueResolverInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ArgumentResolver\ValueResolver; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\InvalidSourceValueException; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; + +/** + * Responsible for resolving the value of an argument based on its metadata and its source value. + * + * @author Nicolas Grekas + * @author Robin Chalas + */ +interface ValueResolverInterface +{ + /** + * Returns the resolved argument value(s). + * + * @throws InvalidSourceValueException + * @throws NearMissValueResolverException + */ + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable; +} diff --git a/src/Symfony/Component/ArgumentResolver/ValueResolver/VariadicValueResolver.php b/src/Symfony/Component/ArgumentResolver/ValueResolver/VariadicValueResolver.php new file mode 100644 index 0000000000000..74d36fabea8e5 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/ValueResolver/VariadicValueResolver.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\ArgumentResolver\ValueResolver; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\InvalidArgumentException; + +/** + * Yields a variadic argument's values from the request attributes. + * + * @author Iltar van der Berg + */ +final readonly class VariadicValueResolver implements ValueResolverInterface +{ + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + $values = $value->get(); + + if (!$argument->isVariadic() || SourceValue::NOT_FOUND === $values) { + return []; + } + + if (!\is_array($values)) { + throw new InvalidArgumentException(\sprintf('Argument "...$%1$s" is required to be an array, source value "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); + } + + + return $values; + } +} diff --git a/src/Symfony/Component/ArgumentResolver/composer.json b/src/Symfony/Component/ArgumentResolver/composer.json new file mode 100644 index 0000000000000..5a7a44479b1aa --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/composer.json @@ -0,0 +1,28 @@ +{ + "name": "symfony/argument-resolver", + "type": "library", + "description": "", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Robin Chalas", + "email": "robin@baksla.sh" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\ArgumentResolver\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/ArgumentResolver/phpunit.xml.dist b/src/Symfony/Component/ArgumentResolver/phpunit.xml.dist new file mode 100644 index 0000000000000..8db9f47572765 --- /dev/null +++ b/src/Symfony/Component/ArgumentResolver/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/Console/Attribute/Argument.php b/src/Symfony/Component/Console/Attribute/Argument.php index 099d49676e033..a9307d75b24c3 100644 --- a/src/Symfony/Component/Console/Attribute/Argument.php +++ b/src/Symfony/Component/Console/Attribute/Argument.php @@ -95,7 +95,7 @@ public function toInputArgument(): InputArgument /** * @internal */ - public function resolveValue(InputInterface $input): mixed + public function resolveArgument(InputInterface $input): mixed { return $input->getArgument($this->name); } diff --git a/src/Symfony/Component/Console/Attribute/Option.php b/src/Symfony/Component/Console/Attribute/Option.php index 02002a5ad1256..a5798e31d3662 100644 --- a/src/Symfony/Component/Console/Attribute/Option.php +++ b/src/Symfony/Component/Console/Attribute/Option.php @@ -112,7 +112,7 @@ public function toInputOption(): InputOption /** * @internal */ - public function resolveValue(InputInterface $input): mixed + public function resolveArgument(InputInterface $input): mixed { $value = $input->getOption($this->name); diff --git a/src/Symfony/Component/HttpKernel/Attribute/AsTargetedValueResolver.php b/src/Symfony/Component/HttpKernel/Attribute/AsTargetedValueResolver.php index 0635566174a45..b4f98a5570cfe 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/AsTargetedValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Attribute/AsTargetedValueResolver.php @@ -11,16 +11,16 @@ namespace Symfony\Component\HttpKernel\Attribute; +trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s" class is deprecated, use "%s" instead.', AsTargetedValueResolver::class, BaseAsTargetedValueResolver::class)); + +use Symfony\Component\ArgumentResolver\Attribute\AsTargetedValueResolver as BaseAsTargetedValueResolver; + /** * Service tag to autoconfigure targeted value resolvers. + * + * deprecated since Symfony 7.3, use {@see BaseAsTargetedValueResolver} instead */ #[\Attribute(\Attribute::TARGET_CLASS)] -class AsTargetedValueResolver +class AsTargetedValueResolver extends BaseAsTargetedValueResolver { - /** - * @param string|null $name The name with which the resolver can be targeted - */ - public function __construct(public readonly ?string $name = null) - { - } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapDateTime.php b/src/Symfony/Component/HttpKernel/Attribute/MapDateTime.php index db6b4d613e4f8..55a583c53dac0 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapDateTime.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapDateTime.php @@ -11,25 +11,30 @@ namespace Symfony\Component\HttpKernel\Attribute; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ControllerValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\ArgumentResolver\Attribute\MapDateTime as BaseMapDateTime; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; /** * Controller parameter tag to configure DateTime arguments. + * + * @deprecated since Symfony 7.3, use {@see BaseMapDateTime} instead */ #[\Attribute(\Attribute::TARGET_PARAMETER)] -class MapDateTime extends ValueResolver +class MapDateTime extends BaseMapDateTime { /** - * @param string|null $format The DateTime format to use, @see https://php.net/datetime.format - * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases - * @param class-string|string $resolver The name of the resolver to use + * @param string|null $format The DateTime format to use, @see https://php.net/datetime.format + * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases + * @param class-string|string $resolver The name of the resolver to use */ public function __construct( - public readonly ?string $format = null, + public ?string $format = null, bool $disabled = false, string $resolver = DateTimeValueResolver::class, ) { - parent::__construct($resolver, $disabled); + parent::__construct($format, $disabled, $resolver); } } diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php index cf086380c03f0..89761792996aa 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapRequestPayload.php @@ -22,7 +22,7 @@ * @author Konstantin Myakshin */ #[\Attribute(\Attribute::TARGET_PARAMETER)] -class MapRequestPayload extends ValueResolver +class MapRequestPayload extends \Symfony\Component\ArgumentResolver\Attribute\ValueResolver { public ArgumentMetadata $metadata; diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapUploadedFile.php b/src/Symfony/Component/HttpKernel/Attribute/MapUploadedFile.php index f90b511dc73f3..abf1d9738568d 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/MapUploadedFile.php +++ b/src/Symfony/Component/HttpKernel/Attribute/MapUploadedFile.php @@ -11,9 +11,9 @@ namespace Symfony\Component\HttpKernel\Attribute; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Validator\Constraint; #[\Attribute(\Attribute::TARGET_PARAMETER)] diff --git a/src/Symfony/Component/HttpKernel/Attribute/ValueResolver.php b/src/Symfony/Component/HttpKernel/Attribute/ValueResolver.php index e295965fca73b..13c51d227a564 100644 --- a/src/Symfony/Component/HttpKernel/Attribute/ValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Attribute/ValueResolver.php @@ -11,21 +11,14 @@ namespace Symfony\Component\HttpKernel\Attribute; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\ArgumentResolver\Attribute\ValueResolver as BaseValueResolver; /** * Defines which value resolver should be used for a given parameter. + * + * @deprecated since Symfony 7.3, use {@see BaseValueResolver} instead */ #[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] -class ValueResolver +class ValueResolver extends BaseValueResolver { - /** - * @param class-string|string $resolver The class name of the resolver to use - * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases - */ - public function __construct( - public string $resolver, - public bool $disabled = false, - ) { - } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index b09a92f02dab3..72a11cbc6fff7 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -29,6 +29,7 @@ * Responsible for resolving the arguments passed to an action. * * @author Iltar van der Berg + * @deprecated */ final class ArgumentResolver implements ArgumentResolverInterface { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php index 9193cee060f69..8df818256285a 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/BackedEnumValueResolver.php @@ -11,9 +11,14 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\InvalidSourceValueException; +use Symfony\Component\ArgumentResolver\ValueResolver\BackedEnumValueResolver as BaseBackedEnumValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -22,47 +27,43 @@ * * @author Maxime Steinhausser */ -final class BackedEnumValueResolver implements ValueResolverInterface +final class BackedEnumValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): iterable + public function __construct(private ValueResolverInterface $inner = new BaseBackedEnumValueResolver()) { - if (!is_subclass_of($argument->getType(), \BackedEnum::class)) { - return []; + if (!$inner instanceof ValueResolverInterface) { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('Not passing an instance of "%" as $inner is deprecated.', ValueResolverInterface::class)); + $this->inner = new BaseBackedEnumValueResolver(); } + } - if ($argument->isVariadic()) { - // only target route path parameters, which cannot be variadic. - return []; + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + try { + return $this->inner->resolveArgument($argument, $value); + } catch (InvalidSourceValueException $e) { + throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: ', $argument->getType(), $argument->getName()).$e->getPrevious()->getMessage(), $e); } + } + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { // do not support if no value can be resolved at all // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. if (!$request->attributes->has($argument->getName())) { - return []; - } - - $value = $request->attributes->get($argument->getName()); - - if (null === $value) { - return [null]; + return SourceValue::notFound(); } - if ($value instanceof \BackedEnum) { - return [$value]; - } - - if (!\is_int($value) && !\is_string($value)) { - throw new \LogicException(\sprintf('Could not resolve the "%s $%s" controller argument: expecting an int or string, got "%s".', $argument->getType(), $argument->getName(), get_debug_type($value))); - } - - /** @var class-string<\BackedEnum> $enumType */ - $enumType = $argument->getType(); + return new SourceValue($request->attributes->get($argument->getName())); + } - try { - return [$enumType::from($value)]; - } catch (\ValueError|\TypeError $e) { - throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: ', $argument->getType(), $argument->getName()).$e->getMessage(), $e); - } + /** + * @deprecated since Symfony 7.3, use `resolveArgument()` instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + // trigger_deprecation + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ControllerValueResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ControllerValueResolverInterface.php new file mode 100644 index 0000000000000..223d17d53d12e --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ControllerValueResolverInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; +use Symfony\Component\HttpFoundation\Request; + +interface ControllerValueResolverInterface extends ValueResolverInterface +{ + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue; +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php index 10ea8826f9d25..cb686276181f9 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php @@ -12,10 +12,14 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Psr\Clock\ClockInterface; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\InvalidSourceValueException; +use Symfony\Component\ArgumentResolver\ValueResolver\DateTimeValueResolver as BaseDateTimeValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Attribute\MapDateTime; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -24,64 +28,47 @@ * @author Benjamin Eberlei * @author Tim Goudriaan */ -final class DateTimeValueResolver implements ValueResolverInterface +final class DateTimeValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { + private ValueResolverInterface $inner; + public function __construct( - private readonly ?ClockInterface $clock = null, + ClockInterface|BaseDateTimeValueResolver|null $inner = null, ) { - } - - public function resolve(Request $request, ArgumentMetadata $argument): array - { - if (!is_a($argument->getType(), \DateTimeInterface::class, true) || !$request->attributes->has($argument->getName())) { - return []; + if ($inner instanceof ClockInterface) { + trigger_deprecation('symfony/http-kernel', '7.3', sprintf('Passing a "%s" instance to "%s::__construct() is deprecated, pass a "%s" instance as "$inner" instead.', __CLASS__, ClockInterface::class, BaseDateTimeValueResolver::class)); + $this->inner = new BaseDateTimeValueResolver($inner); + return; } - $value = $request->attributes->get($argument->getName()); - $class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType(); - - if (!$value) { - if ($argument->isNullable()) { - return [null]; - } - if (!$this->clock) { - return [new $class()]; - } - $value = $this->clock->now(); - } + $this->inner = $inner ?? new BaseDateTimeValueResolver(); + } - if ($value instanceof \DateTimeInterface) { - return [$value instanceof $class ? $value : $class::createFromInterface($value)]; + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + try { + return $this->inner->resolveArgument($argument, $value); + } catch(InvalidSourceValueException $e) { + throw new NotFoundHttpException($e->getMessage(), $e); } + } - $format = null; - - if ($attributes = $argument->getAttributes(MapDateTime::class, ArgumentMetadata::IS_INSTANCEOF)) { - $attribute = $attributes[0]; - $format = $attribute->format; + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + if (!$request->attributes->has($argument->getName())) { + return new SourceValue([]); } - if (null !== $format) { - $date = $class::createFromFormat($format, $value, $this->clock?->now()->getTimeZone()); - - if (($class::getLastErrors() ?: ['warning_count' => 0])['warning_count']) { - $date = false; - } - } else { - if (false !== filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) { - $value = '@'.$value; - } - try { - $date = new $class($value, $this->clock?->now()->getTimeZone()); - } catch (\Exception) { - $date = false; - } - } + return new SourceValue($request->attributes->get($argument->getName())); + } - if (!$date) { - throw new NotFoundHttpException(\sprintf('Invalid date given for parameter "%s".', $argument->getName())); - } + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); - return [$date]; + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php index bf114f3f31352..f70ec7cf0ba39 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php @@ -11,27 +11,44 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +trigger_deprecation('symfony/http-kernel', '7.3', 'The "%s" class is deprecated, use "%s" instead.', DefaultValueResolver::class, BaseDefaultValueResolver::class); + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\ValueResolver\DefaultValueResolver as BaseDefaultValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; /** * Yields the default value defined in the action signature when no value has been given. * * @author Iltar van der Berg + * + * @deprecated since Symfony 7.3, use {@see BaseDefaultValueResolver} instead */ -final class DefaultValueResolver implements ValueResolverInterface +final class DefaultValueResolver implements LegacyValueResolverInterface, ControllerValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): array + private ValueResolverInterface $inner; + + public function __construct() + { + $this->inner = new BaseDefaultValueResolver(); + } + + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { - if ($argument->hasDefaultValue()) { - return [$argument->getDefaultValue()]; - } + return $this->inner->resolveArgument($argument, $value); + } - if (null !== $argument->getType() && $argument->isNullable() && !$argument->isVariadic()) { - return [null]; - } + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue(null); + } - return []; + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + return $this->resolveArgument($argument, new SourceValue(null)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php index c5c862e667d27..5d05748b501de 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php @@ -12,50 +12,57 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Psr\Container\ContainerInterface; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\ValueResolver\NotTaggedServiceValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; /** * Provides an intuitive error message when controller fails because it is not registered as a service. * * @author Simeon Kolev */ -final class NotTaggedControllerValueResolver implements ValueResolverInterface +final class NotTaggedControllerValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { + private ValueResolverInterface $inner; + public function __construct( - private ContainerInterface $container, + ValueResolverInterface|ContainerInterface $inner, ) { + if ($inner instanceof ContainerInterface) { + trigger_deprecation('symfony/http-kernel', '7.3', sprintf('The "$container" argument of "%s::__construct()" is deprecated, pass a "%s" instance as "$inner" instead.', __CLASS__, NotTaggedServiceValueResolver::class)); + $this->inner = new NotTaggedServiceValueResolver($inner); + return; + } + $this->inner = new NotTaggedServiceValueResolver($inner); } - public function resolve(Request $request, ArgumentMetadata $argument): array + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { - $controller = $request->attributes->get('_controller'); - - if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) { - $controller = $controller[0].'::'.$controller[1]; - } elseif (!\is_string($controller) || '' === $controller) { - return []; - } - - if ('\\' === $controller[0]) { - $controller = ltrim($controller, '\\'); + try { + return $this->inner->resolveArgument($argument, $value); + } catch (RuntimeException $e) { + throw new RuntimeException(str_replace('?', ' or missed tagging it with the "controller.service_arguments"?', $e->getMessage())); } + } - if (!$this->container->has($controller)) { - $controller = (false !== $i = strrpos($controller, ':')) - ? substr($controller, 0, $i).strtolower(substr($controller, $i)) - : $controller.'::__invoke'; - } + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue($request->attributes->get('_controller')); - if ($this->container->has($controller)) { - return []; - } + } - $what = \sprintf('argument $%s of "%s()"', $argument->getName(), $controller); - $message = \sprintf('Could not resolve %s, maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?', $what); + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); - throw new RuntimeException($message); + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php index 5fe3d75313a43..52793e8539278 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/QueryParameterValueResolver.php @@ -11,10 +11,13 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\HttpFoundation\InputBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Uid\AbstractUid; @@ -26,9 +29,9 @@ * @author Mateusz Anders * @author Ionut Enache */ -final class QueryParameterValueResolver implements ValueResolverInterface +final class QueryParameterValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): array + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { if (!$attribute = $argument->getAttributesOfType(MapQueryParameter::class)[0] ?? null) { return []; @@ -37,7 +40,10 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $name = $attribute->name ?? $argument->getName(); $validationFailedCode = $attribute->validationFailedStatusCode; - if (!$request->query->has($name)) { + /** @var InputBag $query */ + $query = $value->get(); + + if (!$query->has($name)) { if ($argument->isNullable() || $argument->hasDefaultValue()) { return []; } @@ -45,7 +51,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Missing query parameter "%s".', $name)); } - $value = $request->query->all()[$name]; + $value = $query->all()[$name]; $type = $argument->getType(); if (null === $attribute->filter && 'array' === $type) { @@ -56,7 +62,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $filtered = array_values(array_filter((array) $value, \is_array(...))); if ($filtered !== $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) { - throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); + throw HttpException::fromStatusCode($attribute->validationFailedStatusCode, \sprintf('Invalid query parameter "%s".', $name)); } return $filtered; @@ -137,4 +143,19 @@ public function resolve(Request $request, ArgumentMetadata $argument): array return $argument->isVariadic() ? $filtered : [$filtered]; } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue($request->query); + } + + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); + + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); + } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 2a8d48ee30174..03306aa02fce2 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php @@ -11,19 +11,40 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; /** * Yields a non-variadic argument's value from the request attributes. * * @author Iltar van der Berg */ -final class RequestAttributeValueResolver implements ValueResolverInterface +final class RequestAttributeValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): array + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { - return !$argument->isVariadic() && $request->attributes->has($argument->getName()) ? [$request->attributes->get($argument->getName())] : []; + $value = $value->get(); + + return $argument->isVariadic() ||SourceValue::NOT_FOUND === $value ? [] : [$value]; + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return $request->attributes->has($argument->getName()) + ? new SourceValue($request->attributes->get($argument->getName())) + : SourceValue::notFound(); + } + + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); + + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 2b1c42084448d..c16a0025bfaa6 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -11,18 +11,20 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\NotEncodableValueException; @@ -43,7 +45,7 @@ * * @final */ -class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface +class RequestPayloadValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface, EventSubscriberInterface { /** * @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS @@ -67,7 +69,7 @@ public function __construct( ) { } - public function resolve(Request $request, ArgumentMetadata $argument): iterable + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { $attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? $argument->getAttributesOfType(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] @@ -97,6 +99,11 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable return [$attribute]; } + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue(null); + } + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { $arguments = $event->getArguments(); @@ -182,6 +189,16 @@ public static function getSubscribedEvents(): array ]; } + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); + + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); + } + private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object { if (!($data = $request->query->all($attribute->key)) && ($argument->isNullable() || $argument->hasDefaultValue())) { diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestValueResolver.php index 28e41181e4c7a..ec68fad458f8f 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestValueResolver.php @@ -11,22 +11,25 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException as LegacyNearMissValueResolverException; /** * Yields the same instance as the request object passed along. * * @author Iltar van der Berg */ -final class RequestValueResolver implements ValueResolverInterface +final class RequestValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): array + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { if (Request::class === $argument->getType() || is_subclass_of($argument->getType(), Request::class)) { - return [$request]; + return [$value->get()]; } if (str_ends_with($argument->getType() ?? '', '\\Request')) { @@ -35,4 +38,23 @@ public function resolve(Request $request, ArgumentMetadata $argument): array return []; } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue($request); + } + + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); + + try { + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); + } catch (NearMissValueResolverException $e) { + throw new LegacyNearMissValueResolverException($e->getMessage(), $e->getCode(), $e); + } + } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php index 62074ef00aeae..cd6f992150bfe 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php @@ -12,59 +12,52 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Psr\Container\ContainerInterface; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\ValueResolver\ServiceValueResolver as BaseServiceValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; /** * Yields a service keyed by _controller and argument name. * * @author Nicolas Grekas */ -final class ServiceValueResolver implements ValueResolverInterface +final class ServiceValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { + private ValueResolverInterface $inner; + public function __construct( - private ContainerInterface $container, + ValueResolverInterface|ContainerInterface $inner, ) { + if ($inner instanceof ContainerInterface) { + trigger_deprecation('symfony/http-kernel', '7.3', sprintf('The "$container" argument of "%s::__construct()" is deprecated, pass a "%s" instance as "$inner" instead.', __CLASS__, BaseServiceValueResolver::class)); + $this->inner = new BaseServiceValueResolver($inner); + return; + } + $this->inner = $inner; } - public function resolve(Request $request, ArgumentMetadata $argument): array + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { - $controller = $request->attributes->get('_controller'); - - if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) { - $controller = $controller[0].'::'.$controller[1]; - } elseif (!\is_string($controller) || '' === $controller) { - return []; - } - - if ('\\' === $controller[0]) { - $controller = ltrim($controller, '\\'); - } - - if (!$this->container->has($controller) && false !== $i = strrpos($controller, ':')) { - $controller = substr($controller, 0, $i).strtolower(substr($controller, $i)); - } + return $this->inner->resolveArgument($argument, $value); + } - if (!$this->container->has($controller) || !$this->container->get($controller)->has($argument->getName())) { - return []; - } + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue($request->attributes->get('_controller')); - try { - return [$this->container->get($controller)->get($argument->getName())]; - } catch (RuntimeException $e) { - $what = 'argument $'.$argument->getName(); - $message = str_replace(\sprintf('service "%s"', $argument->getName()), $what, $e->getMessage()); - $what .= \sprintf(' of "%s()"', $controller); - $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message); + } - if ($e->getMessage() === $message) { - $message = \sprintf('Cannot resolve %s: %s', $what, $message); - } + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); - throw new NearMissValueResolverException($message, $e->getCode(), $e); - } + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/SessionValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/SessionValueResolver.php index 30b7f1d7493c7..00773ba600020 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/SessionValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/SessionValueResolver.php @@ -11,29 +11,44 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; /** * Yields the Session. * * @author Iltar van der Berg */ -final class SessionValueResolver implements ValueResolverInterface +final class SessionValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): array + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable { - if (!$request->hasSession()) { - return []; - } - + $session = $value->get(); $type = $argument->getType(); + if (SessionInterface::class !== $type && !is_subclass_of($type, SessionInterface::class)) { return []; } - return $request->getSession() instanceof $type ? [$request->getSession()] : []; + return $session instanceof $type ? [$session] : []; + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return $request->hasSession() ? new SourceValue($request->getSession()) : SourceValue::notFound(); + } + + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); + + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php index 41fd1d9ae9885..5bed29c23dd95 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/TraceableValueResolver.php @@ -11,9 +11,11 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; use Symfony\Component\Stopwatch\Stopwatch; /** @@ -21,15 +23,30 @@ * * @author Iltar van der Berg */ -final class TraceableValueResolver implements ValueResolverInterface +final class TraceableValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { public function __construct( - private ValueResolverInterface $inner, + private LegacyValueResolverInterface|ControllerValueResolverInterface $inner, private Stopwatch $stopwatch, ) { } - public function resolve(Request $request, ArgumentMetadata $argument): iterable + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + $method = $this->inner::class.'::'.__FUNCTION__; + $this->stopwatch->start($method, 'controller.argument_value_resolver'); + + yield from $this->inner->resolveArgument($argument, $value); + + $this->stopwatch->stop($method); + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return $this->inner->extractSourceValue($argument, $request); + } + + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable { $method = $this->inner::class.'::'.__FUNCTION__; $this->stopwatch->start($method, 'controller.argument_value_resolver'); diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php index 4a232eb8aeccf..d5bd9d7fccc12 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/UidValueResolver.php @@ -11,28 +11,47 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Exception\InvalidSourceValueException; +use Symfony\Component\ArgumentResolver\ValueResolver\UidValueResolver as BaseUidValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\Uid\AbstractUid; -final class UidValueResolver implements ValueResolverInterface +final class UidValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): array + public function __construct(private readonly ValueResolverInterface $inner = new BaseUidValueResolver()) { - if ($argument->isVariadic() - || !\is_string($value = $request->attributes->get($argument->getName())) - || null === ($uidClass = $argument->getType()) - || !is_subclass_of($uidClass, AbstractUid::class, true) - ) { - return []; - } + } + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { try { - return [$uidClass::fromString($value)]; - } catch (\InvalidArgumentException $e) { - throw new NotFoundHttpException(\sprintf('The uid for the "%s" parameter is invalid.', $argument->getName()), $e); + return $this->inner->resolveArgument($argument, $value); + } catch (InvalidSourceValueException $e) { + throw new NotFoundHttpException($e->getMessage(), $e); + } + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + if (!$request->attributes->has($argument->getName())) { + return SourceValue::notFound(); } + + return new SourceValue($request->attributes->get($argument->getName())); + } + + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); + + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php index d046129f4f455..141d40b69bc1b 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php @@ -11,29 +11,44 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; +use Symfony\Component\ArgumentResolver\ValueResolver\VariadicValueResolver as BaseVariadicValueResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; /** * Yields a variadic argument's values from the request attributes. * * @author Iltar van der Berg */ -final class VariadicValueResolver implements ValueResolverInterface +final class VariadicValueResolver implements ControllerValueResolverInterface, LegacyValueResolverInterface { - public function resolve(Request $request, ArgumentMetadata $argument): array + public function __construct(private readonly ValueResolverInterface $inner = new BaseVariadicValueResolver()) { - if (!$argument->isVariadic() || !$request->attributes->has($argument->getName())) { - return []; - } + } - $values = $request->attributes->get($argument->getName()); + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + return $this->inner->resolveArgument($argument, $value); + } - if (!\is_array($values)) { - throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); - } + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return $request->attributes->has($argument->getName()) + ? new SourceValue($request->attributes->get($argument->getName())) + : SourceValue::notFound(); + } + + /** + * @deprecated since Symfony 7.3, use resolveArgument() instead + */ + public function resolve(Request $request, LegacyArgumentMetadata $argument): iterable + { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s()" method is deprecated, use "resolveArgument()" instead.', __METHOD__)); - return $values; + return $this->resolveArgument($argument, $this->extractSourceValue($argument, $request)); } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php index 2090a599288df..416704adcb67f 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php @@ -18,6 +18,8 @@ * arguments for a specific action. * * @author Fabien Potencier + * + * @deprecated */ interface ArgumentResolverInterface { diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerArgumentResolver.php new file mode 100644 index 0000000000000..1669872bfe4b1 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerArgumentResolver.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller; + +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentResolver; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\ValueResolver\DefaultValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ControllerValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface as LegacyValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata as LegacyArgumentMetadata; + +/** + * Responsible for resolving the arguments passed to an action. + */ +final class ControllerArgumentResolver extends ArgumentResolver +{ + /** + * @param Request $input + */ + public function getArguments(mixed $input, callable $callable, ?\ReflectionFunctionAbstract $reflector = null): array + { + if (!$input instanceof Request) { + throw new \InvalidArgumentException(sprintf('The "$request" argument must be an instance of %s, got "%s".', Request::class, get_debug_type($request))); + } + + return parent::getArguments($input, $callable, $reflector); + } + + public static function getDefaultValueResolvers(): iterable + { + return [ + new RequestAttributeValueResolver(), + new RequestValueResolver(), + new SessionValueResolver(), + new DefaultValueResolver(), + new VariadicValueResolver(), + ]; + } + + protected static function getExtraValueResolversForNamed(): array + { + return [ + new RequestAttributeValueResolver(), + new DefaultValueResolver(), + ]; + } + + /** + * @param ControllerValueResolverInterface|LegacyValueResolverInterface $resolver + * @param Request $input + */ + protected function callResolver($resolver, ArgumentMetadata $metadata, mixed $input): iterable + { + if ($resolver instanceof ValueResolverInterface) { + return $resolver->resolveArgument($metadata, $resolver instanceof ControllerValueResolverInterface ? $resolver->extractSourceValue($metadata, $input) : new SourceValue(null)); + } else { + trigger_deprecation('symfony/http-kernel', '7.3', \sprintf('The "%s" interface is deprecated, implement "%s" in "%s" instead.', LegacyValueResolverInterface::class, ControllerValueResolverInterface::class, $resolver::class)); + + return $resolver->resolve($input, LegacyArgumentMetadata::fromBaseArgumentMetadata($metadata)); + } + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php index c6dac59373884..d9f737079db65 100644 --- a/src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php @@ -16,6 +16,8 @@ /** * @author Fabien Potencier + * + * @deprecated */ class TraceableArgumentResolver implements ArgumentResolverInterface { diff --git a/src/Symfony/Component/HttpKernel/Controller/ValueResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ValueResolverInterface.php index a861705cb7bb6..433909e4ee24d 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ValueResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ValueResolverInterface.php @@ -18,6 +18,7 @@ * Responsible for resolving the value of an argument based on its metadata. * * @author Nicolas Grekas + * @deprecated since Symfony 7.3, use ControllerValueResolverInterface instead */ interface ValueResolverInterface { diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php index 207fabc14f77a..88566e5f99554 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php @@ -11,134 +11,33 @@ namespace Symfony\Component\HttpKernel\ControllerMetadata; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata as BaseArgumentMetadata; + /** * Responsible for storing metadata of an argument. * * @author Iltar van der Berg + * + * @deprecated */ -class ArgumentMetadata +class ArgumentMetadata extends BaseArgumentMetadata { - public const IS_INSTANCEOF = 2; - - /** - * @param object[] $attributes - */ - public function __construct( - private string $name, - private ?string $type, - private bool $isVariadic, - private bool $hasDefaultValue, - private mixed $defaultValue, - private bool $isNullable = false, - private array $attributes = [], - private string $controllerName = 'n/a', - ) { - $this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue); - } - - /** - * Returns the name as given in PHP, $foo would yield "foo". - */ - public function getName(): string - { - return $this->name; - } - - /** - * Returns the type of the argument. - * - * The type is the PHP class in 5.5+ and additionally the basic type in PHP 7.0+. - */ - public function getType(): ?string - { - return $this->type; - } - - /** - * Returns whether the argument is defined as "...$variadic". - */ - public function isVariadic(): bool - { - return $this->isVariadic; - } - - /** - * Returns whether the argument has a default value. - * - * Implies whether an argument is optional. - */ - public function hasDefaultValue(): bool - { - return $this->hasDefaultValue; - } - - /** - * Returns whether the argument accepts null values. - */ - public function isNullable(): bool - { - return $this->isNullable; - } - - /** - * Returns the default value of the argument. - * - * @throws \LogicException if no default value is present; {@see self::hasDefaultValue()} - */ - public function getDefaultValue(): mixed - { - if (!$this->hasDefaultValue) { - throw new \LogicException(\sprintf('Argument $%s does not have a default value. Use "%s::hasDefaultValue()" to avoid this exception.', $this->name, __CLASS__)); - } - - return $this->defaultValue; - } - - /** - * @param class-string $name - * @param self::IS_INSTANCEOF|0 $flags - * - * @return array - */ - public function getAttributes(?string $name = null, int $flags = 0): array - { - if (!$name) { - return $this->attributes; - } - - return $this->getAttributesOfType($name, $flags); - } - - /** - * @template T of object - * - * @param class-string $name - * @param self::IS_INSTANCEOF|0 $flags - * - * @return array - */ - public function getAttributesOfType(string $name, int $flags = 0): array + public function getControllerName(): string { - $attributes = []; - if ($flags & self::IS_INSTANCEOF) { - foreach ($this->attributes as $attribute) { - if ($attribute instanceof $name) { - $attributes[] = $attribute; - } - } - } else { - foreach ($this->attributes as $attribute) { - if ($attribute::class === $name) { - $attributes[] = $attribute; - } - } - } - - return $attributes; + return $this->getCallableName(); } - public function getControllerName(): string + public static function fromBaseArgumentMetadata(BaseArgumentMetadata $metadata) { - return $this->controllerName; + return new self( + $metadata->getName(), + $metadata->getType(), + $metadata->isVariadic(), + $metadata->hasDefaultValue(), + $metadata->hasDefaultValue() ? $metadata->getDefaultValue() : null, + $metadata->isNullable(), + $metadata->getAttributes(), + $metadata->getCallableName(), + ); } } diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 26b80f9dcf43a..c6eab4b7f3845 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -15,6 +15,8 @@ * Builds {@see ArgumentMetadata} objects based on the given Controller. * * @author Iltar van der Berg + * + * @deprecated */ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface { diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php index 4f4bc0786639e..c7fb2c7f2f99d 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php @@ -11,12 +11,16 @@ namespace Symfony\Component\HttpKernel\ControllerMetadata; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadataFactoryInterface as BaseArgumentMetadataFactoryInterface; + /** * Builds method argument data. * * @author Iltar van der Berg + * + * @deprecated */ -interface ArgumentMetadataFactoryInterface +interface ArgumentMetadataFactoryInterface extends BaseArgumentMetadataFactoryInterface { /** * @return ArgumentMetadata[] diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php index d760e3bcc1955..372ac9069e8e1 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ControllerArgumentValueResolverPass.php @@ -32,7 +32,7 @@ class ControllerArgumentValueResolverPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { - if (!$container->hasDefinition('argument_resolver')) { + if (!$container->hasDefinition('controller.argument_resolver')) { return; } @@ -62,7 +62,7 @@ public function process(ContainerBuilder $container): void } $container - ->getDefinition('argument_resolver') + ->getDefinition('controller.argument_resolver') ->replaceArgument(1, new IteratorArgument(array_values($resolvers))) ->setArgument(2, new ServiceLocatorArgument($namedResolvers)) ; diff --git a/src/Symfony/Component/HttpKernel/Exception/NearMissValueResolverException.php b/src/Symfony/Component/HttpKernel/Exception/NearMissValueResolverException.php index 73ccfe916a89f..b6ba011b8f090 100644 --- a/src/Symfony/Component/HttpKernel/Exception/NearMissValueResolverException.php +++ b/src/Symfony/Component/HttpKernel/Exception/NearMissValueResolverException.php @@ -15,7 +15,9 @@ * Lets value resolvers tell when an argument could be under their watch but failed to be resolved. * * Throwing this exception inside `ValueResolverInterface::resolve` does not interrupt the value resolvers chain. + * + * @deprecated */ -class NearMissValueResolverException extends \RuntimeException +class NearMissValueResolverException extends \Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException { } diff --git a/src/Symfony/Component/HttpKernel/Exception/ResolverNotFoundException.php b/src/Symfony/Component/HttpKernel/Exception/ResolverNotFoundException.php index aa859d5115cf7..00a1992175dc8 100644 --- a/src/Symfony/Component/HttpKernel/Exception/ResolverNotFoundException.php +++ b/src/Symfony/Component/HttpKernel/Exception/ResolverNotFoundException.php @@ -11,23 +11,9 @@ namespace Symfony\Component\HttpKernel\Exception; -class ResolverNotFoundException extends \RuntimeException +/** + * @deprecated + */ +class ResolverNotFoundException extends \Symfony\Component\ArgumentResolver\Exception\ResolverNotFoundException { - /** - * @param string[] $alternatives - */ - public function __construct(string $name, array $alternatives = []) - { - $msg = \sprintf('You have requested a non-existent resolver "%s".', $name); - if ($alternatives) { - if (1 === \count($alternatives)) { - $msg .= ' Did you mean this: "'; - } else { - $msg .= ' Did you mean one of these: "'; - } - $msg .= implode('", "', $alternatives).'"?'; - } - - parent::__construct($msg); - } } diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 91764366cd91d..9036f4c447b63 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -11,13 +11,14 @@ namespace Symfony\Component\HttpKernel; +use Symfony\Component\ArgumentResolver\ArgumentResolverInterface; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface as LegacyArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; @@ -52,14 +53,14 @@ class_exists(KernelEvents::class); class HttpKernel implements HttpKernelInterface, TerminableInterface { protected RequestStack $requestStack; - private ArgumentResolverInterface $argumentResolver; + private ArgumentResolverInterface|LegacyArgumentResolverInterface $argumentResolver; private bool $terminating = false; public function __construct( protected EventDispatcherInterface $dispatcher, protected ControllerResolverInterface $resolver, ?RequestStack $requestStack = null, - ?ArgumentResolverInterface $argumentResolver = null, + ArgumentResolverInterface|LegacyArgumentResolverInterface|null $argumentResolver = null, private bool $handleAllThrowables = false, ) { $this->requestStack = $requestStack ?? new RequestStack(); @@ -172,7 +173,9 @@ private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Re $controller = $event->getController(); // controller arguments - $arguments = $this->argumentResolver->getArguments($request, $controller, $event->getControllerReflector()); + $arguments = $this->argumentResolver instanceof ArgumentResolverInterface + ? $this->argumentResolver->getArguments($request, $controller, $event->getControllerReflector()) + : $this->argumentResolver->getArguments($request, $controller, $event->getControllerReflector()); $event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php index 6b0f4027ffd8b..45642ee18519b 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php @@ -123,7 +123,7 @@ public function testResolveThrowsOnUnexpectedType() $metadata = self::createArgumentMetadata('suit', Suit::class); $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Could not resolve the "Symfony\Component\HttpKernel\Tests\Fixtures\Suit $suit" controller argument: expecting an int or string, got "bool".'); + $this->expectExceptionMessage('Could not resolve the "Symfony\Component\HttpKernel\Tests\Fixtures\Suit $suit" argument: expecting an int or string, got "bool".'); $resolver->resolve($request, $metadata); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ControllerArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ControllerArgumentResolverTest.php new file mode 100644 index 0000000000000..b6b34289fd8b0 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ControllerArgumentResolverTest.php @@ -0,0 +1,544 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadata; +use Symfony\Component\ArgumentResolver\ArgumentMetadata\ArgumentMetadataFactory; +use Symfony\Component\ArgumentResolver\ArgumentValueSource\SourceValue; +use Symfony\Component\ArgumentResolver\Attribute\ValueResolver; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; +use Symfony\Component\ArgumentResolver\Exception\ResolverNotFoundException; +use Symfony\Component\ArgumentResolver\ValueResolver\DefaultValueResolver; +use Symfony\Component\ArgumentResolver\ValueResolver\ValueResolverInterface; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ControllerValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ControllerArgumentResolver; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingRequest; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingSession; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; + +class ControllerArgumentResolverTest extends TestCase +{ + public static function getResolver(array $chainableResolvers = [], ?array $namedResolvers = null): ControllerArgumentResolver + { + if (null !== $namedResolvers) { + $namedResolvers = new ServiceLocator(array_map(fn ($resolver) => fn () => $resolver, $namedResolvers)); + } + + return new ControllerArgumentResolver(new ArgumentMetadataFactory(), $chainableResolvers, $namedResolvers); + } + + public function testDefaultState() + { + $this->assertEquals(self::getResolver(), new ControllerArgumentResolver()); + $this->assertNotEquals(self::getResolver(), new ControllerArgumentResolver(null, [new RequestAttributeValueResolver()])); + } + + public function testGetArguments() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithFoo']; + + $this->assertEquals(['foo'], self::getResolver()->getArguments($request, $controller), '->getArguments() returns an array of arguments for the controller method'); + } + + public function testGetArgumentsReturnsEmptyArrayWhenNoArguments() + { + $request = Request::create('/'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithoutArguments']; + + $this->assertEquals([], self::getResolver()->getArguments($request, $controller), '->getArguments() returns an empty array if the method takes no arguments'); + } + + public function testGetArgumentsUsesDefaultValue() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithFooAndDefaultBar']; + + $this->assertEquals(['foo', null], self::getResolver()->getArguments($request, $controller), '->getArguments() uses default values if present'); + } + + public function testGetArgumentsOverrideDefaultValueByRequestAttribute() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', 'bar'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithFooAndDefaultBar']; + + $this->assertEquals(['foo', 'bar'], self::getResolver()->getArguments($request, $controller), '->getArguments() overrides default values if provided in the request attributes'); + } + + public function testGetArgumentsFromClosure() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = function ($foo) {}; + + $this->assertEquals(['foo'], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetArgumentsUsesDefaultValueFromClosure() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = function ($foo, $bar = 'bar') {}; + + $this->assertEquals(['foo', 'bar'], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetArgumentsFromInvokableObject() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = new ControllerArgumentResolverTestController(); + + $this->assertEquals(['foo', null], self::getResolver()->getArguments($request, $controller)); + + // Test default bar overridden by request attribute + $request->attributes->set('bar', 'bar'); + + $this->assertEquals(['foo', 'bar'], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetArgumentsFromFunctionName() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('foobar', 'foobar'); + $controller = __NAMESPACE__.'\controller_function'; + + $this->assertEquals(['foo', 'foobar'], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetArgumentsFailsOnUnresolvedValue() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('foobar', 'foobar'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithFooBarFoobar']; + + try { + self::getResolver()->getArguments($request, $controller); + $this->fail('->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); + } catch (\Exception $e) { + $this->assertInstanceOf(\RuntimeException::class, $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); + } + } + + public function testGetArgumentsInjectsRequest() + { + $request = Request::create('/'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithRequest']; + + $this->assertEquals([$request], self::getResolver()->getArguments($request, $controller), '->getArguments() injects the request'); + } + + public function testGetArgumentsInjectsExtendingRequest() + { + $request = ExtendingRequest::create('/'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithExtendingRequest']; + + $this->assertEquals([$request], self::getResolver()->getArguments($request, $controller), '->getArguments() injects the request when extended'); + } + + public function testGetVariadicArguments() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', ['foo', 'bar']); + $controller = [new VariadicController(), 'action']; + + $values = self::getResolver()->getArguments($request, $controller); + + $this->assertEquals(['foo', 'foo', 'bar'], $values); + } + + public function testGetVariadicArgumentsWithoutArrayInRequest() + { + $this->expectException(\InvalidArgumentException::class); + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', 'foo'); + $controller = [new VariadicController(), 'action']; + + self::getResolver()->getArguments($request, $controller); + } + + public function testIfExceptionIsThrownWhenMissingAnArgument() + { + $request = Request::create('/'); + $controller = (new ControllerArgumentResolverTestController())->controllerWithFoo(...); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Controller "'.ControllerArgumentResolverTestController::class.'::controllerWithFoo" requires the "$foo" argument that could not be resolved. Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.'); + self::getResolver()->getArguments($request, $controller); + } + + public function testGetNullableArguments() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', new \stdClass()); + $request->attributes->set('last', 'last'); + $controller = [new NullableController(), 'action']; + + $this->assertEquals(['foo', new \stdClass(), 'value', 'last'], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetNullableArgumentsWithDefaults() + { + $request = Request::create('/'); + $request->attributes->set('last', 'last'); + $controller = [new NullableController(), 'action']; + + $this->assertEquals([null, null, 'value', 'last'], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetSessionArguments() + { + $session = new Session(new MockArraySessionStorage()); + $request = Request::create('/'); + $request->setSession($session); + $controller = (new ControllerArgumentResolverTestController())->controllerWithSession(...); + + $this->assertEquals([$session], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetSessionArgumentsWithExtendedSession() + { + $session = new ExtendingSession(new MockArraySessionStorage()); + $request = Request::create('/'); + $request->setSession($session); + $controller = (new ControllerArgumentResolverTestController())->controllerWithExtendingSession(...); + + $this->assertEquals([$session], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetSessionArgumentsWithInterface() + { + $session = $this->createMock(SessionInterface::class); + $request = Request::create('/'); + $request->setSession($session); + $controller = (new ControllerArgumentResolverTestController())->controllerWithSessionInterface(...); + + $this->assertEquals([$session], self::getResolver()->getArguments($request, $controller)); + } + + public function testGetSessionMissMatchWithInterface() + { + $this->expectException(\RuntimeException::class); + $session = $this->createMock(SessionInterface::class); + $request = Request::create('/'); + $request->setSession($session); + $controller = (new ControllerArgumentResolverTestController())->controllerWithExtendingSession(...); + + self::getResolver()->getArguments($request, $controller); + } + + public function testGetSessionMissMatchWithImplementation() + { + $this->expectException(\RuntimeException::class); + $session = new Session(new MockArraySessionStorage()); + $request = Request::create('/'); + $request->setSession($session); + $controller = (new ControllerArgumentResolverTestController())->controllerWithExtendingSession(...); + + self::getResolver()->getArguments($request, $controller); + } + + public function testGetSessionMissMatchOnNull() + { + $this->expectException(\RuntimeException::class); + $request = Request::create('/'); + $controller = (new ControllerArgumentResolverTestController())->controllerWithExtendingSession(...); + + self::getResolver()->getArguments($request, $controller); + } + + public function testTargetedResolver() + { + $resolver = self::getResolver([], [DefaultValueResolver::class => new DefaultValueResolver()]); + + $request = Request::create('/'); + $request->attributes->set('foo', 'bar'); + $controller = (new ControllerArgumentResolverTestController())->controllerTargetingResolver(...); + + $this->assertSame([1], $resolver->getArguments($request, $controller)); + } + + public function testTargetedResolverWithDefaultValue() + { + $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]); + + $request = Request::create('/'); + $controller = (new ControllerArgumentResolverTestController())->controllerTargetingResolverWithDefaultValue(...); + + /** @var Post[] $arguments */ + $arguments = $resolver->getArguments($request, $controller); + + $this->assertCount(1, $arguments); + $this->assertSame('Default', $arguments[0]->title); + } + + public function testTargetedResolverWithNullableValue() + { + $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]); + + $request = Request::create('/'); + $controller = (new ControllerArgumentResolverTestController())->controllerTargetingResolverWithNullableValue(...); + + $this->assertSame([null], $resolver->getArguments($request, $controller)); + } + + public function testTargetedResolverWithRequestAttributeValue() + { + $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]); + + $request = Request::create('/'); + $request->attributes->set('foo', $object = new Post('Random '.time())); + $controller = (new ControllerArgumentResolverTestController())->controllerTargetingResolverWithTestEntity(...); + + $this->assertSame([$object], $resolver->getArguments($request, $controller)); + } + + public function testDisabledResolver() + { + $resolver = self::getResolver(namedResolvers: []); + + $request = Request::create('/'); + $request->attributes->set('foo', 'bar'); + $controller = (new ControllerArgumentResolverTestController())->controllerDisablingResolver(...); + + $this->assertSame([1], $resolver->getArguments($request, $controller)); + } + + public function testManyTargetedResolvers() + { + $resolver = self::getResolver(namedResolvers: []); + + $request = Request::create('/'); + $controller = (new ControllerArgumentResolverTestController())->controllerTargetingManyResolvers(...); + + $this->expectException(\LogicException::class); + $resolver->getArguments($request, $controller); + } + + public function testUnknownTargetedResolver() + { + $resolver = self::getResolver(namedResolvers: []); + + $request = Request::create('/'); + $controller = (new ControllerArgumentResolverTestController())->controllerTargetingUnknownResolver(...); + + $this->expectException(ResolverNotFoundException::class); + $resolver->getArguments($request, $controller); + } + + public function testResolversChainCompletionWhenResolverThrowsSpecialException() + { + $failingValueResolver = new class implements ControllerValueResolverInterface { + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + throw new NearMissValueResolverException('This resolver throws an exception'); + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue(123); + } + }; + + // Put failing value resolver in the beginning + $expectedToCallValueResolver = $this->createMock(ValueResolverInterface::class); + $expectedToCallValueResolver->expects($this->once())->method('resolveArgument')->willReturn([123]); + + $resolver = self::getResolver([$failingValueResolver, ...ControllerArgumentResolver::getDefaultValueResolvers(), $expectedToCallValueResolver]); + $request = Request::create('/'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithFoo']; + + $actualArguments = $resolver->getArguments($request, $controller); + self::assertEquals([123], $actualArguments); + } + + public function testExceptionListSingle() + { + $failingValueResolverOne = new class implements ControllerValueResolverInterface { + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + throw new NearMissValueResolverException('Some reason why value could not be resolved.'); + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue(123); + } + }; + + $resolver = self::getResolver([$failingValueResolverOne]); + $request = Request::create('/'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithFoo']; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\ControllerArgumentResolverTestController::controllerWithFoo" requires the "$foo" argument that could not be resolved. Some reason why value could not be resolved.'); + $resolver->getArguments($request, $controller); + } + + public function testExceptionListMultiple() + { + $failingValueResolverOne = new class implements ControllerValueResolverInterface { + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + throw new NearMissValueResolverException('Some reason why value could not be resolved.'); + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue(123); + } + }; + $failingValueResolverTwo = new class implements ControllerValueResolverInterface { + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + throw new NearMissValueResolverException('Another reason why value could not be resolved.'); + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return new SourceValue(123); + } + }; + + $resolver = self::getResolver([$failingValueResolverOne, $failingValueResolverTwo]); + $request = Request::create('/'); + $controller = [new ControllerArgumentResolverTestController(), 'controllerWithFoo']; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\ControllerArgumentResolverTestController::controllerWithFoo" requires the "$foo" argument that could not be resolved. Possible reasons: 1) Some reason why value could not be resolved. 2) Another reason why value could not be resolved.'); + $resolver->getArguments($request, $controller); + } +} + +class ControllerArgumentResolverTestController +{ + public function __invoke($foo, $bar = null) + { + } + + public function controllerWithFoo($foo) + { + } + + public function controllerWithoutArguments() + { + } + + public function controllerWithFooAndDefaultBar($foo, $bar = null) + { + } + + public function controllerWithFooBarFoobar($foo, $bar, $foobar) + { + } + + public function controllerWithRequest(Request $request) + { + } + + public function controllerWithExtendingRequest(ExtendingRequest $request) + { + } + + public function controllerWithSession(Session $session) + { + } + + public function controllerWithSessionInterface(SessionInterface $session) + { + } + + public function controllerWithExtendingSession(ExtendingSession $session) + { + } + + public function controllerTargetingResolver(#[ValueResolver(DefaultValueResolver::class)] int $foo = 1) + { + } + + public function controllerTargetingResolverWithDefaultValue(#[ValueResolver(TestEntityValueResolver::class)] Post $foo = new Post('Default')) + { + } + + public function controllerTargetingResolverWithNullableValue(#[ValueResolver(TestEntityValueResolver::class)] ?Post $foo) + { + } + + public function controllerTargetingResolverWithTestEntity(#[ValueResolver(TestEntityValueResolver::class)] Post $foo) + { + } + + public function controllerDisablingResolver(#[ValueResolver(RequestAttributeValueResolver::class, disabled: true)] int $foo = 1) + { + } + + public function controllerTargetingManyResolvers( + #[ValueResolver(RequestAttributeValueResolver::class)] + #[ValueResolver(DefaultValueResolver::class)] + int $foo, + ) { + } + + public function controllerTargetingUnknownResolver( + #[ValueResolver('foo')] + int $bar, + ) { + } +} + +function controller_function($foo, $foobar) +{ +} + +class TestEntityValueResolver implements ControllerValueResolverInterface +{ + public function resolveArgument(ArgumentMetadata $argument, SourceValue $value): iterable + { + $title = $value->get(); + + return Post::class === $argument->getType() && SourceValue::NOT_FOUND !== $title + ? [new Post($title)] + : []; + } + + public function extractSourceValue(ArgumentMetadata $argument, Request $request): SourceValue + { + return $request->request->has('title') + ? new SourceValue($request->request->get('title')) + : SourceValue::notFound(); + } +} + +class Post +{ + public function __construct( + public readonly string $title, + ) { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php index 3fc74a1d701f5..7bf4ff4d8914a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php @@ -43,7 +43,7 @@ public function testDoNotSupportEmptyController() public function testController() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); + $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register it as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::method']); @@ -53,7 +53,7 @@ public function testController() public function testControllerWithATrailingBackSlash() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); + $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register it as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => '\\App\\Controller\\Mine::method']); @@ -63,7 +63,7 @@ public function testControllerWithATrailingBackSlash() public function testControllerWithMethodNameStartUppercase() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); + $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register it as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::Method']); @@ -73,7 +73,7 @@ public function testControllerWithMethodNameStartUppercase() public function testControllerNameIsAnArray() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); + $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register it as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => ['App\\Controller\\Mine', 'method']]); @@ -83,7 +83,7 @@ public function testControllerNameIsAnArray() public function testInvokableController() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::__invoke()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); + $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::__invoke()", maybe you forgot to register it as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\Controller\Mine']); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 77cf7d9c58adb..f698ba88a2c60 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -12,15 +12,15 @@ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; +use Symfony\Component\ArgumentResolver\Attribute\ValueResolver; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; -use Symfony\Component\HttpKernel\Attribute\ValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php index a1a80fe82f2c2..cffcb8c00eed5 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php @@ -12,13 +12,13 @@ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; +use Symfony\Component\ArgumentResolver\Exception\NearMissValueResolverException; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass; -use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; class ServiceValueResolverTest extends TestCase { diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index 93fe699fcd48b..a163f80652444 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -35,9 +35,9 @@ public function testSignature1() $arguments = $this->factory->createArgumentMetadata([$this, 'signature1']); $this->assertEquals([ - new ArgumentMetadata('foo', self::class, false, false, null, controllerName: $this::class.'::signature1'), - new ArgumentMetadata('bar', 'array', false, false, null, controllerName: $this::class.'::signature1'), - new ArgumentMetadata('baz', 'callable', false, false, null, controllerName: $this::class.'::signature1'), + new ArgumentMetadata('foo', self::class, false, false, null, callableName: $this::class.'::signature1'), + new ArgumentMetadata('bar', 'array', false, false, null, callableName: $this::class.'::signature1'), + new ArgumentMetadata('baz', 'callable', false, false, null, callableName: $this::class.'::signature1'), ], $arguments); } @@ -46,9 +46,9 @@ public function testSignature2() $arguments = $this->factory->createArgumentMetadata($this->signature2(...)); $this->assertEquals([ - new ArgumentMetadata('foo', self::class, false, true, null, true, controllerName: $this::class.'::signature2'), - new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, true, null, true, controllerName: $this::class.'::signature2'), - new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null, true, controllerName: $this::class.'::signature2'), + new ArgumentMetadata('foo', self::class, false, true, null, true, callableName: $this::class.'::signature2'), + new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, true, null, true, callableName: $this::class.'::signature2'), + new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null, true, callableName: $this::class.'::signature2'), ], $arguments); } @@ -57,8 +57,8 @@ public function testSignature3() $arguments = $this->factory->createArgumentMetadata($this->signature3(...)); $this->assertEquals([ - new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, false, null, controllerName: $this::class.'::signature3'), - new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null, controllerName: $this::class.'::signature3'), + new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, false, null, callableName: $this::class.'::signature3'), + new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null, callableName: $this::class.'::signature3'), ], $arguments); } @@ -67,9 +67,9 @@ public function testSignature4() $arguments = $this->factory->createArgumentMetadata($this->signature4(...)); $this->assertEquals([ - new ArgumentMetadata('foo', null, false, true, 'default', controllerName: $this::class.'::signature4'), - new ArgumentMetadata('bar', null, false, true, 500, controllerName: $this::class.'::signature4'), - new ArgumentMetadata('baz', null, false, true, [], controllerName: $this::class.'::signature4'), + new ArgumentMetadata('foo', null, false, true, 'default', callableName: $this::class.'::signature4'), + new ArgumentMetadata('bar', null, false, true, 500, callableName: $this::class.'::signature4'), + new ArgumentMetadata('baz', null, false, true, [], callableName: $this::class.'::signature4'), ], $arguments); } @@ -78,8 +78,8 @@ public function testSignature5() $arguments = $this->factory->createArgumentMetadata($this->signature5(...)); $this->assertEquals([ - new ArgumentMetadata('foo', 'array', false, true, null, true, controllerName: $this::class.'::signature5'), - new ArgumentMetadata('bar', null, false, true, null, true, controllerName: $this::class.'::signature5'), + new ArgumentMetadata('foo', 'array', false, true, null, true, callableName: $this::class.'::signature5'), + new ArgumentMetadata('bar', null, false, true, null, true, callableName: $this::class.'::signature5'), ], $arguments); } @@ -88,8 +88,8 @@ public function testVariadicSignature() $arguments = $this->factory->createArgumentMetadata([new VariadicController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('foo', null, false, false, null, controllerName: VariadicController::class.'::action'), - new ArgumentMetadata('bar', null, true, false, null, controllerName: VariadicController::class.'::action'), + new ArgumentMetadata('foo', null, false, false, null, callableName: VariadicController::class.'::action'), + new ArgumentMetadata('bar', null, true, false, null, callableName: VariadicController::class.'::action'), ], $arguments); } @@ -98,9 +98,9 @@ public function testBasicTypesSignature() $arguments = $this->factory->createArgumentMetadata([new BasicTypesController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('foo', 'string', false, false, null, controllerName: BasicTypesController::class.'::action'), - new ArgumentMetadata('bar', 'int', false, false, null, controllerName: BasicTypesController::class.'::action'), - new ArgumentMetadata('baz', 'float', false, false, null, controllerName: BasicTypesController::class.'::action'), + new ArgumentMetadata('foo', 'string', false, false, null, callableName: BasicTypesController::class.'::action'), + new ArgumentMetadata('bar', 'int', false, false, null, callableName: BasicTypesController::class.'::action'), + new ArgumentMetadata('baz', 'float', false, false, null, callableName: BasicTypesController::class.'::action'), ], $arguments); } @@ -109,9 +109,9 @@ public function testNamedClosure() $arguments = $this->factory->createArgumentMetadata($this->signature1(...)); $this->assertEquals([ - new ArgumentMetadata('foo', self::class, false, false, null, controllerName: $this::class.'::signature1'), - new ArgumentMetadata('bar', 'array', false, false, null, controllerName: $this::class.'::signature1'), - new ArgumentMetadata('baz', 'callable', false, false, null, controllerName: $this::class.'::signature1'), + new ArgumentMetadata('foo', self::class, false, false, null, callableName: $this::class.'::signature1'), + new ArgumentMetadata('bar', 'array', false, false, null, callableName: $this::class.'::signature1'), + new ArgumentMetadata('baz', 'callable', false, false, null, callableName: $this::class.'::signature1'), ], $arguments); } @@ -120,10 +120,10 @@ public function testNullableTypesSignature() $arguments = $this->factory->createArgumentMetadata([new NullableController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('foo', 'string', false, false, null, true, controllerName: NullableController::class.'::action'), - new ArgumentMetadata('bar', \stdClass::class, false, false, null, true, controllerName: NullableController::class.'::action'), - new ArgumentMetadata('baz', 'string', false, true, 'value', true, controllerName: NullableController::class.'::action'), - new ArgumentMetadata('last', 'string', false, true, '', false, controllerName: NullableController::class.'::action'), + new ArgumentMetadata('foo', 'string', false, false, null, true, callableName: NullableController::class.'::action'), + new ArgumentMetadata('bar', \stdClass::class, false, false, null, true, callableName: NullableController::class.'::action'), + new ArgumentMetadata('baz', 'string', false, true, 'value', true, callableName: NullableController::class.'::action'), + new ArgumentMetadata('last', 'string', false, true, '', false, callableName: NullableController::class.'::action'), ], $arguments); } @@ -132,7 +132,7 @@ public function testAttributeSignature() $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], controllerName: AttributeController::class.'::action'), + new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], callableName: AttributeController::class.'::action'), ], $arguments); } @@ -146,8 +146,8 @@ public function testIssue41478() { $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'issue41478']); $this->assertEquals([ - new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], controllerName: AttributeController::class.'::issue41478'), - new ArgumentMetadata('bat', 'string', false, false, null, false, [], controllerName: AttributeController::class.'::issue41478'), + new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], callableName: AttributeController::class.'::issue41478'), + new ArgumentMetadata('bat', 'string', false, false, null, false, [], callableName: AttributeController::class.'::issue41478'), ], $arguments); } diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php index 3c2d6738f5d1e..a860672cbe1b8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ControllerArgumentResolver; use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass; use Symfony\Component\Stopwatch\Stopwatch; @@ -35,9 +36,9 @@ public function testServicesAreOrderedAccordingToPriority() new Reference('n3'), ]; - $definition = new Definition(ArgumentResolver::class, [null, []]); + $definition = new Definition(ControllerArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); - $container->setDefinition('argument_resolver', $definition); + $container->setDefinition('controller.argument_resolver', $definition); foreach ($services as $id => [$tag]) { $container->register($id)->addTag('controller.argument_value_resolver', $tag); @@ -70,7 +71,7 @@ public function testInDebugWithStopWatchDefinition() $definition = new Definition(ArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); $container->register('debug.stopwatch', Stopwatch::class); - $container->setDefinition('argument_resolver', $definition); + $container->setDefinition('controller.argument_resolver', $definition); foreach ($services as $id => [$tag]) { $container->register($id)->addTag('controller.argument_value_resolver', $tag); @@ -97,7 +98,7 @@ public function testInDebugWithouStopWatchDefinition() $definition = new Definition(ArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); $container->register('n1')->addTag('controller.argument_value_resolver'); - $container->setDefinition('argument_resolver', $definition); + $container->setDefinition('controller.argument_resolver', $definition); $container->setParameter('kernel.debug', true); @@ -112,7 +113,7 @@ public function testReturningEmptyArrayWhenNoService() { $definition = new Definition(ArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); - $container->setDefinition('argument_resolver', $definition); + $container->setDefinition('controller.argument_resolver', $definition); $container->setParameter('kernel.debug', false); @@ -126,6 +127,6 @@ public function testNoArgumentResolver() (new ControllerArgumentValueResolverPass())->process($container); - $this->assertFalse($container->hasDefinition('argument_resolver')); + $this->assertFalse($container->hasDefinition('controller.argument_resolver')); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php index 2e1f7d58b7258..9da75d708085f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php @@ -21,6 +21,7 @@ use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; use Symfony\Component\HttpKernel\Attribute\WithLogLevel; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ControllerArgumentResolver; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; @@ -286,7 +287,7 @@ public function testOnControllerArguments(callable $controller) $kernel = $this->createMock(HttpKernelInterface::class); $kernel->method('handle')->willReturnCallback(function (Request $request) use ($listener, $controller, $kernel) { $this->assertSame($controller, $request->attributes->get('_controller')); - $arguments = (new ArgumentResolver())->getArguments($request, $controller); + $arguments = (new ControllerArgumentResolver())->getArguments($request, $controller); $event = new ControllerArgumentsEvent($kernel, $controller, $arguments, $request, HttpKernelInterface::SUB_REQUEST); $listener->onControllerArguments($event);