-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions #21771
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,48 @@ | ||
<?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\ArgumentResolver; | ||
|
||
use Psr\Container\ContainerInterface; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
|
||
/** | ||
* Yields a service keyed by _controller and argument name. | ||
* | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
final class ServiceValueResolver implements ArgumentValueResolverInterface | ||
{ | ||
private $container; | ||
|
||
public function __construct(ContainerInterface $container) | ||
{ | ||
$this->container = $container; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supports(Request $request, ArgumentMetadata $argument) | ||
{ | ||
return is_string($controller = $request->attributes->get('_controller')) && $this->container->has($controller) && $this->container->get($controller)->has($argument->getName()); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function resolve(Request $request, ArgumentMetadata $argument) | ||
{ | ||
yield $this->container->get($request->attributes->get('_controller'))->get($argument->getName()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
<?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\DependencyInjection; | ||
|
||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; | ||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\ContainerInterface; | ||
use Symfony\Component\DependencyInjection\Definition; | ||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; | ||
use Symfony\Component\DependencyInjection\LazyProxy\InheritanceProxyHelper; | ||
use Symfony\Component\DependencyInjection\Reference; | ||
use Symfony\Component\DependencyInjection\ServiceLocator; | ||
use Symfony\Component\DependencyInjection\TypedReference; | ||
|
||
/** | ||
* Creates the service-locators required by ServiceArgumentValueResolver. | ||
* | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface | ||
{ | ||
private $resolverServiceId; | ||
private $controllerTag; | ||
|
||
public function __construct($resolverServiceId = 'argument_resolver.service', $controllerTag = 'controller.service_arguments') | ||
{ | ||
$this->resolverServiceId = $resolverServiceId; | ||
$this->controllerTag = $controllerTag; | ||
} | ||
|
||
public function process(ContainerBuilder $container) | ||
{ | ||
if (false === $container->hasDefinition($this->resolverServiceId)) { | ||
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. Why not the other way around? If some tags are found then register the resolver. Because currently the resolver is asked to resolve before DefaultValueResolver even if there is no controller tagged. 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'd say because "less dynamic" is better. The extra check you're talking about is not a perf issue: it is really fast - and happens once per action (you don't call that many actions for constructing one response, do you?) 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.
Isn't it once per argument of an action, and sub-action(s)? Although I'm fine with it. |
||
return; | ||
} | ||
|
||
$parameterBag = $container->getParameterBag(); | ||
$controllers = array(); | ||
|
||
foreach ($container->findTaggedServiceIds($this->controllerTag) as $id => $tags) { | ||
$def = $container->getDefinition($id); | ||
|
||
if ($def->isAbstract()) { | ||
continue; | ||
} | ||
$class = $def->getClass(); | ||
$isAutowired = $def->isAutowired(); | ||
|
||
// resolve service class, taking parent definitions into account | ||
while (!$class && $def instanceof ChildDefinition) { | ||
$def = $container->findDefinition($def->getParent()); | ||
$class = $def->getClass(); | ||
} | ||
$class = $parameterBag->resolveValue($class); | ||
|
||
if (!$r = $container->getReflectionClass($class)) { | ||
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); | ||
} | ||
|
||
// get regular public methods | ||
$methods = array(); | ||
$arguments = array(); | ||
foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $r) { | ||
if (!$r->isConstructor() && !$r->isDestructor() && !$r->isAbstract()) { | ||
$methods[strtolower($r->name)] = array($r, $r->getParameters()); | ||
} | ||
} | ||
|
||
// validate and collect explicit per-actions and per-arguments service references | ||
foreach ($tags as $attributes) { | ||
if (!isset($attributes['action']) && !isset($attributes['argument']) && !isset($attributes['id'])) { | ||
continue; | ||
} | ||
foreach (array('action', 'argument', 'id') as $k) { | ||
if (!isset($attributes[$k][0])) { | ||
throw new InvalidArgumentException(sprintf('Missing "%s" attribute on tag "%s" %s for service "%s".', $k, $this->controllerTag, json_encode($attributes, JSON_UNESCAPED_UNICODE), $id)); | ||
} | ||
} | ||
if (!isset($methods[$action = strtolower($attributes['action'])])) { | ||
throw new InvalidArgumentException(sprintf('Invalid "action" attribute on tag "%s" for service "%s": no public "%s()" method found on class "%s".', $this->controllerTag, $id, $attributes['action'], $class)); | ||
} | ||
list($r, $parameters) = $methods[$action]; | ||
$found = false; | ||
|
||
foreach ($parameters as $p) { | ||
if ($attributes['argument'] === $p->name) { | ||
if (!isset($arguments[$r->name][$p->name])) { | ||
$arguments[$r->name][$p->name] = $attributes['id']; | ||
} | ||
$found = true; | ||
break; | ||
} | ||
} | ||
|
||
if (!$found) { | ||
throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": method "%s()" has no "%s" argument on class "%s".', $this->controllerTag, $id, $r->name, $attributes['argument'], $class)); | ||
} | ||
} | ||
|
||
foreach ($methods as list($r, $parameters)) { | ||
// create a per-method map of argument-names to service/type-references | ||
$args = array(); | ||
foreach ($parameters as $p) { | ||
$type = $target = InheritanceProxyHelper::getTypeHint($r, $p, true); | ||
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; | ||
|
||
if (isset($arguments[$r->name][$p->name])) { | ||
$target = $arguments[$r->name][$p->name]; | ||
if ('?' !== $target[0]) { | ||
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; | ||
} elseif ('' === $target = (string) substr($target, 1)) { | ||
throw new InvalidArgumentException(sprintf('A "%s" tag must have non-empty "id" attributes for service "%s".', $this->controllerTag, $id)); | ||
} elseif ($p->allowsNull() && !$p->isOptional()) { | ||
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; | ||
} | ||
} elseif (!$type) { | ||
continue; | ||
} | ||
|
||
$args[$p->name] = new ServiceClosureArgument($type ? new TypedReference($target, $type, $invalidBehavior, false) : new Reference($target, $invalidBehavior)); | ||
} | ||
// register the maps as a per-method service-locators | ||
if ($args) { | ||
$argsId = sprintf('arguments.%s:%s', $id, $r->name); | ||
$container->register($argsId, ServiceLocator::class) | ||
->addArgument($args) | ||
->setPublic(false) | ||
->setAutowired($isAutowired) | ||
->addTag('controller.arguments_locator', array($class, $id, $r->name)); | ||
$controllers[$id.':'.$r->name] = new ServiceClosureArgument(new Reference($argsId)); | ||
if ($id === $class) { | ||
$controllers[$id.'::'.$r->name] = new ServiceClosureArgument(new Reference($argsId)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
$container->getDefinition($this->resolverServiceId) | ||
->replaceArgument(0, (new Definition(ServiceLocator::class, array($controllers)))->addTag('container.service_locator')); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?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\DependencyInjection; | ||
|
||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
|
||
/** | ||
* Removes empty service-locators registered for ServiceArgumentValueResolver. | ||
* | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface | ||
{ | ||
private $resolverServiceId; | ||
|
||
public function __construct($resolverServiceId = 'argument_resolver.service') | ||
{ | ||
$this->resolverServiceId = $resolverServiceId; | ||
} | ||
|
||
public function process(ContainerBuilder $container) | ||
{ | ||
if (false === $container->hasDefinition($this->resolverServiceId)) { | ||
return; | ||
} | ||
|
||
$serviceResolver = $container->getDefinition($this->resolverServiceId); | ||
$controllers = $serviceResolver->getArgument(0)->getArgument(0); | ||
|
||
foreach ($container->findTaggedServiceIds('controller.arguments_locator') as $id => $tags) { | ||
$argumentLocator = $container->getDefinition($id)->clearTag('controller.arguments_locator'); | ||
list($class, $service, $action) = $tags[0]; | ||
|
||
if (!$argumentLocator->getArgument(0)) { | ||
// remove empty argument locators | ||
$reason = sprintf('Removing service-argument-resolver for controller "%s:%s": no corresponding definitions were found for the referenced services/types.%s', $service, $action, !$argumentLocator->isAutowired() ? ' Did you forget to enable autowiring?' : ''); | ||
} else { | ||
// any methods listed for call-at-instantiation cannot be actions | ||
$reason = false; | ||
foreach ($container->getDefinition($service)->getMethodCalls() as list($method, $args)) { | ||
if (0 === strcasecmp($action, $method)) { | ||
$reason = sprintf('Removing method "%s" of service "%s" from controller candidates: the method is called at instantiation, thus cannot be an action.', $action, $service); | ||
break; | ||
} | ||
} | ||
if (!$reason) { | ||
continue; | ||
} | ||
} | ||
|
||
$container->removeDefinition($id); | ||
unset($controllers[$service.':'.$action]); | ||
if ($service === $class) { | ||
unset($controllers[$service.'::'.$action]); | ||
} | ||
$container->log($this, $reason); | ||
} | ||
|
||
$serviceResolver->getArgument(0)->replaceArgument(0, $controllers); | ||
} | ||
} |
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 there any way to optimize the calls? AFAIU
$request->attributes->get('_controller')
,$this->container->has($controller)
,$this->container->get($controller)->has($argument->getName()
will be called for every arguments of the controller although we could keep a map of controllers by request that can be reused even inresolve()
, wdyt?Uh oh!
There was an error while loading. Please reload this page.
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 calls resolve to immediate isset checks, cannot be faster
Uh oh!
There was an error while loading. Please reload this page.
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 if I would use the other notation here?
Foo::bar
, with controller as a service, this would be supported.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.
@iltar it will work: both variants are now registered as references when id==class
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.
Ah yes, you're right! https://github.com/symfony/symfony/pull/21771/files#diff-4f6a92ae3706365662df9167009eaffaR205