-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
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 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
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 <fabien@symfony.com> | ||
* | ||
* 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 <kjarli@gmail.com> | ||
*/ | ||
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,7 +17,28 @@ | |
<argument type="service" id="logger" on-invalid="ignore" /> | ||
</service> | ||
|
||
<service id="argument_resolver" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver" 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_metadata_factory" class="Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory" public="false" /> | ||
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 thought there was a kind of convention to write services used as arguments before being used as arguments. I was wrong ? 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. In this case I just put it under 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. Agreed. It was more a question about "what does the symfony style advise in that case ?" |
||
|
||
<service id="argument_value_resolver.argument_from_attribute" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\ArgumentFromAttributeResolver" public="false"> | ||
<tag name="controller_argument.value_resolver" priority="100" /> | ||
</service> | ||
|
||
<service id="argument_value_resolver.argument_is_request" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\RequestResolver" public="false"> | ||
<tag name="controller_argument.value_resolver" priority="50" /> | ||
</service> | ||
|
||
<service id="argument_value_resolver.default_argument_value" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\DefaultArgumentValueResolver" public="false"> | ||
<tag name="controller_argument.value_resolver" priority="-100" /> | ||
</service> | ||
|
||
<service id="argument_value_resolver.variadic_argument_from_attribute" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\VariadicArgumentValueResolver" 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" /> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,57 +12,64 @@ | |
namespace Symfony\Component\HttpKernel\Controller; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; | ||
|
||
/** | ||
* Responsible for the creation of the action arguments. | ||
* Responsible for the resolving of arguments passed to an action. | ||
* | ||
* @author Fabien Potencier <fabien@symfony.com> | ||
* @author Iltar van der Berg <kjarli@gmail.com> | ||
*/ | ||
class ArgumentResolver implements ArgumentResolverInterface | ||
final class ArgumentResolver implements ArgumentResolverInterface | ||
{ | ||
private $argumentMetadataFactory; | ||
|
||
/** | ||
* {@inheritdoc} | ||
* @var ArgumentValueResolverInterface[] | ||
*/ | ||
public function getArguments(Request $request, $controller) | ||
{ | ||
if (is_array($controller)) { | ||
$r = new \ReflectionMethod($controller[0], $controller[1]); | ||
} elseif (is_object($controller) && !$controller instanceof \Closure) { | ||
$r = new \ReflectionObject($controller); | ||
$r = $r->getMethod('__invoke'); | ||
} else { | ||
$r = new \ReflectionFunction($controller); | ||
} | ||
private $argumentValueResolvers; | ||
|
||
return $this->doGetArguments($request, $controller, $r->getParameters()); | ||
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, array $argumentValueResolvers = array()) | ||
{ | ||
$this->argumentMetadataFactory = $argumentMetadataFactory; | ||
$this->argumentValueResolvers = $argumentValueResolvers; | ||
} | ||
|
||
protected function doGetArguments(Request $request, $controller, array $parameters) | ||
/** | ||
* {@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. |
||
{ | ||
$attributes = $request->attributes->all(); | ||
$arguments = array(); | ||
foreach ($parameters as $param) { | ||
if (array_key_exists($param->name, $attributes)) { | ||
if (PHP_VERSION_ID >= 50600 && $param->isVariadic() && is_array($attributes[$param->name])) { | ||
$arguments = array_merge($arguments, array_values($attributes[$param->name])); | ||
} else { | ||
$arguments[] = $attributes[$param->name]; | ||
|
||
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; | ||
} | ||
} elseif ($param->getClass() && $param->getClass()->isInstance($request)) { | ||
$arguments[] = $request; | ||
} elseif ($param->isDefaultValueAvailable()) { | ||
$arguments[] = $param->getDefaultValue(); | ||
} else { | ||
if (is_array($controller)) { | ||
$repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]); | ||
} elseif (is_object($controller)) { | ||
$repr = get_class($controller); | ||
} else { | ||
$repr = $controller; | ||
|
||
$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; | ||
} | ||
|
||
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, $param->name)); | ||
// 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 <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\Controller\ArgumentValueResolver; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
|
||
/** | ||
* Grabs a non-variadic value from the request and returns it. | ||
* | ||
* @author Iltar van der Berg <kjarli@gmail.com> | ||
*/ | ||
final class ArgumentFromAttributeResolver implements ArgumentValueResolverInterface | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supports(Request $request, ArgumentMetadata $argument) | ||
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. So I guess doc blocks were removed here too ? 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 had inherit doc tags before so it was rather useless. If I remember correctly, the "way to go" is to omit the docblock if it's the same as the parent/interface 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 AFAIK, we use inheritdoc tags in Symfony, to make it clear that the phpdoc is inherited and so does not need to be written by someone looking for missing phpdoc (unless I missed a policy change). 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. 👍 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. 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. Rules are the following:
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. They are needed here though. |
||
{ | ||
return !$argument->isVariadic() && $request->attributes->has($argument->getName()); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function resolve(Request $request, ArgumentMetadata $argument) | ||
{ | ||
yield $request->attributes->get($argument->getName()); | ||
} | ||
} |
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 <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\Controller\ArgumentValueResolver; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
|
||
/** | ||
* Returns the default value defined in the action signature if present and no value has been given. | ||
* | ||
* @author Iltar van der Berg <kjarli@gmail.com> | ||
*/ | ||
final class DefaultArgumentValueResolver 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 <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\Controller\ArgumentValueResolver; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
|
||
/** | ||
* Supports the same instance as the request object passed along. | ||
* | ||
* @author Iltar van der Berg <kjarli@gmail.com> | ||
*/ | ||
final class RequestResolver implements ArgumentValueResolverInterface | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supports(Request $request, ArgumentMetadata $argument) | ||
{ | ||
return $argument->getType() === Request::class || is_subclass_of($request, $argument->getType()); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function resolve(Request $request, ArgumentMetadata $argument) | ||
{ | ||
yield $request; | ||
} | ||
} |
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.
missing empty line
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.
Good catch