Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d94a2ab

Browse filesBrowse files
[FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions
1 parent 64f9f7b commit d94a2ab
Copy full SHA for d94a2ab

File tree

9 files changed

+571
-0
lines changed
Filter options

9 files changed

+571
-0
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
+4Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
+5Lines 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>
+48Lines 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+
}
+150Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
}
101+
}
102+
103+
if (!$found) {
104+
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));
105+
}
106+
}
107+
108+
foreach ($methods as list($r, $parameters)) {
109+
// create a per-method map of argument-names to service/type-references
110+
$args = array();
111+
foreach ($parameters as $p) {
112+
$type = $target = InheritanceProxyHelper::getTypeHint($r, $p, true);
113+
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
114+
115+
if (isset($arguments[$r->name][$p->name])) {
116+
$target = $arguments[$r->name][$p->name];
117+
if ('?' !== $target[0]) {
118+
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
119+
} elseif ('' === $target = (string) substr($target, 1)) {
120+
throw new InvalidArgumentException(sprintf('A "%s" tag must have non-empty "service" attributes for service "%s".', $this->controllerTag, $id));
121+
} elseif ($p->allowsNull() && !$p->isOptional()) {
122+
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
123+
}
124+
} elseif (!$type) {
125+
continue;
126+
}
127+
128+
$args[$p->name] = new ServiceClosureArgument($type ? new TypedReference($target, $type, $invalidBehavior, false) : new Reference($target, $invalidBehavior));
129+
}
130+
// register the maps as a per-method service-locators
131+
if ($args) {
132+
$argsId = sprintf('arguments.%s:%s', $id, $r->name);
133+
$container->register($argsId, ServiceLocator::class)
134+
->addArgument($args)
135+
->setPublic(false)
136+
->setAutowired($isAutowired)
137+
->addTag('controller.arguments_locator', array($class, $id, $r->name));
138+
$controllers[$id.':'.$r->name] = new Reference($argsId);
139+
if ($id === $class) {
140+
$controllers[$id.'::'.$r->name] = new Reference($argsId);
141+
}
142+
}
143+
}
144+
}
145+
146+
$container->getDefinition($this->resolverServiceId)
147+
->getArgument(0)
148+
->setValues($controllers);
149+
}
150+
}
+75Lines 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 ($listed = 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.