8000 feature #21383 [DependencyInjection] Add support for named arguments … · Bilge/symfony@9ac3a7e · GitHub
[go: up one dir, main page]

Skip to content

Commit 9ac3a7e

Browse files
committed
feature symfony#21383 [DependencyInjection] Add support for named arguments (dunglas, nicolas-grekas)
This PR was merged into the 3.3-dev branch. Discussion ---------- [DependencyInjection] Add support for named arguments | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | todo This PR introduces named arguments for services definitions. It's especially useful to inject parameters in an autowired service. It is (at least partially) an alternative to symfony#21376 and symfony#20738. Usage: ```yml services: _defaults: { autowire: true } Acme\NewsletterManager: { $apiKey: "%mandrill_api_key%" } # Alternative (traditional) syntax services: newsletter_manager: class: Acme\NewsletterManager arguments: $apiKey: "%mandrill_api_key%" autowire: true ``` ```php use Doctrine\ORM\EntityManager; use Psr\Log\LoggerInterface; namespace Acme; class NewsletterManager { private $logger; private $em; private $apiKey; public function __construct(LoggerInterface $logger, EntityManager $em, $apiKey) { $this->logger = $logger; $this->em = $em; $this->apiKey = $apiKey; } } ``` Commits ------- 8a126c8 [DI] Deprecate string keys in arguments 2ce36a6 [DependencyInjection] Add a new pass to check arguments validity 6e50129 [DependencyInjection] Add support for named arguments
2 parents 17b4363 + 8a126c8 commit 9ac3a7e

14 files changed

+447
-12
lines changed

src/Symfony/Component/DependencyInjection/ChildDefinition.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,14 @@ public function getArgument($index)
213213
*/
214214
public function replaceArgument($index, $value)
215215
{
216-
if (!is_int($index)) {
216+
if (is_int($index)) {
217+
$this->arguments['index_'.$index] = $value;
218+
} elseif (0 === strpos($index, '$')) {
219+
$this->arguments[$index] = $value;
220+
} else {
217221
throw new InvalidArgumentException('$index must be an integer.');
218222
}
219223

220-
$this->arguments['index_'.$index] = $value;
221-
222224
return $this;
223225
}
224226
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Definition;
15+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
16+
17+
/**
18+
* Checks if arguments of methods are properly configured.
19+
*
20+
* @author Kévin Dunglas <dunglas@gmail.com>
21+
* @author Nicolas Grekas <p@tchwork.com>
22+
*/
23+
class CheckArgumentsValidityPass extends AbstractRecursivePass
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
protected function processValue($value, $isRoot = false)
29+
{
30+
if (!$value instanceof Definition) {
31+
return parent::processValue($value, $isRoot);
32+
}
33+
34+
$i = 0;
35+
foreach ($value->getArguments() as $k => $v) {
36+
if ($k !== $i++) {
37+
if (!is_int($k)) {
38+
throw new RuntimeException(sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k));
39+
}
40+
41+
throw new RuntimeException(sprintf('Invalid constructor argument %d for service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $this->currentId, $i));
42+
}
43+
}
44+
45+
foreach ($value->getMethodCalls() as $methodCall) {
46+
$i = 0;
47+
foreach ($methodCall[1] as $k => $v) {
48+
if ($k !== $i++) {
49+
if (!is_int($k)) {
50+
throw new RuntimeException(sprintf('Invalid argument for method call "%s" of service "%s": integer expected but found string "%s". Check your service definition.', $methodCall[0], $this->currentId, $k));
51+
}
52+
53+
throw new RuntimeException(sprintf('Invalid argument %d for method call "%s" of service "%s": argument %d must be defined before. Check your service definition.', 1 + $k, $methodCall[0], $this->currentId, $i));
54+
}
55+
}
56+
}
57+
}
58+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,14 @@ public function __construct()
5353
new ResolveFactoryClassPass(),
5454
new FactoryReturnTypePass($resolveClassPass),
5555
new CheckDefinitionValidityPass(),
56+
new ResolveNamedArgumentsPass(),
5657
new AutowirePass(),
5758
new ResolveReferencesToAliasesPass(),
5859
new ResolveInvalidReferencesPass(),
5960
new AnalyzeServiceReferencesPass(true),
6061
new CheckCircularReferencesPass(),
6162
new CheckReferenceValidityPass(),
63+
new CheckArgumentsValidityPass(),
6264
));
6365

6466
$this->removingPasses = array(array(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,12 @@ private function doResolveDefinition(ChildDefinition $definition)
145145
continue;
146146
}
147147

148-
if (0 !== strpos($k, 'index_')) {
148+
if (0 === strpos($k, 'index_')) {
149+
$index = (int) substr($k, strlen('index_'));
150+
} elseif (0 !== strpos($k, '$')) {
149151
throw new RuntimeException(sprintf('Invalid argument key "%s" found.', $k));
150152
}
151153

152-
$index = (int) substr($k, strlen('index_'));
153154
$def->replaceArgument($index, $v);
154155
}
155156

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Definition;
15+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
17+
/**
18+
* Resolves named arguments to their corresponding numeric index.
19+
*
20+
* @author Kévin Dunglas <dunglas@gmail.com>
21+
*/
22+
class ResolveNamedArgumentsPass extends AbstractRecursivePass
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
protected function processValue($value, $isRoot = false)
28+
{
29+
if (!$value instanceof Definition) {
30+
return parent::processValue($value, $isRoot);
31+
}
32+
33+
$parameterBag = $this->container->getParameterBag();
34+
35+
if ($class = $value->getClass()) {
36+
$class = $parameterBag->resolveValue($class);
37+
}
38+
39+
$calls = $value->getMethodCalls();
40+
$calls[] = array('__construct', $value->getArguments());
41+
42+
foreach ($calls as $i => $call) {
43+
list($method, $arguments) = $call;
44+
$method = $parameterBag->resolveValue($method);
45+
$parameters = null;
46+
$resolvedArguments = array();
47+
48+
foreach ($arguments as $key => $argument) {
49+
if (is_int($key) || '' === $key || '$' !== $key[0]) {
50+
if (!is_int($key)) {
51+
@trigger_error(sprintf('Using key "%s" for defining arguments of method "%s" for service "%s" is deprecated since Symfony 3.3 and will throw an exception in 4.0. Use no keys or $named arguments instead.', $key, $method, $this->currentId), E_USER_DEPRECATED);
52+
}
53+
$resolvedArguments[] = $argument;
54+
continue;
55+
}
56+
57+
$parameters = null !== $parameters ? $parameters : $this->getParameters($class, $method);
58+
59+
foreach ($parameters as $j => $p) {
60+
if ($key === '$'.$p->name) {
61+
$resolvedArguments[$j] = $argument;
62+
63+
continue 2;
64+
}
65+
}
66+
67+
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" has no argument named "%s". Check your service definition.', $this->currentId, $class, $method, $key));
68+
}
69+
70+
if ($resolvedArguments !== $call[1]) {
71+
ksort($resolvedArguments);
72+
$calls[$i][1] = $resolvedArguments;
73+
}
74+
}
75+
76+
list(, $arguments) = array_pop($calls);
77+
78+
if ($arguments !== $value->getArguments()) {
79+
$value->setArguments($arguments);
80+
}
81+
if ($calls !== $value->getMethodCalls()) {
82+
$value->setMethodCalls($calls);
83+
}
84+
85+
return parent::processValue($value, $isRoot);
86+
}
87+
88+
/**
89+
* @param string|null $class
90+
* @param string $method
91+
*
92+
* @throws InvalidArgumentException
93+
*
94+
* @return array
95+
*/
96+
private function getParameters($class, $method)
97+
{
98+
if (!$class) {
99+
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId));
100+
}
101+
102+
if (!$r = $this->container->getReflectionClass($class)) {
103+
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class));
104+
}
105+
106+
if (!$r->hasMethod($method)) {
107+
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" does not exist.', $this->currentId, $class, $method));
108+
}
109+
110+
$method = $r->getMethod($method);
111+
if (!$method->isPublic()) {
112+
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" must be public.', $this->currentId, $class, $method->name));
113+
}
114+
115+
return $method->getParameters();
116+
}
117+
}

src/Symfony/Component/DependencyInjection/Definition.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ public function addArgument($argument)
190190
/**
191191
* Sets a specific argument.
192192
*
193-
* @param int $index
194-
* @param mixed $argument
193+
* @param int|string $index
194+
* @param mixed $argument
195195
*
196196
* @return $this
197197
*
@@ -203,10 +203,14 @@ public function replaceArgument($index, $argument)
203203
throw new OutOfBoundsException('Cannot replace arguments if none have been configured yet.');
204204
}
205205

206-
if ($index < 0 || $index > count($this->arguments) - 1) {
206+
if (is_int($index) && ($index < 0 || $index > count($this->arguments) - 1)) {
207207
throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [0, %d].', $index, count($this->arguments) - 1));
208208
}
209209

210+
if (!array_key_exists($index, $this->arguments)) {
211+
throw new OutOfBoundsException(sprintf('The argument "%s" doesn\'t exist.', $index));
212+
}
213+
210214
$this->arguments[$index] = $argument;
211215

212216
return $this;
@@ -225,16 +229,16 @@ public function getArguments()
225229
/**
226230
* Gets an argument to pass to the service constructor/factory method.
227231
*
228-
* @param int $index
232+
* @param int|string $index
229233
*
230234
* @return mixed The argument value
231235
*
232236
* @throws OutOfBoundsException When the argument does not exist
233237
*/
234238
public function getArgument($index)
235239
{
236-
if ($index < 0 || $index > count($this->arguments) - 1) {
237-
throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [0, %d].', $index, count($this->arguments) - 1));
240+
if (!array_key_exists($index, $this->arguments)) {
241+
throw new OutOfBoundsException(sprintf('The argument "%s" doesn\'t exist.', $index));
238242
}
239243

240244
return $this->arguments[$index];

src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,22 @@ private function parseDefaults(array &$content, $file)
254254
return $defaults;
255255
}
256256

257+
/**
258+
* @param array $service
259+
*
260+
* @return bool
261+
*/
262+
private function isUsingShortSyntax(array $service)
263+
{
264+
foreach ($service as $key => $value) {
265+
if (is_string($key) && ('' === $key || '$' !== $key[0])) {
266+
return false;
267+
}
268+
}
269+
270+
return true;
271+
}
272+
257273
/**
258274
* Parses a definition.
259275
*
@@ -273,7 +289,7 @@ private function parseDefinition($id, $service, $file, array $defaults)
273289
return;
274290
}
275291

276-
if (is_array($service) && array_values($service) === $service) {
292+
if (is_array($service) && $this->isUsingShortSyntax($service)) {
277293
$service = array('arguments' => $service);
278294
}
279295

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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\Tests\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CheckArgumentsValidityPass;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* @author Kévin Dunglas <dunglas@gmail.com>
19+
*/
20+
class CheckArgumentsValidityPassTest extends \PHPUnit_Framework_TestCase
21+
{
22+
public function testProcess()
23+
{
24+
$container = new ContainerBuilder();
25+
$definition = $container->register('foo');
26+
$definition->setArguments(array(null, 1, 'a'));
27+
$definition->setMethodCalls(array(
28+
array('bar', array('a', 'b')),
29+
array('baz', array('c', 'd')),
30+
));
31+
32+
$pass = new CheckArgumentsValidityPass();
33+
$pass->process($container);
34+
35+
$this->assertEquals(array(null, 1, 'a'), $container->getDefinition('foo')->getArguments());
36+
$this->assertEquals(array(
37+
array('bar', array('a', 'b')),
38+
array('baz', array('c', 'd')),
39+
), $container->getDefinition('foo')->getMethodCalls());
40+
}
41+
42+
/**
43+
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
44+
* @dataProvider definitionProvider
45+
*/
46+
public function testException(array $arguments, array $methodCalls)
47+
{
48+
$container = new ContainerBuilder();
49+
$definition = $container->register('foo');
50+
$definition->setArguments($arguments);
51+
$definition->setMethodCalls($methodCalls);
52+
53+
$pass = new CheckArgumentsValidityPass();
54+
$pass->process($container);
55+
}
56+
57+
public function definitionProvider()
58+
{
59+
return array(
60+
array(array(null, 'a' => 'a'), array()),
61+
array(array(1 => 1), array()),
62+
array(array(), array(array('baz', array(null, 'a' => 'a')))),
63+
array(array(), array(array('baz', array(1 => 1)))),
64+
);
65+
}
66+
}

0 commit comments

Comments
 (0)
0