10000 [FrameworkBundle] Add new "routing.controller" tag to inject autowire… · symfony/symfony@d33e043 · GitHub
[go: up one dir, main page]

Skip to content

Commit d33e043

Browse files
[FrameworkBundle] Add new "routing.controller" tag to inject autowired services into actions
1 parent e7c12d3 commit d33e043

File tree

7 files changed

+425
-0
lines changed

7 files changed

+425
-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 "routing.controller" tag, for injecting services into controllers' actions
78
* Changed default configuration for
89
assets/forms/validation/translation/serialization/csrf from `canBeEnabled()` to
910
`canBeDisabled()` when Flex is used

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class UnusedTagsPass implements CompilerPassInterface
3434
'kernel.event_subscriber',
3535
'kernel.fragment_renderer',
3636
'monolog.logger',
37+
'routing.controller',
3738
'routing.expression_language_provider',
3839
'routing.loader',
3940
'security.expression_language_provider',

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
3737
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
3838
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
39+
use Symfony\Component\Routing\DependencyInjection\RoutingControllerPass;
3940
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
4041
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
4142
use Symfony\Component\Debug\ErrorHandler;
@@ -76,6 +77,7 @@ public function build(ContainerBuilder $container)
7677
{
7778
parent::build($container);
7879

80+
$container->addCompilerPass(new RoutingControllerPass());
7981
$container->addCompilerPass(new RoutingResolverPass());
8082
$container->addCompilerPass(new ProfilerPass());
8183
// 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>
38< B41A /code>38

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 /> <!-- service locator -->
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
* @experimental in version 3.3
25+
*/
26+
final class ServiceValueResolver implements ArgumentValueResolverInterface
27+
{
28+
private $container;
29+
30+
public function __construct(ContainerInterface $container)
31+
{
32+
$this->container = $container;
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
public function supports(Request $request, ArgumentMetadata $argument)
39+
{
40+
return is_string($controller = $request->attributes->get('_controller')) && $this->container->has($controller) && $this->container->get($controller)->has($argument->getName());
41+
}
42+
43+
/**
44+
* {@inheritdoc}
45+
*/
46+
public function resolve(Request $request, ArgumentMetadata $argument)
47+
{
48+
yield $this->container->get($request->attributes->get('_controller'))->get($argument->getName());
49+
}
50+
}
Lines changed: 163 additions & 0 deletions
2851
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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\Routing\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
18+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
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+
* @experimental in version 3.3
31+
*/
32+
class RoutingControllerPass implements CompilerPassInterface
33+
{
34+
private $resolverServiceId;
35+
private $controllerTag;
36+
37+
public function __construct($resolverServiceId = 'argument_resolver.service', $controllerTag = 'routing.controller')
38+
{
39+
$this->resolverServiceId = $resolverServiceId;
40+
$this->controllerTag = $controllerTag;
41+
}
42+
43+
public function process(ContainerBuilder $container)
44+
{
45+
if (false === $container->hasDefinition($this->resolverServiceId)) {
46+
return;
47+
}
48+
49+
$serviceResolver = $container->getDefinition($this->resolverServiceId);
50+
$parameterBag = $container->getParameterBag();
51+
$controllers = array();
52+
53+
foreach ($container->findTaggedServiceIds($this->controllerTag) as $id => $tags) {
54+
$def = $container->getDefinition($id);
55+
$class = $def->getClass();
56+
$isAutowired = $def->isAutowired();
57+
58+
if ($def->isAbstract()) {
59+
continue;
60+
}
61+
62+
while (!$class && $def instanceof ChildDefinition) {
63+
$def = $container->findDefinition($def->getParent());
64+
$class = $def->getClass();
65+
}
66+
$class = $parameterBag->resolveValue($class);
67+
68+
if (!$r = $container->getReflectionClass($class)) {
69+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
70+
}
71+
72+
$methods = array();
73+
foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $r) {
74+
$methods[strtolower($r->name)] = $r;
75+
}
76+
77+
$actions = array();
78+
$arguments = array();
79+
foreach ($tags as $attributes) {
80+
if (!isset($attributes['action'])) {
81+
throw new InvalidArgumentException(sprintf('Service "%s" must define the "action" attribute on "routing.controller" tags.', $id));
82+
}
83+
ksort($attributes);
84+
if (1 < count($attributes) && array('action', 'argument', 'service') !== $r = array_keys(array_filter($attributes))) {
85+
throw new InvalidArgumentException(sprintf('A "routing.controller" tag must have either one "action" or exactly three non-empty "action", "argument" and "service" attributes, "%s" given for service "%s".', implode('", "', $r), $id));
86+
}
87+
$action = strtolower($attributes['action']);
88+
89+
if (false !== strpos($action, '*')) {
90+
$regex = '/^'.str_replace('\*', '.*', preg_quote($action, '/')).'$/';
91+
$found = false;
92+
93+
foreach ($methods as $name => $r) {
94+
if (preg_match($regex, $name)) {
95+
$actions[$name = $methods[$name]->name] = $r;
96+
$found = true;
97+
98+
if (isset($attributes['argument']) && !isset($arguments[$name][$attributes['argument']])) {
99+
$arguments[$name][$attributes['argument']] = $attributes['service'];
100+
}
101+
}
102+
}
103+
104+
if (!$found) {
105+
$container->log($this, sprintf('No "action" found for service "%s": class "%s" has no public "%s()" methods.', $id, $class, $attributes['action']));
106+
}
107+
} elseif (isset($methods[$action])) {
108+
$actions[$name = $methods[$action]->name] = $methods[$action];
109+
110+
if (isset($attributes['argument']) && !isset($arguments[$name][$attributes['argument']])) {
111+
$arguments[$name][$attributes['argument']] = $attributes['service'];
112+
$found = false;
113+
114+
foreach ($methods[$action]->getParameters() as $r) {
115+
if ($attributes['argument'] === $r->name) {
116+
$found = true;
117+
break;
118+
}
119+
}
120+
121+
if (!$found) {
122+
throw new InvalidArgumentException(sprintf('Invalid "routing.controller" tag for service "%s": method "%s()" has no "%s" argument on class "%s".', $id, $name, $attributes['argument'], $class));
123+
}
124+
}
125+
} else {
126+
throw new InvalidArgumentException(sprintf('Invalid "action" for service "%s": no public "%s()" method found on class "%s".', $id, $attributes['action'], $class));
127+
}
128+
}
129+
130+
if (!$actions) {
131+
continue;
132+
}
133+
134+
foreach ($actions as $name => $r) {
135+
$args = array();
136+
foreach ($r->getParameters() as $p) {
137+
$type = $target = InheritanceProxyHelper::getTypeHint($r, $p, true);
138+
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
139+
140+
if (isset($arguments[$name][$p->name])) {
141+
$target = $arguments[$name][$p->name];
142+
if ('?' !== $target[0]) {
143+
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
144+
} elseif ('' === $target = (string) substr($target, 1)) {
145+
throw new InvalidArgumentException(sprintf('A "routing.controller" tag must have non-empty "service" attributes for service "%s".', $id));
146+
}
147+
} elseif (!$type) {
148+
continue;
149+
}
150+
151+
$args[$p->name] = new ServiceClosureArgument($type ? new TypedReference($target, $type, $invalidBehavior, false) : new Reference($target, $invalidBehavior));
152+
}
153+
if ($args) {
154+
$argsId = sprintf('arguments.%s:%s', $id, $name);
155+
$container->register($argsId, ServiceLocator::class)->addArgument($args)->setPublic(false)->setAutowired($isAutowired);
156+
$controllers[$id.':'.$name] = new Reference($argsId);
157+
}
158+
}
159+
}
160+
161+
$serviceResolver->replaceArgument(0, new ServiceLocatorArgument($controllers));
162+
}
163+
}

0 commit comments

Comments
 (0)
0