-
-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #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 #21376 and #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 ------- 8a126c8537 [DI] Deprecate string keys in arguments 2ce36a6074 [DependencyInjection] Add a new pass to check arguments validity 6e501296f9 [DependencyInjection] Add support for named arguments
- Loading branch information
Showing
14 changed files
with
447 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Definition; | ||
use Symfony\Component\DependencyInjection\Exception\RuntimeException; | ||
|
||
/** | ||
* Checks if arguments of methods are properly configured. | ||
* | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
class CheckArgumentsValidityPass extends AbstractRecursivePass | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function processValue($value, $isRoot = false) | ||
{ | ||
if (!$value instanceof Definition) { | ||
return parent::processValue($value, $isRoot); | ||
} | ||
|
||
$i = 0; | ||
foreach ($value->getArguments() as $k => $v) { | ||
if ($k !== $i++) { | ||
if (!is_int($k)) { | ||
throw new RuntimeException(sprintf('Invalid constructor argument for service "%s": integer expected but found string "%s". Check your service definition.', $this->currentId, $k)); | ||
} | ||
|
||
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)); | ||
} | ||
} | ||
|
||
foreach ($value->getMethodCalls() as $methodCall) { | ||
$i = 0; | ||
foreach ($methodCall[1] as $k => $v) { | ||
if ($k !== $i++) { | ||
if (!is_int($k)) { | ||
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)); | ||
} | ||
|
||
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)); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Definition; | ||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; | ||
|
||
/** | ||
* Resolves named arguments to their corresponding numeric index. | ||
* | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
class ResolveNamedArgumentsPass extends AbstractRecursivePass | ||
{ | ||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function processValue($value, $isRoot = false) | ||
{ | ||
if (!$value instanceof Definition) { | ||
return parent::processValue($value, $isRoot); | ||
} | ||
|
||
$parameterBag = $this->container->getParameterBag(); | ||
|
||
if ($class = $value->getClass()) { | ||
$class = $parameterBag->resolveValue($class); | ||
} | ||
|
||
$calls = $value->getMethodCalls(); | ||
$calls[] = array('__construct', $value->getArguments()); | ||
|
||
foreach ($calls as $i => $call) { | ||
list($method, $arguments) = $call; | ||
$method = $parameterBag->resolveValue($method); | ||
$parameters = null; | ||
$resolvedArguments = array(); | ||
|
||
foreach ($arguments as $key => $argument) { | ||
if (is_int($key) || '' === $key || '$' !== $key[0]) { | ||
if (!is_int($key)) { | ||
@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); | ||
} | ||
$resolvedArguments[] = $argument; | ||
continue; | ||
} | ||
|
||
$parameters = null !== $parameters ? $parameters : $this->getParameters($class, $method); | ||
|
||
foreach ($parameters as $j => $p) { | ||
if ($key === '$'.$p->name) { | ||
$resolvedArguments[$j] = $argument; | ||
|
||
continue 2; | ||
} | ||
} | ||
|
||
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)); | ||
} | ||
|
||
if ($resolvedArguments !== $call[1]) { | ||
ksort($resolvedArguments); | ||
$calls[$i][1] = $resolvedArguments; | ||
} | ||
} | ||
|
||
list(, $arguments) = array_pop($calls); | ||
|
||
if ($arguments !== $value->getArguments()) { | ||
$value->setArguments($arguments); | ||
} | ||
if ($calls !== $value->getMethodCalls()) { | ||
$value->setMethodCalls($calls); | ||
} | ||
|
||
return parent::processValue($value, $isRoot); | ||
} | ||
|
||
/** | ||
* @param string|null $class | ||
* @param string $method | ||
* | ||
* @throws InvalidArgumentException | ||
* | ||
* @return array | ||
*/ | ||
private function getParameters($class, $method) | ||
{ | ||
if (!$class) { | ||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId)); | ||
} | ||
|
||
if (!$r = $this->container->getReflectionClass($class)) { | ||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class)); | ||
} | ||
|
||
if (!$r->hasMethod($method)) { | ||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" does not exist.', $this->currentId, $class, $method)); | ||
} | ||
|
||
$method = $r->getMethod($method); | ||
if (!$method->isPublic()) { | ||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" must be public.', $this->currentId, $class, $method->name)); | ||
} | ||
|
||
return $method->getParameters(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\DependencyInjection\Tests\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Compiler\CheckArgumentsValidityPass; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
|
||
/** | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
class CheckArgumentsValidityPassTest extends \PHPUnit_Framework_TestCase | ||
{ | ||
public function testProcess() | ||
{ | ||
$container = new ContainerBuilder(); | ||
$definition = $container->register('foo'); | ||
$definition->setArguments(array(null, 1, 'a')); | ||
$definition->setMethodCalls(array( | ||
array('bar', array('a', 'b')), | ||
array('baz', array('c', 'd')), | ||
)); | ||
|
||
$pass = new CheckArgumentsValidityPass(); | ||
$pass->process($container); | ||
|
||
$this->assertEquals(array(null, 1, 'a'), $container->getDefinition('foo')->getArguments()); | ||
$this->assertEquals(array( | ||
array('bar', array('a', 'b')), | ||
array('baz', array('c', 'd')), | ||
), $container->getDefinition('foo')->getMethodCalls()); | ||
} | ||
|
||
/** | ||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException | ||
* @dataProvider definitionProvider | ||
*/ | ||
public function testException(array $arguments, array $methodCalls) | ||
{ | ||
$container = new ContainerBuilder(); | ||
$definition = $container->register('foo'); | ||
$definition->setArguments($arguments); | ||
$definition->setMethodCalls($methodCalls); | ||
|
||
$pass = new CheckArgumentsValidityPass(); | ||
$pass->process($container); | ||
} | ||
|
||
public function definitionProvider() | ||
{ | ||
return array( | ||
array(array(null, 'a' => 'a'), array()), | ||
array(array(1 => 1), array()), | ||
array(array(), array(array('baz', array(null, 'a' => 'a')))), | ||
array(array(), array(array('baz', array(1 => 1)))), | ||
); | ||
} | ||
} |
Oops, something went wrong.