Skip to content
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

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

Merged
merged 1 commit into from
Mar 6, 2023

Conversation

MatTheCat
Copy link
Contributor

@MatTheCat MatTheCat commented Jan 16, 2023

Q A
Branch? 6.3
Bug fix? no
New feature? yes
Deprecations? no
Tickets Fix #48927
License MIT
Doc PR symfony/symfony-docs#17763

Introducing a new ValueResolver attribute, which allows to

  • “pin” a value resolver to an argument, meaning only said resolver will be called
  • prevent a resolver to be called for an argument

Every existing resolver-related attribute (MapEntity, CurrentUser…) now extends ValueResolver.

Each controller.argument_value_resolver tag is added a name attribute, which is the resolver’s FQCN. This is the first argument ValueResolver expects.

A new AsPinnedValueResolver attribute is added for autoconfiguration, adding the controller.pinned_value_resolver tag. Such resolvers can only be “pinned”, meaning they won’t ever be called for an argument missing the ValueResolver attribute.

@carsonbot carsonbot added this to the 6.3 milestone Jan 16, 2023
@MatTheCat MatTheCat changed the title [HttpKernel] Add #[ValueResolver] for specifying a parameter argume… [HttpKernel] Add #[ValueResolver] for specifying a parameter argument resolver Jan 16, 2023
@MatTheCat MatTheCat changed the title [HttpKernel] Add #[ValueResolver] for specifying a parameter argument resolver [HttpKernel] Add #[ValueResolver] for specifying an argument resolver Jan 16, 2023
@MatTheCat MatTheCat changed the title [HttpKernel] Add #[ValueResolver] for specifying an argument resolver [HttpKernel] Add #[ValueResolver] for specifying a controller argument resolver Jan 16, 2023
Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature makes sense to me

@MatTheCat MatTheCat force-pushed the ticket_48927 branch 2 times, most recently from 539c344 to 3ddc732 Compare January 16, 2023 11:35
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For param converters we have this behavior:

You can register a converter by priority, by name (attribute "converter"), or both. If you don't specify a priority or a name, the converter will be added to the converter stack with a priority of 0. To explicitly disable the registration by priority you have to set priority="false" in your tag definition.

Do we want a way to not register a value resolver in the iterator and make it accessible only via explicit attribute? That's require accepting both an iterator and a locator on the constructor.

@MatTheCat
Copy link
Contributor Author

MatTheCat commented Jan 16, 2023

Thanks for the review!

Do we want a way to not register a value resolver in the iterator and make it accessible only via explicit attribute?

From what I understood, we only want a way to force a specific resolver to be used for an argument.

You can register a converter […] by name

Maybe we could do the same? This would require to name existing resolvers.

@MatTheCat MatTheCat force-pushed the ticket_48927 branch 3 times, most recently from 165a0e2 to e1cebd9 Compare January 16, 2023 16:41
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is my review as a diff proposal:

diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
index 3ce8519c26..ab318d1334 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php
@@ -11,7 +11,6 @@
 
 namespace Symfony\Component\HttpKernel\Controller;
 
-use Symfony\Component\DependencyInjection\ServiceLocator;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Attribute\ValueResolver;
 use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
@@ -31,34 +30,39 @@ use Symfony\Contracts\Service\ServiceProviderInterface;
  */
 final class ArgumentResolver implements ArgumentResolverInterface
 {
-    private ArgumentMetadataFactoryInterface $argumentMetadataFactory;
-    private ServiceProviderInterface $argumentValueResolvers;
-
     /**
      * @param iterable<mixed, ArgumentValueResolverInterface|ValueResolverInterface>|ServiceProviderInterface<ValueResolverInterface> $argumentValueResolvers
      */
-    public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable|ServiceProviderInterface $argumentValueResolvers = [])
-    {
-        $this->argumentMetadataFactory = $argumentMetadataFactory ?? new ArgumentMetadataFactory();
-        $this->argumentValueResolvers = $argumentValueResolvers instanceof ServiceProviderInterface
-            ? $argumentValueResolvers
-            : self::getServiceProvider($argumentValueResolvers ?: self::getDefaultArgumentValueResolvers())
-        ;
+    public function __construct(
+        private ArgumentMetadataFactoryInterface|null $argumentMetadataFactory = new ArgumentMetadataFactory(),
+        private iterable|ServiceProviderInterface $argumentValueResolvers = [],
+    ) {
+        if (null === $argumentMetadataFactory) {
+            $this->argumentMetadataFactory = new ArgumentMetadataFactory();
+            trigger_deprecation('symfony/http-kernel', '6.3', 'Passing null as $argumentMetadataFactory to "%s" is deprecated. Pass a "%s" instead.', static::class, ArgumentMetadataFactory::class);
+        }
     }
 
     public function getArguments(Request $request, callable $controller, \ReflectionFunctionAbstract $reflector = null): array
     {
         $arguments = [];
-        $resolversName = array_keys($this->argumentValueResolvers->getProvidedServices());
+        $resolversMap = null;
+        $allResolvers = $this->argumentValueResolvers ?: self::getDefaultArgumentValueResolvers();
+        $isProvider = $allResolvers instanceof ServiceProviderInterface;
 
         foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller, $reflector) as $metadata) {
-            $argumentResolversName = $resolversName;
+
             if ($attributes = $metadata->getAttributesOfType(ValueResolver::class)) {
-                $argumentResolversName = [$attributes[0]->name];
+                $name = $attributes[0]->name;
+                $resolvers = [$name => $isProvider ? null : ($resolversMap ??= self::getResolversMap($allResolvers))[$name]];
+            } else {
+                $resolvers = $isProvider ? $allResolvers->getProvidedServices() : $allResolvers;
             }
 
-            foreach ($argumentResolversName as $resolverName) {
-                $resolver = $this->argumentValueResolvers->get($resolverName);
+            foreach ($resolvers as $name => $resolver) {
+                if ($isProvider) {
+                    $resolver = $allResolvers->get($name);
+                }
 
                 if ((!$resolver instanceof ValueResolverInterface || $resolver instanceof TraceableValueResolver) && !$resolver->supports($request, $metadata)) {
                     continue;
@@ -115,16 +119,16 @@ final class ArgumentResolver implements ArgumentResolverInterface
     /**
      * @param iterable<ArgumentValueResolverInterface|ValueResolverInterface> $resolvers
      *
-     * @return ServiceProviderInterface<ArgumentValueResolverInterface|ValueResolverInterface>
+     * @return array<string, ArgumentValueResolverInterface|ValueResolverInterface>
      */
-    private static function getServiceProvider(iterable $resolvers): ServiceProviderInterface
+    private static function getResolversMap(iterable $resolvers): array
     {
-        $factories = [];
+        $map = [];
         $i = 0;
         foreach ($resolvers as $name => $resolver) {
-            $factories[$name === $i++ ? $resolver::class : $name] = static fn () => $resolver;
+            $map[$name === $i++ ? $resolver::class : $name] = $resolver;
         }
 
-        return new ServiceLocator($factories);
+        return $map;
     }
 }

From what I understood, we only want a way to force a specific resolver to be used for an argument.

Sure, but this is unrelated to my question: do we want a way to make a value resolver accessible only via the attribute? The extra bundle allows this. Do we also want that capability? I don't have an answer yet but I'm still wondering.

@MatTheCat
Copy link
Contributor Author

MatTheCat commented Jan 16, 2023

Hmm the ServiceLocatorTagPass is sorting services by key, thus overriding the priority order (from #22175) 🤔

Is there a way to circumvent this behavior?

do we want a way to make a value resolver accessible only via the attribute?

I personally never had any use-case for this so I cannot say.

@stof
Copy link
Member

stof commented Jan 17, 2023

a ServiceLocator does not guarantee iteration order. To me, this means you should inject 2 different arguments:

  • an iterator sorted by priority
  • a service locator keyed by name and used only for this ValueResolver argument.

@stof
Copy link
Member

stof commented Jan 17, 2023

An alternative way is to make this ValueResolver be processed by a ValueResolver registered first (with a very high priority) and receiving this ServiceLocator instead of making it a special feature of the ArgumentResolver.

@MatTheCat
Copy link
Contributor Author

Thanks @stof for the leads 👍

Is there any incentive for implementing one rather than the other?

@nicolas-grekas
Copy link
Member

I like @stof's idea! Less low level complexity, each class their use case.

@MatTheCat
Copy link
Contributor Author

Okay, going with a dedicated value resolver.

@MatTheCat MatTheCat force-pushed the ticket_48927 branch 3 times, most recently from d74a72c to 3701de7 Compare January 17, 2023 14:43
@nicolas-grekas
Copy link
Member

/cc @symfony/mergers I'd like to merge this to unlock some other PRs. Could you please have a look? 🙏

Copy link
Member

@lyrixx lyrixx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼 for the feature

@nicolas-grekas
Copy link
Member

Thank you @MatTheCat.

@nicolas-grekas nicolas-grekas merged commit b6bf9de into symfony:6.3 Mar 6, 2023
@MatTheCat MatTheCat deleted the ticket_48927 branch March 9, 2023 09:32
nicolas-grekas added a commit that referenced this pull request Mar 9, 2023
…solvers (nicolas-grekas)

This PR was merged into the 6.3 branch.

Discussion
----------

[HttpKernel] Renamed "pinned" to "targeted" for value resolvers

| Q             | A
| ------------- | ---
| Branch?       | 6.3
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

"pinned" was introduced in #48992 but a quick chat within the core-team lead to this proposal, which might be preferred. WDYT?

Commits
-------

ad58cc6 [HttpKernel] Renamed "pinned" to "targeted" for value resolvers
MatTheCat pushed a commit to MatTheCat/symfony-docs that referenced this pull request Mar 10, 2023
MatTheCat pushed a commit to MatTheCat/symfony-docs that referenced this pull request Mar 11, 2023
MatTheCat pushed a commit to MatTheCat/symfony-docs that referenced this pull request Mar 13, 2023
MatTheCat pushed a commit to MatTheCat/symfony-docs that referenced this pull request Mar 22, 2023
MatTheCat pushed a commit to MatTheCat/symfony-docs that referenced this pull request Apr 21, 2023
@fabpot fabpot mentioned this pull request May 1, 2023
MatTheCat pushed a commit to MatTheCat/symfony-docs that referenced this pull request May 24, 2023
nicolas-grekas added a commit that referenced this pull request May 29, 2023
…rs (HypeMC)

This PR was merged into the 6.3 branch.

Discussion
----------

[HttpKernel] Fix default value ignored with pinned resolvers

| Q             | A
| ------------- | ---
| Branch?       | 6.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Since #48992 the default value is ignored when, for example, `#[MapEntity]` is used:

```php
#[Route('/')]
#[Route('/{someId}')]
public function index(#[MapEntity(id: 'someId')] ?Post $post): Response
{
    // ...
}
```

Before, `$post` would be `null` when making a request to `/`, now an exception is thrown:

```
Controller "App\Controller\TestController::index" requires that you provide a value for the "$post" argument.
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.
```

Since I can't think of a valid case when one would want to ignore the default value, I'd suggest always adding the `DefaultValueResolver` to the list when a pinned resolver is used.

Commits
-------

fabe7bc [HttpKernel] Fix default value ignored with pinned resolvers
MatTheCat pushed a commit to MatTheCat/symfony-docs that referenced this pull request Jun 4, 2023
javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request Jun 15, 2023
…edValueResolver]` (Mathieu, MatTheCat)

This PR was merged into the 6.3 branch.

Discussion
----------

[HttpKernel] Document `#[ValueResolver]` and `#[AsTargetedValueResolver]`

cf. symfony/symfony#48992

Commits
-------

b57df54 Reword disabled resolvers paragraph
432c2eb Remove “pin” usage remnants
ac13dfc Update “Managing Value Resolvers” section
31edf49 Fix code samples
1009ca2 Do not hardcode priorities
50450a4 Address #17763 (comment)
7fc8065 Document symfony/symfony#48992
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow to define a specific controller value resolver
8 participants