Skip to content

[HttpKernel] Introduce pinnable value resolvers with #[ValueResolver] and #[AsPinnedValueResolver] #48992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@

namespace Symfony\Bridge\Doctrine\Attribute;

use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;

/**
* Indicates that a controller argument should receive an Entity.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class MapEntity
class MapEntity extends ValueResolver
{
public function __construct(
public ?string $class = null,
Expand All @@ -26,8 +29,10 @@ public function __construct(
public ?bool $stripNull = null,
public array|string|null $id = null,
public ?bool $evictCache = null,
public bool $disabled = false,
bool $disabled = false,
string $resolver = EntityValueResolver::class,
) {
parent::__construct($resolver, $disabled);
}

public function withDefaults(self $defaults, ?string $class): static
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bridge/Doctrine/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"symfony/config": "^5.4|^6.0",
"symfony/dependency-injection": "^6.2",
"symfony/form": "^5.4.21|^6.2.7",
"symfony/http-kernel": "^6.2",
"symfony/http-kernel": "^6.3",
"symfony/messenger": "^5.4|^6.0",
"symfony/doctrine-messenger": "^5.4|^6.0",
"symfony/property-access": "^5.4|^6.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
use Symfony\Component\HttpClient\UriTemplateHttpClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Attribute\AsPinnedValueResolver;
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
Expand Down Expand Up @@ -691,6 +692,10 @@ public function load(array $configs, ContainerBuilder $container)
$definition->addTag('messenger.message_handler', $tagAttributes);
});

$container->registerAttributeForAutoconfiguration(AsPinnedValueResolver::class, static function (ChildDefinition $definition, AsPinnedValueResolver $attribute): void {
$definition->addTag('controller.pinned_value_resolver', $attribute->name ? ['name' => $attribute->name] : []);
});

if (!$container->getParameter('kernel.debug')) {
// remove tagged iterator argument for resource checkers
$container->getDefinition('config_cache_factory')->setArguments([]);
Expand Down
19 changes: 10 additions & 9 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,40 +46,41 @@
->args([
service('argument_metadata_factory'),
abstract_arg('argument value resolvers'),
abstract_arg('pinned value resolvers'),
])

->set('argument_resolver.backed_enum_resolver', BackedEnumValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => 100])
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => BackedEnumValueResolver::class])

->set('argument_resolver.uid', UidValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => 100])
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => UidValueResolver::class])

->set('argument_resolver.datetime', DateTimeValueResolver::class)
->args([
service('clock')->nullOnInvalid(),
])
->tag('controller.argument_value_resolver', ['priority' => 100])
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => DateTimeValueResolver::class])

->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => 100])
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => RequestAttributeValueResolver::class])

->set('argument_resolver.request', RequestValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => 50])
->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => RequestValueResolver::class])

->set('argument_resolver.session', SessionValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => 50])
->tag('controller.argument_value_resolver', ['priority' => 50, 'name' => SessionValueResolver::class])

->set('argument_resolver.service', ServiceValueResolver::class)
->args([
abstract_arg('service locator, set in RegisterControllerArgumentLocatorsPass'),
])
->tag('controller.argument_value_resolver', ['priority' => -50])
->tag('controller.argument_value_resolver', ['priority' => -50, 'name' => ServiceValueResolver::class])

->set('argument_resolver.default', DefaultValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => -100])
->tag('controller.argument_value_resolver', ['priority' => -100, 'name' => DefaultValueResolver::class])

->set('argument_resolver.variadic', VariadicValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => -150])
->tag('controller.argument_value_resolver', ['priority' => -150, 'name' => VariadicValueResolver::class])

->set('response_listener', ResponseListener::class)
->args([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
->args([
service('security.token_storage'),
])
->tag('controller.argument_value_resolver', ['priority' => 120])
->tag('controller.argument_value_resolver', ['priority' => 120, 'name' => UserValueResolver::class])

// Authentication related services
->set('security.authentication.trust_resolver', AuthenticationTrustResolver::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Attribute;

/**
* Service tag to autoconfigure pinned value resolvers.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsPinnedValueResolver
{
public function __construct(
public readonly ?string $name = null,
) {
}
}
9 changes: 7 additions & 2 deletions src/Symfony/Component/HttpKernel/Attribute/MapDateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@

namespace Symfony\Component\HttpKernel\Attribute;

use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;

/**
* Controller parameter tag to configure DateTime arguments.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class MapDateTime
class MapDateTime extends ValueResolver
{
public function __construct(
public readonly ?string $format = null
public readonly ?string $format = null,
bool $disabled = false,
string $resolver = DateTimeValueResolver::class,
) {
parent::__construct($resolver, $disabled);
}
}
27 changes: 27 additions & 0 deletions src/Symfony/Component/HttpKernel/Attribute/ValueResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Attribute;

use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;

#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)]
class ValueResolver
{
/**
* @param class-string<ValueResolverInterface>|string $name
*/
public function __construct(
public string $name,
public bool $disabled = false,
) {
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CHANGELOG
* Use an instance of `Psr\Clock\ClockInterface` to generate the current date time in `DateTimeValueResolver`
* Add `#[WithLogLevel]` for defining log levels for exceptions
* Add `skip_response_headers` to the `HttpCache` options
* Introduce pinnable value resolvers with `#[ValueResolver]` and `#[AsPinnedValueResolver]`

6.2
---
Expand Down
60 changes: 49 additions & 11 deletions src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

namespace Symfony\Component\HttpKernel\Controller;

use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
Expand All @@ -20,6 +22,8 @@
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface;
use Symfony\Component\HttpKernel\Exception\ResolverNotFoundException;
use Symfony\Contracts\Service\ServiceProviderInterface;

/**
* Responsible for resolving the arguments passed to an action.
Expand All @@ -30,25 +34,54 @@ final class ArgumentResolver implements ArgumentResolverInterface
{
private ArgumentMetadataFactoryInterface $argumentMetadataFactory;
private iterable $argumentValueResolvers;
private ?ContainerInterface $namedResolvers;

/**
* @param iterable<mixed, ArgumentValueResolverInterface|ValueResolverInterface> $argumentValueResolvers
*/
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable $argumentValueResolvers = [])
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable $argumentValueResolvers = [], ContainerInterface $namedResolvers = null)
{
$this->argumentMetadataFactory = $argumentMetadataFactory ?? new ArgumentMetadataFactory();
$this->argumentValueResolvers = $argumentValueResolvers ?: self::getDefaultArgumentValueResolvers();
$this->namedResolvers = $namedResolvers;
}

public function getArguments(Request $request, callable $controller, \ReflectionFunctionAbstract $reflector = null): array
{
$arguments = [];

foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller, $reflector) as $metadata) {
foreach ($this->argumentValueResolvers as $resolver) {
$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->name] = true;
} elseif ($resolverName) {
throw new \LogicException(sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $metadata->getName(), $this->getPrettyName($controller)));
} else {
$resolverName = $attribute->name;
}
}

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)];
}
}

foreach ($argumentValueResolvers as $name => $resolver) {
if ((!$resolver instanceof ValueResolverInterface || $resolver instanceof TraceableValueResolver) && !$resolver->supports($request, $metadata)) {
continue;
}
if (isset($disabledResolvers[\is_int($name) ? $resolver::class : $name])) {
continue;
}

$count = 0;
foreach ($resolver->resolve($request, $metadata) as $argument) {
Expand All @@ -70,15 +103,7 @@ public function getArguments(Request $request, callable $controller, \Reflection
}
}

$representative = $controller;

if (\is_array($representative)) {
$representative = sprintf('%s::%s()', $representative[0]::class, $representative[1]);
} elseif (\is_object($representative)) {
$representative = get_debug_type($representative);
}

throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.', $representative, $metadata->getName()));
throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.', $this->getPrettyName($controller), $metadata->getName()));
}

return $arguments;
Expand All @@ -97,4 +122,17 @@ public static function getDefaultArgumentValueResolvers(): iterable
new VariadicValueResolver(),
];
}

private function getPrettyName($controller): string
{
if (\is_array($controller)) {
return $controller[0]::class.'::'.$controller[1];
}

if (\is_object($controller)) {
return get_debug_type($controller);
}

return $controller;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
namespace Symfony\Component\HttpKernel\DependencyInjection;

use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -37,10 +39,24 @@ public function process(ContainerBuilder $container)
return;
}

$resolvers = $this->findAndSortTaggedServices('controller.argument_value_resolver', $container);
$definitions = $container->getDefinitions();
$namedResolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('controller.pinned_value_resolver', 'name', needsIndexes: true), $container);
$resolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('controller.argument_value_resolver', 'name', needsIndexes: true), $container);

foreach ($resolvers as $name => $resolverReference) {
$id = (string) $resolverReference;

if ($definitions[$id]->hasTag('controller.pinned_value_resolver')) {
unset($resolvers[$name]);
} else {
$namedResolvers[$name] ??= clone $resolverReference;
}
}

$resolvers = array_values($resolvers);

if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class) && $container->has('debug.stopwatch')) {
foreach ($resolvers as $resolverReference) {
foreach ($resolvers + $namedResolvers as $resolverReference) {
$id = (string) $resolverReference;
$container->register("debug.$id", TraceableValueResolver::class)
->setDecoratedService($id)
Expand All @@ -51,6 +67,7 @@ public function process(ContainerBuilder $container)
$container
->getDefinition('argument_resolver')
->replaceArgument(1, new IteratorArgument($resolvers))
->setArgument(2, new ServiceLocatorArgument($namedResolvers))
;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\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);
}
}
Loading