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

Skip to content

Commit 2b6e005

Browse files
[FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions
1 parent 64f9f7b commit 2b6e005

File tree

9 files changed

+593
-0
lines changed

9 files changed

+593
-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
* 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
@@ -23,6 +23,7 @@ class UnusedTagsPass implements CompilerPassInterface
2323
{
2424
private $whitelist = array(
2525
'console.command',
26+
'controller.service_arguments',
2627
'config_cache.resource_checker',
2728
'data_collector',
2829
'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 type="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: 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\Exception\InvalidArgumentException;
19+
use Symfony\Component\DependencyInjection\LazyProxy\InheritanceProxyHelper;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
use Symfony\Component\DependencyInjection\ServiceLocator;
22+
use Symfony\Component\DependencyInjection\TypedReference;
23+
24+
/**
25+
* Creates the service-locators required by ServiceArgumentValueResolver.
26+
*
27+
* @author Nicolas Grekas <p@tchwork.com>
28+
*/
29+
class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
30+
{
31+
private $resolverServiceId;
32+
private $controllerTag;
33+
34+
public function __construct($resolverServiceId = 'argument_resolver.service', $controllerTag = 'controller.service_arguments')
35+
{
36+
$this->resolverServiceId = $resolverServiceId;
37+
$this->controllerTag = $controllerTag;
38+
}
39+
40+
public function process(ContainerBuilder $container)
41+
{
42+
if (false === $container->hasDefinition($this->resolverServiceId)) {
43+
return;
44+
}
45+
46+
$parameterBag = $container->getParameterBag();
47+
$controllers = array();
48+
49+
foreach ($container->findTaggedServiceIds($this->controllerTag) as $id => $tags) {
50+
$def = $container->getDefinition($id);
51+
52+
if ($def->isAbstract()) {
53+
continue;
54+
}
55+
$class = $def->getClass();
56+
$isAutowired = $def->isAutowired();
57+
58+
// resolve service class, taking parent definitions into account
59+
while (!$class && $def instanceof ChildDefinition) {
60+
$def = $container->findDefinition($def->getParent());
61+
$class = $def->getClass();
62+
}
63+
$class = $parameterBag->resolveValue($class);
64+
65+
if (!$r = $container->getReflectionClass($class)) {
66+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
67+
}
68+
69+
// get regular public methods
70+
$methods = array();
71+
$arguments = array();
72+
foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $r) {
73+
if (!$r->isConstructor() && !$r->isDestructor() && !$r->isAbstract()) {
74+
$methods[strtolower($r->name)] = array($r, $r->getParameters());
75+
}
76+
}
77+
78+
// validate and collect explicit per-actions and per-arguments service references
79+
foreach ($tags as $attributes) {
80+
if (!isset($attributes['action']) && !isset($attributes['argument']) && !isset($attributes['service'])) {
81+
continue;
82+
}
83+
foreach (array('action', 'argument', 'service') as $k) {
84+
if (!isset($attributes[$k][0])) {
85+
throw new InvalidArgumentException(sprintf('Missing "%s" attribute on tag "%s" %s for service "%s".', $k, $this->controllerTag, json_encode($attributes, JSON_UNESCAPED_UNICODE), $id));
86+
}
87+
}
88+
if (!isset($methods[$action = strtolower($attributes['action'])])) {
89+
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));
90+
}
91+
list($r, $parameters) = $methods[$action];
92+
$found = false;
93+
94+
foreach ($parameters as $p) {
95+
if ($attributes['argument'] === $p->name) {
96+
if (!isset($arguments[$r->name][$p->name])) {
97+
$arguments[$r->name][$p->name] = $attributes['service'];
98+
}
99+
$found = true;
100+
break;
101+
}
102+
}
103+
104+
if (!$found) {
105+
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));
106+
}
107+
}
108+
109+
foreach ($methods as list($r, $parameters)) {
110+
// create a per-method map of argument-names to service/type-references
111+
$args = array();
112+
foreach ($parameters as $p) {
113+
$type = $target = InheritanceProxyHelper::getTypeHint($r, $p, true);
114+
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
115+
116+
if (isset($arguments[$r->name][$p->name])) {
117+
$target = $arguments[$r->name][$p->name];
118+
if ('?' !== $target[0]) {
119+
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
120+
} elseif ('' === $target = (string) substr($target, 1)) {
121+
throw new InvalidArgumentException(sprintf('A "%s" tag must have non-empty "service" attributes for service "%s".', $this->controllerTag, $id));
122+
} elseif ($p->allowsNull() && !$p->isOptional()) {
123+
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
124+
}
125+
} elseif (!$type) {
126+
continue;
127+
}
128+
129+
$args[$p->name] = new ServiceClosureArgument($type ? new TypedReference($target, $type, $invalidBehavior, false) : new Reference($target, $invalidBehavior));
130+
}
131+
// register the maps as a per-method service-locators
132+
if ($args) {
133+
$argsId = sprintf('arguments.%s:%s', $id, $r->name);
134+
$container->register($argsId, ServiceLocator::class)
135+
->addArgument($args)
136+
->setPublic(false)
137+
->setAutowired($isAutowired)
138+
->addTag('controller.arguments_locator', array($class, $id, $r->name));
139+
$controllers[$id.':'.$r->name] = new Reference($argsId);
140+
if ($id === $class) {
141+
$controllers[$id.'::'.$r->name] = new Reference($argsId);
142+
}
143+
}
144+
}
145+
}
146+
147+
$container->getDefinition($this->resolverServiceId)
148+
->getArgument(0)
149+
->setValues($controllers);
150+
}
151+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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)->getValues();
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+
if ($controllers) {
70+
$serviceResolver->getArgument(0)->setValues($controllers);
71+
} else {
72+
$container->removeDefinition($this->resolverServiceId);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)
0