Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #21771 [FrameworkBundle] Add new "controller.service_argument…
…s" tag to inject services into actions (nicolas-grekas) This PR was merged into the 3.3-dev branch. Discussion ---------- [FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | (no test yet) | Fixed tickets | - | License | MIT | Doc PR | - Talking with @simensen and @weaverryan, we wondered if we could leverage the `ArgumentResolver` mechanism to make it inject services on demand, using e.g. autowiring. ```php class PostController { public function indexAction(Request $request, PostRepository $postRepository) { // PostRepository comes from the container $postRepository->findAll(); // ... } } ``` This PR achieves that, using a new "controller.service_arguments" tag. Typically: ```yaml services: AppBundle\Controller\PostController: autowire: true tags: - name: controller.service_arguments ``` It also supports with explicit wiring (thus doesn't necessarily require autowiring if you don't want to use it): ```yaml services: AppBundle\Controller\PostController: tags: - name: controller.service_arguments action: fooAction argument: logger id: my_logger ``` ~~The attached diff is bigger than strictly required for now, until #21770 is merged.~~ Todo: - [x] rebase on top of #21770 when merged - [x] add tests - [x] add cleaning pass to remove empty service locators Commits ------- 9c6e672 [FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions
- Loading branch information
Showing
9 changed files
with
591 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
...mfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) { | ||
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')); | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
...ny/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.