-
-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Reference; | ||
|
||
/** | ||
* Gathers and configures the argument value resolvers. | ||
* | ||
* @author Iltar van der Berg <[email protected]> | ||
*/ | ||
class ControllerArgumentValueResolverPass implements CompilerPassInterface | ||
{ | ||
public function process(ContainerBuilder $container) | ||
{ | ||
if (!$container->hasDefinition('argument_resolver')) { | ||
return; | ||
} | ||
|
||
$definition = $container->getDefinition('argument_resolver'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. first, check if the definition exists and return early otherwise. |
||
$argumentResolvers = $this->findAndSortTaggedServices('controller_argument.value_resolver', $container); | ||
$definition->replaceArgument(1, $argumentResolvers); | ||
} | ||
|
||
/** | ||
* Finds all services with the given tag name and order them by their priority. | ||
* | ||
* @param string $tagName | ||
* @param ContainerBuilder $container | ||
* | ||
* @return array | ||
*/ | ||
private function findAndSortTaggedServices($tagName, ContainerBuilder $container) | ||
{ | ||
$services = $container->findTaggedServiceIds($tagName); | ||
|
||
$sortedServices = array(); | ||
foreach ($services as $serviceId => $tags) { | ||
foreach ($tags as $attributes) { | ||
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0; | ||
$sortedServices[$priority][] = new Reference($serviceId); | ||
} | ||
} | ||
|
||
if (empty($sortedServices)) { | ||
return array(); | ||
} | ||
|
||
krsort($sortedServices); | ||
|
||
// Flatten the array | ||
return call_user_func_array('array_merge', $sortedServices); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing empty line at the end of this file There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't github show this if the case? Anyhow, line 66 is an empty line here for me, adding one more would result in 2 empty lines in the end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @HeahDude we should not have an empty line at the end. We should have a final LF char. PhpStorm displays an empty line after this LF, but there is no actual line 66. Github handles this properly (as git does) and displays an extra info at the end of line when it does not end with an EOL character There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stof Noted. Thanks for that precision :) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,29 @@ | |
<argument type="service" id="logger" on-invalid="ignore" /> | ||
</service> | ||
|
||
<service id="argument_metadata_factory" class="Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory" public="false" /> | ||
|
||
<service id="argument_resolver" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver" public="false"> | ||
<argument type="service" id="argument_metadata_factory" /> | ||
<argument type="collection" /> | ||
</service> | ||
|
||
<service id="argument_resolver.request_attribute" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver" public="false"> | ||
<tag name="controller_argument.value_resolver" priority="100" /> | ||
</service> | ||
|
||
<service id="argument_resolver.request" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver" public="false"> | ||
<tag name="controller_argument.value_resolver" priority="50" /> | ||
</service> | ||
|
||
<service id="argument_resolver.default" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver" public="false"> | ||
<tag name="controller_argument.value_resolver" priority="-100" /> | ||
</service> | ||
|
||
<service id="argument_resolver.variadic" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver" public="false"> | ||
<tag name="controller_argument.value_resolver" priority="-150" /> | ||
</service> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about merging the last two as being the "fallback" behavior based on reflection? That could even be "hardcoded" (not sure). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean directly in the |
||
|
||
<service id="response_listener" class="Symfony\Component\HttpKernel\EventListener\ResponseListener"> | ||
<tag name="kernel.event_subscriber" /> | ||
<argument>%kernel.charset%</argument> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* 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\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; | ||
|
||
/** | ||
* Responsible for resolving the arguments passed to an action. | ||
* | ||
* @author Iltar van der Berg <[email protected]> | ||
*/ | ||
final class ArgumentResolver implements ArgumentResolverInterface | ||
{ | ||
private $argumentMetadataFactory; | ||
|
||
/** | ||
* @var ArgumentValueResolverInterface[] | ||
*/ | ||
private $argumentValueResolvers; | ||
|
||
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, array $argumentValueResolvers = array()) | ||
{ | ||
$this->argumentMetadataFactory = $argumentMetadataFactory ?: new ArgumentMetadataFactory(); | ||
$this->argumentValueResolvers = $argumentValueResolvers ?: array( | ||
new RequestAttributeValueResolver(), | ||
new RequestValueResolver(), | ||
new DefaultValueResolver(), | ||
new VariadicValueResolver(), | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Elsewhere, we would also have an There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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
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 commentThe 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 commentThe 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 commentThe 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 commentThe 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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 |
||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getArguments(Request $request, $controller) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing doc block ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was removed on purpose, I can always add it instead of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, needed. |
||
{ | ||
$arguments = array(); | ||
|
||
foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) { | ||
foreach ($this->argumentValueResolvers as $resolver) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edited foreach ($this->argumentValueResolvers as $resolver) {
if (!$resolver->supports($request, $metadata)) {
continue;
}
$resolved = $resolver->getValue($request, $metadata);
if ($resolved) {
// variadic is a special case, always being the last and being an array
if ($metadata->isVariadic() && is_array($resolved)) {
return array_merge($arguments, $resolved);
}
return $resolved;
}
}
$repr = $controller;
if (is_array($controller)) {
$repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]);
} elseif (is_object($controller)) {
$repr = get_class($controller);
}
throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $metadata->getArgumentName())); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would cause an issue with I've also thought about the guard clause you've posted but this case will actually cause 2 arguments to be resolved if they overlap. That means if
this would mean you'd call the method with 2 parameters instead of 1. |
||
if (!$resolver->supports($request, $metadata)) { | ||
continue; | ||
} | ||
|
||
$resolved = $resolver->resolve($request, $metadata); | ||
|
||
if (!$resolved instanceof \Generator) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, we should be more flexible here, by allowing any Traversable (as we only need to loop over it using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes it more complex. If it returns an array, I should iterate over it. If it's not an array, I should make it an array and iterate over it. This last case was discussed to rather throw an exception. If you check the comments above, it was decide to use yield as an elegant alternative. This way someone can yield an array and it won't give a buggy result. If I were to return an array while that single argument would have to be an array, it would start adding each array item as one, effectively breaking This is one of the cases where I think it's easier to support only 1 method which is compatible with both arrays, non-arrays and variadic arguments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iltar Why ? Document that the return value is a Traversable being a list of argument, and always iterate over it. The code is the same than today, except for the The only case where a consumer should care that it deals with a Generator is when using Generator-only APIs (i.e. I'm not asking you to be able to return a single value from the resolver here (which is what you describe in your comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I've discussed this before; @fabpot do you also agree that returning In my opinion it's a bit dangerous as this should only be possible for variadics. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iltar returning an array in this case would then work the same than when returning an iterator. Iterating over it. I don't see much difference between the case of the array and the Generator here, for the consumer side (and the resolver can be implement in the way the implementer prefers) What we should have though is a validation in the ArgumentResolver that any non-variadic argument leads to an iterator containing exactly 1 value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stof as the current solution is more strict, we can always decide to change it in a later version as it will remain backwards compatible. I don't think the current situation will cause any issues and if they do, we can patch it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iltar If we want to make that change, it must be done before the 3.1 release. Otherwise, code relying on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @xabbuh but we're talking about the return value that is only internally used. No other code should ever rely on what the argument value resolver returns There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @wouterj If we don't expect other code to make use of the return values, we should remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @xabbuh in theory it shouldn't return anything else and a clear exception In my opinion having just yield is a good idea because it limits the usage On Sun, Apr 10, 2016 at 1:41 PM Christian Flothmann <
|
||
throw new \InvalidArgumentException(sprintf('%s::resolve() must yield at least one value.', get_class($resolver))); | ||
} | ||
|
||
foreach ($resolved as $append) { | ||
$arguments[] = $append; | ||
} | ||
|
||
// continue to the next controller argument | ||
continue 2; | ||
} | ||
|
||
$representative = $controller; | ||
|
||
if (is_array($representative)) { | ||
$representative = sprintf('%s::%s()', get_class($representative[0]), $representative[1]); | ||
} elseif (is_object($representative)) { | ||
$representative = get_class($representative); | ||
} | ||
|
||
throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $representative, $metadata->getName())); | ||
} | ||
|
||
return $arguments; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* 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\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
|
||
/** | ||
* Yields the default value defined in the action signature when no value has been given. | ||
* | ||
* @author Iltar van der Berg <[email protected]> | ||
*/ | ||
final class DefaultValueResolver implements ArgumentValueResolverInterface | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supports(Request $request, ArgumentMetadata $argument) | ||
{ | ||
return $argument->hasDefaultValue() && !$request->attributes->has($argument->getName()); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function resolve(Request $request, ArgumentMetadata $argument) | ||
{ | ||
yield $argument->getDefaultValue(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* 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\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
|
||
/** | ||
* Yields a non-variadic argument's value from the request attributes. | ||
* | ||
* @author Iltar van der Berg <[email protected]> | ||
*/ | ||
final class RequestAttributeValueResolver implements ArgumentValueResolverInterface | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supports(Request $request, ArgumentMetadata $argument) | ||
{ | ||
return !$argument->isVariadic() && $request->attributes->has($argument->getName()); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function resolve(Request $request, ArgumentMetadata $argument) | ||
{ | ||
yield $request->attributes->get($argument->getName()); | ||
} | ||
} |
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?