8000 [FrameworkBundle] Add new "controller.service_arguments" tag to injec… · symfony/symfony@9c6e672 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9c6e672

Browse files
[FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions
1 parent 3023e4b commit 9c6e672

File tree

9 files changed

+591
-0
lines changed

9 files changed

+591
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
3.3.0
55
-----
66

7+
* Added support for the `controller.service_arguments` tag, for injecting services into controllers' actions
78
* Deprecated `cache:clear` with warmup (always call it with `--no-warmup`)
89
* Changed default configuration for
910
assets/forms/validation/translation/serialization/csrf from `canBeEnabled()` to

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class UnusedTagsPass implements CompilerPassInterface
2525
'console.command',
2626
'container.service_locator',
2727
'container.service_subscriber',
28+
'controller.service_arguments',
2829
'config_cache.resource_checker',
2930
'data_collector',
3031
'form.type',

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
use Symfony\Component\Config\DependencyInjection\ConfigCachePass;
3636
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
3737
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
38+
use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass;
39+
use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass;
3840
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
3941
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
4042
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
@@ -76,6 +78,8 @@ public function build(ContainerBuilder $container)
7678
{
7779
parent::build($container);
7880

81+
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
82+
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);
7983
$container->addCompilerPass(new RoutingResolverPass());
8084
$container->addCompilerPass(new ProfilerPass());
8185
// must be registered before removing private services as some might be listeners/subscribers

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
<tag name="controller.argument_value_resolver" priority="50" />
3737
</service>
3838

39+
<service id="argument_resolver.service" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver" public="false">
40+
<tag name="controller.argument_value_resolver" priority="-50" />
41+
<argument />
42+
</service>
43+
3944
<service id="argument_resolver.default" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver" public="false">
4045
<tag name="controller.argument_value_resolver" priority="-100" />
4146
</service>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
17+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
18+
19+
/**
20+
* Yields a service keyed by _controller and argument name.
21+
*
22+
* @author Nicolas Grekas <p@tchwork.com>
23+
*/
24+
final class ServiceValueResolver implements ArgumentValueResolverInterface
25+
{
26+
private $container;
27+
28+
public function __construct(ContainerInterface $container)
29+
{
30+
$this->container = $container;
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function supports(Request $request, ArgumentMetadata $argument)
37+
{
38+
return is_string($controller = $request->attributes->get('_controller')) && $this->container->has($controller) && $this->container->get($controller)->has($argument->getName());
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function resolve(Request $request, ArgumentMetadata $argument)
45+
{
46+
yield $this->container->get($request->attributes->get('_controller'))->get($argument->getName());
47+
}
48+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
18+
use Symfony\Component\DependencyInjection\Definition;
19+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
20+
use Symfony\Component\DependencyInjection\LazyProxy\InheritanceProxyHelper;
21+
use Symfony\Component\DependencyInjection\Reference;
22+
use Symfony\Component\DependencyInjection\ServiceLocator;
23+
use Symfony\Component\DependencyInjection\TypedReference;
24+
25+
/**
26+
* Creates the service-locators required by ServiceArgumentValueResolver.
27+
*
28+
* @author Nicolas Grekas <p@tchwork.com>
29+
*/
30+
class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
31+
{
32+
7802 private $resolverServiceId;
33+
private $controllerTag;
34+
35+
public function __construct($resolverServiceId = 'argument_resolver.service', $controllerTag = 'controller.service_arguments')
36+
{
37+
$this->resolverServiceId = $resolverServiceId;
38+
$this->controllerTag = $controllerTag;
39+
}
40+
41+
public function process(ContainerBuilder $container)
42+
{
43+
if (false === $container->hasDefinition($this->resolverServiceId)) {
44+
return;
45+
}
46+
47+
$parameterBag = $container->getParameterBag();
48+
$controllers = array();
49+
50+
foreach ($container->findTaggedServiceIds($this->controllerTag) as $id => $tags) {
51+
$def = $container->getDefinition($id);
52+
53+
if ($def->isAbstract()) {
54+
continue;
55+
}
56+
$class = $def->getClass();
57+
$isAutowired = $def->isAutowired();
58+
59+
// resolve service class, taking parent definitions into account
60+
while (!$class && $def instanceof ChildDefinition) {
61+
$def = $container->findDefinition($def->getParent());
62+
$class = $def->getClass();
63+
}
64+
$class = $parameterBag->resolveValue($class);
65+
66+
if (!$r = $container->getReflectionClass($class)) {
67+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
68+
}
69+
70+
// get regular public methods
71+
$methods = array();
72+
$arguments = array();
73+
foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $r) {
74+
if (!$r->isConstructor() && !$r->isDestructor() && !$r->isAbstract()) {
75+
$methods[strtolower($r->name)] = array($r, $r->getParameters());
76+
}
77+
}
78+
79+
// validate and collect explicit per-actions and per-arguments service references
80+
foreach ($tags as $attributes) {
81+
if (!isset($attributes['action']) && !isset($attributes['argument']) && !isset($attributes['id'])) {
82+
continue;
83+
}
84+
foreach (array('action', 'argument', 'id') as $k) {
85+
if (!isset($attributes[$k][0])) {
86+
throw new InvalidArgumentException(sprintf('Missing "%s" attribute on tag "%s" %s for service "%s".', $k, $this->controllerTag, json_encode($attributes, JSON_UNESCAPED_UNICODE), $id));
87+
}
88+
}
89+
if (!isset($methods[$action = strtolower($attributes['action'])])) {
90+
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));
91+
}
92+
list($r, $parameters) = $methods[$action];
93+
$found = false;
94+
95+
foreach ($parameters as $p) {
96+
if ($attributes['argument'] === $p->name) {
97+
if (!isset($arguments[$r->name][$p->name])) {
98+
$arguments[$r->name][$p->name] = $attributes['id'];
99+
}
100+
$found = true;
101+
break;
102+
}
103+
}
104+
105+
if (!$found) {
106+
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));
107+
}
108+
}
109+
110+
foreach ($methods as list($r, $parameters)) {
111+
// create a per-method map of argument-names to service/type-references
112+
$args = array();
113+
foreach ($parameters as $p) {
114+
$type = $target = InheritanceProxyHelper::getTypeHint($r, $p, true);
115+
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
116+
117+
if (isset($arguments[$r->name][$p->name])) {
118+
$target = $arguments[$r->name][$p->name];
119+
if ('?' !== $target[0]) {
120+
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
121+
} elseif ('' === $target = (string) substr($target, 1)) {
122+
throw new InvalidArgumentException(sprintf('A "%s" tag must have non-empty "id" attributes for service "%s".', $this->controllerTag, $id));
123+
} elseif ($p->allowsNull() && !$p->isOptional()) {
124+
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
125+
}
126+
} elseif (!$type) {
127+
continue;
128+
}
129+
130+
$args[$p->name] = new ServiceClosureArgument($type ? new TypedReference($target, $type, $invalidBehavior, false) : new Reference($target, $invalidBehavior));
131+
}
132+
// register the maps as a per-method service-locators
133+
if ($args) {
134+
$argsId = sprintf('arguments.%s:%s', $id, $r->name);
135+
$container->register($argsId, ServiceLocator::class)
136+
->addArgument($args)
137+
->setPublic(false)
138+
->setAutowired($isAutowired)
139+
->addTag('controller.arguments_locator', array($class, $id, $r->name));
140+
$controllers[$id.':'.$r->name] = new ServiceClosureArgument(new Reference($argsId));
141+
if ($id === $class) {
142+
$controllers[$id.'::'.$r->name] = new ServiceClosureArgument(new Reference($argsId));
143+
}
144+
}
145+
}
146+
}
147+
148+
$container->getDefinition($this->resolverServiceId)
149+
->replaceArgument(0, (new Definition(ServiceLocator::class, array($controllers)))->addTag('container.service_locator'));
150+
}
151+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* Removes empty service-locators registered for ServiceArgumentValueResolver.
19+
*
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*/
22+
class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface
23+
{
24+
private $resolverServiceId;
25+
26+
public function __construct($resolverServiceId = 'argument_resolver.service')
27+
{
28+
$this->resolverServiceId = $resolverServiceId;
29+
}
30+
31+
public function process(ContainerBuilder $container)
32+
{
33+
if (false === $container->hasDefinition($this->resolverServiceId)) {
34+
return;
35+
}
36+
37+
$serviceResolver = $container->getDefinition($this->resolverServiceId);
38+
$controllers = $serviceResolver->getArgument(0)->getArgument(0);
39+
40+
foreach ($container->findTaggedServiceIds('controller.arguments_locator') as $id => $tags) {
41+
$argumentLocator = $container->getDefinition($id)->clearTag('controller.arguments_locator');
42+
list($class, $service, $action) = $tags[0];
43+
44+
if (!$argumentLocator->getArgument(0)) {
45+
// remove empty argument locators
46+
$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?' : '');
47+
} else {
48+
// any methods listed for call-at-instantiation cannot be actions
49+
$reason = false;
50+
foreach ($container->getDefinition($service)->getMethodCalls() as list($method, $args)) {
51+
if (0 === strcasecmp($action, $method)) {
52+
$reason = sprintf('Removing method "%s" of service "%s" from controller candidates: the method is called at instantiation, thus cannot be an action.', $action, $service);
53+
break;
54+
}
55+
}
56+
if (!$reason) {
57+
continue;
58+
}
59+
}
60+
61+
$container->removeDefinition($id);
62+
unset($controllers[$service.':'.$action]);
63+
if ($service === $class) {
64+
unset($controllers[$service.'::'.$action]);
65+
}
66+
$container->log($this, $reason);
67+
}
68+
69+
$serviceResolver->getArgument(0)->replaceArgument(0, $controllers);
70+
}
71+
}

0 commit comments

Comments
 (0)
0