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

Skip to content

Commit 5e84861

Browse files
[FrameworkBundle] Add new "routing.controller" tag to inject autowired services into actions
1 parent 7ccc6aa commit 5e84861

File tree

7 files changed

+232
-0
lines changed

7 files changed

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

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
@@ -22,6 +22,7 @@
2222
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass;
2323
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass;
2424
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass;
25+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingControllerPass;
2526
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingResolverPass;
2627
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
2728
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
@@ -32,6 +32,11 @@
3232
<tag name="controller.argument_value_resolver" priority="50" />
3333
</service>
3434

35+
<service id="argument_resolver.service" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver" public="false">
36+
<tag name="controller.argument_value_resolver" priority="-50" />
37+
<argument /> <!-- service locator -->
38+
</service>
39+
3540
<service id="argument_resolver.default" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver" public="false">
3641
<tag name="controller.argument_value_resolver" priority="-100" />
3742
</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\DependencyInjection;
13+
14+
/**
15+
* Represents a typed service reference that can be autowired if the target is invalid.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*
19+
* @experimental in version 3.3
20+
*/
21+
class AutowirableReference extends Reference
22+
{
23+
private $type;
24+
private $autoRegister;
25+
26+
/**
27+
* @param string $id The service identifier
28+
* @param string $type The type of the identified service
29+
* @param int $invalidBehavior The behavior when the service does not exist
30+
* @param bool $autoRegister Whether new services should be created for discovered classes or not
31+
*
32+
* @see Container
33+
*/
34+
public function __construct($id, $type, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $autoRegister = true)
35+
{
36+
parent::__construct($id, $invalidBehavior);
37+
$this->type = $type;
38+
$this->autoRegister = $autoRegister;
39+
}
40+
41+
public function getType()
42+
{
43+
return $this->type;
44+
}
45+
46+
public function autoRegister()
47+
{
48+
return $this->autoRegister;
49+
}
50+
}

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

14+
use Symfony\Component\DependencyInjection\AutowirableReference;
1415
use Symfony\Component\DependencyInjection\Config\AutowireServiceResource;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\ContainerInterface;
1618
use Symfony\Component\DependencyInjection\Definition;
1719
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
1820
use Symfony\Component\DependencyInjection\LazyProxy\InheritanceProxyHelper;
@@ -100,6 +102,18 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass)
100102
*/
101103
protected function processValue($value, $isRoot = false)
102104
{
105+
if ($value instanceof AutowirableReference) {
106+
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior() || $this->container->has((string) $value)) {
107+
return $value;
108+
}
109+
110+
if ($ref = $this->getAutowiredReference($value->getType(), $value->autoRegister())) {
111+
return $ref;
112+
}
113+
114+
return $value;
115+
}
116+
103117
if (!$value instanceof Definition || !$value->getAutowiredCalls()) {
104118
return parent::processValue($value, $isRoot);
105119
}
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+
}

0 commit comments

Comments
 (0)
0