Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Added an ArgumentResolver with clean extension point #18308

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 5 commits into from
Apr 3, 2016
Merged

Added an ArgumentResolver with clean extension point #18308

merged 5 commits into from
Apr 3, 2016

Conversation

linaori
Copy link
Contributor

@linaori linaori commented Mar 25, 2016

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? yes
Tests pass? yes
Fixed tickets #17933 (pre-work), #1547, #10710
License MIT
Doc PR symfony/symfony-docs#6422

This PR is a follow up for and blocked by: #18187, relates to #11457 by @wouterj. When reviewing, please take the last commit: Added an ArgumentResolver with clean extension point

This PR provides:

  • The ability to tag your own ArgumentValueResolverInterface. This means that you can effectively expand on the argument resolving in the HttpKernel without having to implement your own ArgumentResolver.
  • The possibility to cache away argument metadata via a new ArgumentMetadataFactory which simply fetches the data from the cache, effectively omitting 1 reflection call per request. Not implemented in this PR, but possible once this is merged.
  • The possibility to add a PSR-7 adapter to resolve the correct request, avoids the paramconverters
  • The possibility to add a value resolver to fetch stuff from $request->query
  • Drupal could simplify their argument resolving by a lot
  • etc.

The aim for this PR is to provide a 100% BC variant to add argument resolving in a clean way, this is shown by the 2 tests: LegacyArgumentResolverTest and ArgumentResolverTest.

/cc @dawehner @larowlan if you have time, can you check the impact for Drupal? I think this should be a very simple change which should make it more maintainable.

public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('argument_resolver');
$argument_resolvers = $this->findAndSortTaggedServices('controller_argument.value_resolver', $container);
Copy link
Contributor

Choose a reason for hiding this comment

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

Variable should be camel cased.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Woops, will fix this, thanks!

@Koc
Copy link
Contributor

Koc commented Mar 25, 2016

What benefits of ArgumentMetadata class instead of passing ReflectionProperty? https://github.com/symfony/symfony/pull/11457/files#diff-e00e36af5c58d36a069a385025be4f34R22 looks better than your ArgumentIsRequest

*/
public function getValue(Request $request, ArgumentMetadataInterface $argument)
{
return $request->attributes->all()[$argument->getArgumentName()];
Copy link
Contributor

Choose a reason for hiding this comment

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

get() instead of all()[]?

@Koc
Copy link
Contributor

Koc commented Mar 25, 2016

Also #11457 wasn't merged because of lack of performance tests. Maybe you should add blackfire comparation before and after.

@linaori
Copy link
Contributor Author

linaori commented Mar 25, 2016

@Koc for the simple reason that it's reflection. When this PR is merged, I can add a cache warmer that calculates all this information already so no reflection would be required run-time (unless not in cache).

Additionally it also avoids PHP_VERSION_ID checks because types and variadic differs between 5.5, 5.6 and 7.0, see 6496e67#diff-4cbcb4a4736436b811aa3f5059ca46b1R19

@linaori
Copy link
Contributor Author

linaori commented Mar 25, 2016

@Koc how would I do that? I have to admit that I've never used blackfire yet. In terms of performance this might actually be slightly slower (minimally) until a cached variant has been added.

*/
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('argument_resolver');
Copy link
Member

Choose a reason for hiding this comment

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

first, check if the definition exists and return early otherwise.

@Koc
Copy link
Contributor

Koc commented Mar 25, 2016

https://blackfire.io/docs/up-and-running/installation
http://blog.blackfire.io/profiles-public-sharing.html

Create simple app based on symfony-standard, create profile, replace symfony's version with your patched, create profile again and compare them.

@linaori
Copy link
Contributor Author

linaori commented Mar 25, 2016

@Koc alright, I will see if I can do that

*/
public function supports(Request $request, ArgumentMetadataInterface $argument)
{
return $argument->hasDefaultValue() && !$request->attributes->has($argument->getArgumentName());
Copy link
Contributor

Choose a reason for hiding this comment

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

Is second check really needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's pretty much a failsafe for when the ArgumentFromAttribute is triggered after this one. In the framework bundle I've solved this with priorities but here I'd like to make sure it's always triggered only when no default could be found. This would be equal to the if/else check in the LegacyArgumentResolver

@linaori
Copy link
Contributor Author

linaori commented Mar 25, 2016

@Koc I made an application that can be tested https://github.com/iltar/blackfire-symfony-18308

Sadly I cannot run blackfire here due to system permissions.

<argument type="collection" />
</service>

<service id="argument_metadata_actory" class="Symfony\Component\HttpKernel\ControllerMetadata\Argument\ArgumentMetadataFactory" public="false" />
Copy link
Member

Choose a reason for hiding this comment

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

typo argument_metadata_factory

@fabpot
Copy link
Member

fabpot commented Mar 28, 2016

This is an interesting topic as we've been talking about doing this kind of changes for years now.

Having more flexibility looks good on paper but we need to keep complexity as low as possible (that's why I'm quite reluctant on adding interfaces and factories when not needed -- see my comments). Also, we need to keep good performance. This last point is probably the only blocker if any. Can you profile the old and the new code on a "typical Symfony app" (not an hello world one) to see if there is an impact and what kind if impact these changes have if any?

@linaori
Copy link
Contributor Author

linaori commented Mar 28, 2016

@fabpot I'll hook it up somewhere this week and check the performance. Ideally I'd also implement a cache for the benchmarks (because that's what this is all pre-work for).

I agree about the complexity, that's why I want to keep this as simple as possible. Here's a list of the classes/interfaces and what they are designed for:

  • ArgumentResolver(Interface); This is from the previous PR, making it easier to implement your own variant (this PR). This also allows the LegacyArgumentResolver (currently in ControllerResolver) to be injected.
  • ArgumentValueResolverInterface; This is what can actually be used on an application level for action arguments, think about: ParamConverter to inject the current user #327. Parameter converters are nice, but slow(er than when this is cached) due to their reflection. This involves less magic to implement and a clear trace for developers where it hooks in. It also avoids injecting objects in the Request object, scoping them more effectively.
  • ArgumentMetadataInterface; As I have commented before, this is a nice way to use the following bullet point to return a cached variant if generating classes.
  • ArgumentMetadataFactoryInterface; This allows multiple implementations to accommodate a cache layer. My idea is to decorate the real-time resolver, when the cached version does not exist, I can fall back to the real-time and still provide the same functionality. I think this interface should be marked with @internal but I'm not 100% sure, in case someone wants to provide their own layer (like never resolving due to performance?).

Regarding the cache implementation I want to open a new PR so it can focus on that specific subject as I'm inexperienced with caching.

@fabpot
Copy link
Member

fabpot commented Mar 28, 2016

Caching should indeed be part of another PR. Performance without caching should stay in the same range as the current performance. Adding a cache layer is adding another layer of complexity by itself and won't be implemented by everyone (think Silex for instance). That's why performance optimization without caching is very important.

But then again, without numbers, we cannot really reason about the proposed changes.

@linaori
Copy link
Contributor Author

linaori commented Mar 28, 2016

You've got a fair point there. In any case, I could rename the LegacyArgumentResolver to something else and make sure everyone can keep using that regarding performance.

@javiereguiluz
Copy link
Member

@iltar thanks for working on this. Although I cannot provide specific details, I agree with Fabien and this looks a bit over engineered. The fact that adds +1,200 lines of code and removes just 111 might be an warning about this.

@linaori
Copy link
Contributor Author

linaori commented Mar 29, 2016

@fabpot here are the results of my real-world application with the code as-is in this PR.

@javiereguiluz a lot of those lines are actually test duplication to provide 100% BC and a lot of those lines are also comments because a lot of small files got added. If you look at executable lines of code excluding tests, the difference is a lot less.

I agree that it's a bit of complexity added, but it would solve a lot of other problems; Mainly DX wise. Right now it's a pain to customize this code and is highly dependent on the default implementation. With this PR (e.g.) Drupal could significantly reduce the complexity in their code if they want. It also means that they would not have to backport the current fix for the variadic functionality either.

It means that I could add the following code instead of a ParamConverter

    /**
     * {@inheritdoc}
     */
    public function supports(Request $request, ArgumentMetadataInterface $argument)
    {
        return $argument->getArgumentType() === MyUser::class;
    }

    /**
     * {@inheritdoc}
     */
    public function getValue(Request $request, ArgumentMetadataInterface $argument)
    {
        return $this->tokenStorage->getToken()->getUser();
    }

The initial overhead is a bit more, but a ParamConverter is conceptually wrong here and causes a lot more overhead as it uses reflection to determine the same information yet again. For Developers it will be easier


Conclusion: it's slower in this PR, not by much but it is slower. The difference with 0 parameters is minimal and gains ~initial time*N Arguments. I think the added DX and flexibility is worth the minimal overhead, especially when you can avoid some of the magic done by ParamConverters.

note that if this functionality is not desired in the core, I can always publish it myself if #18187 gets accepted

// code used in HttpKernel
$sw = new Stopwatch();
$sw->start('18308');
// controller arguments
for ($i = 0; $i < 1000; $i++) {
    $arguments = $this->argumentResolver->getArguments($request, $controller);
}
$event = $sw->stop('18308');
$class = explode('\\', get_class($controller[0]));
dump(end($class).': '.$event.'; arguments: '.count($arguments).', '.$i.' iteration(s); branch: feature/argument-resolver-extention-point');

1 iteration on getArguments (master)

1 iteration on getArguments (feature/argument-resolver-extention-point)

1000 iterations on getArguments (master)

1000 iterations on getArguments (feature/argument-resolver-extention-point)

Same as the above but changed the variadic priority to -150 (as it cannot have a default value and is a rare case)

@HeahDude
Copy link
Contributor

HeahDude commented Apr 1, 2016

@iltar having separated commits will certainly ease reviews, but once done you should squash mines, they are not relevant. Thank you ;)

@linaori
Copy link
Contributor Author

linaori commented Apr 1, 2016

Docs PR is made regarding the controller resolver / argument resolver, so that should be ready before 3.1 arrives. I will start working on writing something for the new extension point (otherwise it's pretty useless imo).

@linaori
Copy link
Contributor Author

linaori commented Apr 1, 2016

@fabpot I'm having doubts about the resolvers:

service (prefixed by argument_value_resolver.) class
...argument_from_attribute ArgumentFromAttributeResolver
...argument_is_request DefaultArgumentValueResolver
...default_argument_value RequestResolver
...variadic_argument_from_attribute VariadicArgumentValueResolver

I would like to propose the following naming before this is merged:

service class
argument_value_resolver.request_attribute RequestAttributeValueResolver
argument_value_resolver.default DefaultValueResolver
argument_value_resolver.request RequestValueResolver
argument_value_resolver.variadic VariadicValueResolver

While writing the docs I noticed it's still fairly inconsistent and a lot to write.

@javiereguiluz
Copy link
Member

@iltar don't you think the _value_ part in argument_value_resolver.* is a bit redundant? What about:

service class
argument_resolver.request_attribute RequestAttributeValueResolver
argument_resolver.default DefaultValueResolver
argument_resolver.request RequestValueResolver
argument_resolver.variadic VariadicValueResolver

@fabpot
Copy link
Member

fabpot commented Apr 1, 2016

the new names are indeed much more consistent. I also like @javiereguiluz shorter proposal. Up to you between the 2 new proposals.

@linaori
Copy link
Contributor Author

linaori commented Apr 1, 2016

I like that, less is more in this case. I was running into another DX issue; when showing the ArgumentResolver in the documentation, I always have to pass an array of 4 items which together mimic the 3.0 behaviour. If I were to make the argument of the value resolvers nullable, I could set those 4 resolvers by default in the constructor. Or is an empty array enough? This implies you cannot ever have no resolvers.

@linaori
Copy link
Contributor Author

linaori commented Apr 1, 2016

Okay, small update:

  • The ArgumentResolver arguments were optional (don't know why, lost in rebase). I've made use of that and initialized the objects with the defaults that symfony is using now.
  • The HttpKernel now gives a slightly different deprecation about a behavioral change in 4.0 (BC break)
  • DX is improved for people who don't know anything about the inner workings of the ArgumentResolver, it now works out of the box with the current default behavior.

@@ -12,6 +12,11 @@
namespace Symfony\Component\HttpKernel\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\DefaultValueResolver;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the term "value" really needed in namespace and classes ?

It could be:

..\ArgumentResover\DefaultArgumentResolver
..\ArgumentResover\RequestArgumentResolver
..\ArgumentResover\VariadicArgumentResolver
...

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would imply they are ArgumentResolver implementations, which they are not, hence I named them a ValueResolver. What I could do, is remove it from the namespace as they are tightly coupled to the ArgumentResolver.

Copy link
Member

Choose a reason for hiding this comment

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

Like Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver? Sounds good

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@linaori
Copy link
Contributor Author

linaori commented Apr 3, 2016

@fabpot I think it's ready to be merged, all tests are passing without issues and I think most edge-cases are covered now.

new RequestValueResolver(),
new DefaultValueResolver(),
new VariadicValueResolver(),
);
Copy link
Member

Choose a reason for hiding this comment

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

Elsewhere, we would also have an add() method to be able to add more value resolver. But this logic here forbids to have one. What about (and I know it's going to be controversial) removing the array typehint, make the default value to null and only automatically register the default revolsers when the value is null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current method of adding them is via a compiler pass, you tag it, gets collected and then injected into the constructor. I'm personally in favour of this method to avoid 4+ method calls each webrequest

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that I'm not in the middle of nowhere with an actual keyboard...

This compiler pass does all the logic: https://github.com/symfony/symfony/pull/18308/files#diff-6df4a64da7596af5827901ecbc5d2e78R18

  • If not given, same behavior as the LegacyArgumentResolver
  • Enhanced experience without the FrameworkBundle wiring everything via services
  • Easily extendable because you only need to tag your service with controller_argument.value_resolver

In your service.yml you would only have to do this:

services:
    app.argument_resolver.user:
        class: App\ArgumentResolver\UserValueResolver
        arguments:
            - "@security.token_storage"
        tags:
            - { name: controller_argument.value_resolver, priority: 150 }

The compiler pass will simply generate an array and replace the second argument to avoid the aforementioned method calls.

Copy link
Member

Choose a reason for hiding this comment

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

I totally understand how that works for Symfony full-stack, I was more thinking about the integration with Silex or Drupal where the FrameworkBundle is not available. Anyway, I'm going to merge like this and we still have 2 months to see how to deal with that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What could be done, is move the compiler pass to the component, but I'm not sure if silex uses this

Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK Silex does not use Config and DependencyInjection components by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once this is merged, let's see if we can find a nice solution for those cases because ideally I'd like to support that before 3.1 is released.

Copy link
Member

Choose a reason for hiding this comment

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

If we were to allow to add value resolvers after the argument resolver has been created, we should imo move the priority handling to this class instead of doing that in the compiler pass (i.e. adding a method like addArgumentValueResolver(ArgumentValueResolver $resolver, $priority)).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now we have the advantage of little overhead because everything is compiled, I'm personally for this approach because it feels safer

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should allow to add value resolver after the argument resolver has been created. 👍 for the current way

@fabpot
Copy link
Member

fabpot commented Apr 3, 2016

Apart from my comment about the default value resolvers, I'm 👍 to merge this PR.

* The `ControllerResolver::getArguments()` method has been deprecated and will
be removed in 4.0. If you have your own `ControllerResolverInterface`
implementation, you should inject either an `ArgumentResolverInterface`
instance or the new `ArgumentResolver` in the `HttpKernel`.
Copy link
Member

Choose a reason for hiding this comment

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

something similar (but then removed instead of deprecated) should be added to UPGRADE-4.0.md

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't it be easier to document this in the PR where it's actually removed?

@fabpot
Copy link
Member

fabpot commented Apr 3, 2016

Thank you @iltar.

@fabpot fabpot merged commit 1bf80c9 into symfony:master Apr 3, 2016
fabpot added a commit that referenced this pull request Apr 3, 2016
…iltar, HeahDude)

This PR was merged into the 3.1-dev branch.

Discussion
----------

Added an ArgumentResolver with clean extension point

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #17933 (pre-work), #1547, #10710
| License       | MIT
| Doc PR        | symfony/symfony-docs#6422

**This PR is a follow up for and blocked by: #18187**, relates to #11457 by @wouterj. When reviewing, please take the last commit: [Added an ArgumentResolver with clean extension point](4c092b3)

This PR provides:
- The ability to tag your own `ArgumentValueResolverInterface`. This means that you can effectively expand on the argument resolving in the `HttpKernel` without having to implement your own `ArgumentResolver`.

- The possibility to cache away argument metadata via a new `ArgumentMetadataFactory` which simply fetches the data from the cache, effectively omitting 1 reflection call per request. *Not implemented in this PR, but possible once this is merged.*

- The possibility to add a PSR-7 adapter to resolve the correct request, avoids the paramconverters
- The possibility to add a value resolver to fetch stuff from $request->query
- Drupal could simplify [their argument resolving](https://github.com/drupal/drupal/blob/8.1.x/core/lib/Drupal/Core/Controller/ControllerResolver.php) by a lot
- etc.

The aim for this PR is to provide a 100% BC variant to add argument resolving in a clean way, this is shown by the 2 tests: `LegacyArgumentResolverTest` and `ArgumentResolverTest`.

/cc @dawehner @larowlan if you have time, can you check the impact for Drupal? I think this should be a very simple change which should make it more maintainable.

Commits
-------

1bf80c9 Improved DX for the ArgumentResolver
f29bf4c Refactor ArgumentResolverTest
cee5106 cs fixes
cfcf764 Added an ArgumentResolver with clean extension point
360fc5f Extracting arg resolving from ControllerResolver
@HeahDude
Copy link
Contributor

HeahDude commented Apr 3, 2016

🎉 Thanks @iltar !

@linaori
Copy link
Contributor Author

linaori commented Apr 3, 2016

Couldn't have finished it this polished without you guys, thanks!

fabpot added a commit that referenced this pull request Apr 28, 2016
This PR was merged into the 3.1-dev branch.

Discussion
----------

Fixed a redundant check in DefaultValueResolver

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | ~
| License       | MIT
| Doc PR        | ~

In #18308 I have introduced a `DefaultValueResolver`. When writing documentation, I was planning on adding the code as an example and I noticed it did a check in the request attributes. A default value value should always be injected, whether the request has it or not. In case the request _does_ have the value, it would've already been added and thus never reach the default resolver.

Thus as this is never called in the default and configured flows and should not change the default value behavior, I'm removing this.

Commits
-------

e54c1a6 Fixed a redundant check in DefaultValueResolver
@fabpot fabpot mentioned this pull request May 13, 2016
wouterj added a commit to symfony/symfony-docs that referenced this pull request Jun 11, 2016
This PR was submitted for the master branch but it was merged into the 3.1 branch instead (closes #6438).

Discussion
----------

Added docs about ArgumentValueResolvers

| Q             | A
| ------------- | ---
| Doc fix?      | no
| New docs?     | yes
| Applies to    | 3.1
| Fixed tickets | ~

Adds the documentation for the new `ArgumentValueResolver` feature from symfony/symfony#18308.

Commits
-------

f22dc96 Added docs about ArgumentValueResolvers
wouterj added a commit to symfony/symfony-docs that referenced this pull request Jul 5, 2016
…olver (iltar)

This PR was submitted for the master branch but it was merged into the 3.1 branch instead (closes #6422).

Discussion
----------

Documented the ArgumentResolver along the ControllerResolver

| Q             | A
| ------------- | ---
| Doc fix?      | yes
| New docs?     | no ~ symfony/symfony#18308
| Applies to    | 3.1
| Fixed tickets | ~

The ArgumentResolver is used now instead of the ControllerResolver. I have yet to document the extension point but first I want to have this page mention it.

Commits
-------

11920e3 Documented the ArgumentResolver along the ControllerResolver
@linaori linaori deleted the feature/argument-resolver-extention-point branch February 8, 2019 13:38
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.