-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
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
Added an ArgumentResolver with clean extension point #18308
Conversation
public function process(ContainerBuilder $container) | ||
{ | ||
$definition = $container->getDefinition('argument_resolver'); | ||
$argument_resolvers = $this->findAndSortTaggedServices('controller_argument.value_resolver', $container); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
What benefits of |
*/ | ||
public function getValue(Request $request, ArgumentMetadataInterface $argument) | ||
{ | ||
return $request->attributes->all()[$argument->getArgumentName()]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get()
instead of all()[]
?
Also #11457 wasn't merged because of lack of performance tests. Maybe you should add blackfire comparation before and after. |
@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 |
@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'); |
There was a problem hiding this comment.
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.
https://blackfire.io/docs/up-and-running/installation Create simple app based on symfony-standard, create profile, replace symfony's version with your patched, create profile again and compare them. |
@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()); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
@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" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo argument_metadata_factory
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? |
@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:
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. |
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. |
You've got a fair point there. In any case, I could rename the |
@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. |
@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 /**
* {@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 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 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) |
@iltar having separated commits will certainly ease reviews, but once done you should squash mines, they are not relevant. Thank you ;) |
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). |
@fabpot I'm having doubts about the resolvers:
I would like to propose the following naming before this is merged:
While writing the docs I noticed it's still fairly inconsistent and a lot to write. |
@iltar don't you think the
|
the new names are indeed much more consistent. I also like @javiereguiluz shorter proposal. Up to you between the 2 new proposals. |
I like that, less is more in this case. I was running into another DX issue; when showing the |
Okay, small update:
|
@@ -12,6 +12,11 @@ | |||
namespace Symfony\Component\HttpKernel\Controller; | |||
|
|||
use Symfony\Component\HttpFoundation\Request; | |||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\DefaultValueResolver; |
There was a problem hiding this comment.
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
...
?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@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(), | ||
); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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`. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
Thank you @iltar. |
…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
🎉 Thanks @iltar ! |
Couldn't have finished it this polished without you guys, thanks! |
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
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
…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
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:
ArgumentValueResolverInterface
. This means that you can effectively expand on the argument resolving in theHttpKernel
without having to implement your ownArgumentResolver
.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 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
andArgumentResolverTest
./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.