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

Skip to content

Commit d1889dd

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

File tree

6 files changed

+175
-0
lines changed

6 files changed

+175
-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 (autowired) 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
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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\Bundle\FrameworkBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
16+
use Symfony\Component\DependencyInjection\AutowirableReference;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\ContainerInterface;
19+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
20+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
21+
use Symfony\Component\DependencyInjection\LazyProxy\InheritanceProxyHelper;
22+
use Symfony\Component\DependencyInjection\Reference;
23+
use Symfony\Component\DependencyInjection\ServiceLocator;
24+
25+
/**
26+
* Creates the controller arguments service locator required by "argument_resolver.service".
27+
*
28+
* @author Nicolas Grekas <p@tchwork.com>
29+
*
30+
* @experimental in version 3.3
31+
*/
32+
class RoutingControllerPass implements CompilerPassInterface
33+
{
34+
public function process(ContainerBuilder $container)
35+
{
36+
if (false === $container->hasDefinition('argument_resolver.service')) {
37+
return;
38+
}
39+
40+
$serviceResolver = $container->getDefinition('argument_resolver.service');
41+
$parameterBag = $container->getParameterBag();
42+
$controllers = array();
43+
44+
foreach ($container->findTaggedServiceIds('routing.controller') as $id => $tags) {
45+
$def = $container->getDefinition($id);
46+
$class = $def->getClass();
47+
48+
if ($def->isAbstract()) {
49+
continue;
50+
}
51+
52+
while (!$class && $def instanceof ChildDefinition) {
53+
$def = $container->findDefinition($def->getParent());
54+
$class = $def->getClass();
55+
}
56+
$class = $parameterBag->resolveValue($class);
57+
58+
if (!$r = $container->getReflectionClass($class)) {
59+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
60+
}
61+
62+
$methods = array();
63+
foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $r) {
64+
$methods[strtolower($r->name)] = $r;
65+
}
66+
67+
$actions = array();
68+
foreach ($tags as $attributes) {
69+
if (!isset($attributes['action'])) {
70+
throw new InvalidArgumentException(sprintf('Service "%s" must define the "action" attribute on "routing.controller" tags.', $id));
71+
}
72+
$action = strtolower($attributes['action']);
73+
74+
if (false !== strpos($action, '*')) {
75+
$regex = '/^'.str_replace('\*', '.*', preg_quote($action, '/')).'$/';
76+
$found = false;
77+
78+
foreach ($methods as $name => $r) {
79+
if (preg_match($regex, $name)) {
80+
$actions[$methods[$action]->name] = $methods[$action];
81+
$found = true;
82+
}
83+
}
84+
85+
if (!$found) {
86+
$container->log($this, sprintf('No "action" found for service "%s": class "%s" has no "%s" methods.', $id, $class, $attributes['action']));
87+
}
88+
} elseif (isset($methods[$action])) {
89+
$actions[$methods[$action]->name] = $methods[$action];
90+
} else {
91+
throw new InvalidArgumentException(sprintf('Invalid "action" for service "%s": no "%s" method found on class "%s".', $id, $class, $attributes['action']));
92+
}
93+
}
94+
95+
if (!$actions) {
96+
continue;
97+
}
98+
99+
foreach ($actions as $name => $r) {
100+
$args = array();
101+
foreach ($r->getParameters() as $p) {
102+
if ($type = InheritanceProxyHelper::getTypeHint($r, $p, true)) {
103+
$args[$p->name] = new ServiceClosureArgument(new AutowirableReference($type, $type, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, false));
104+
}
105+
}
106+
if ($args) {
107+
$argsId = sprintf('arguments.%s:%s', $id, $name);
108+
$container->register($argsId, ServiceLocator::class)->addArgument($args)->setPublic(false);
109+
$controllers[$id.':'.$name] = new Reference($argsId);
110+
}
111+
}
112+
}
113+
114+
$serviceResolver->replaceArgument(0, new ServiceLocatorArgument($controllers));
115+
}
116+
}

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
@@ -21,6 +21,7 @@
2121
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ControllerArgumentValueResolverPass;
2222
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass;
2323
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass;
24+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingControllerPass;
2425
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingResolverPass;
2526
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
2627
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass;
@@ -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>
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 /> <!-- 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+
}

0 commit comments

Comments
 (0)
0