diff --git a/UPDATE.md b/UPDATE.md index eef0f78c18..916c669dfd 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -63,6 +63,11 @@ PR11 to PR12 arbitrary accounts when the SwitchUserListener was activated. Configurations which do not use the SwitchUserListener are not affected. +* The Dependency Injection Container now strongly validates the references of + all your services at the end of its compilation process. If you have invalid + references this will result in a compile-time exception instead of a run-time + exception (the previous behavior). + PR10 to PR11 ------------ diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php new file mode 100644 index 0000000000..55bef03c10 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -0,0 +1,55 @@ + + */ +class CheckExceptionOnInvalidReferenceBehaviorPass implements CompilerPassInterface +{ + private $container; + private $sourceId; + + public function process(ContainerBuilder $container) + { + $this->container = $container; + + foreach ($container->getDefinitions() as $id => $definition) { + $this->sourceId = $id; + $this->processDefinition($definition); + } + } + + private function processDefinition(Definition $definition) + { + $this->processReferences($definition->getArguments()); + $this->processReferences($definition->getMethodCalls()); + $this->processReferences($definition->getProperties()); + } + + private function processReferences(array $arguments) + { + foreach ($arguments as $argument) { + if (is_array($argument)) { + $this->processReferences($argument); + } else if ($argument instanceof Definition) { + $this->processDefinition($argument); + } else if ($argument instanceof Reference && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $argument->getInvalidBehavior()) { + $destId = (string) $argument; + + if (!$this->container->has($destId)) { + throw new NonExistentServiceException($destId, $this->sourceId); + } + } + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index b340c38616..fc48e0b25f 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -66,6 +66,7 @@ class PassConfig new AnalyzeServiceReferencesPass(), new RemoveUnusedDefinitionsPass(), )), + new CheckExceptionOnInvalidReferenceBehaviorPass(), ); } diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index 01fdc29fbb..2a0af61409 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection; +use Symfony\Component\DependencyInjection\Exception\NonExistentServiceException; use Symfony\Component\DependencyInjection\Exception\CircularReferenceException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -237,7 +238,7 @@ class Container implements ContainerInterface } if (self::EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior) { - throw new \InvalidArgumentException(sprintf('The service "%s" does not exist.', $id)); + throw new NonExistentServiceException($id); } } diff --git a/src/Symfony/Component/DependencyInjection/Exception/NonExistentServiceException.php b/src/Symfony/Component/DependencyInjection/Exception/NonExistentServiceException.php new file mode 100644 index 0000000000..ee42ea9972 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Exception/NonExistentServiceException.php @@ -0,0 +1,38 @@ + + */ +class NonExistentServiceException extends InvalidArgumentException +{ + private $id; + private $sourceId; + + public function __construct($id, $sourceId = null) + { + if (null === $sourceId) { + $msg = sprintf('You have requested a non-existent service "%s".', $id); + } else { + $msg = sprintf('The service "%s" has a dependency on a non-existent service "%s".', $sourceId, $id); + } + + parent::__construct($msg); + + $this->id = $id; + $this->sourceId = $sourceId; + } + + public function getId() + { + return $this->id; + } + + public function getSourceId() + { + return $this->sourceId; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php new file mode 100644 index 0000000000..4db6b8a990 --- /dev/null +++ b/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php @@ -0,0 +1,62 @@ +register('a', '\stdClass') + ->addArgument(new Reference('b')) + ; + $container->register('b', '\stdClass'); + } + + /** + * @expectedException Symfony\Component\DependencyInjection\Exception\NonExistentServiceException + */ + public function testProcessThrowsExceptionOnInvalidReference() + { + $container = new ContainerBuilder(); + + $container + ->register('a', '\stdClass') + ->addArgument(new Reference('b')) + ; + + $this->process($container); + } + + /** + * @expectedException Symfony\Component\DependencyInjection\Exception\NonExistentServiceException + */ + public function testProcessThrowsExceptionOnInvalidReferenceFromInlinedDefinition() + { + $container = new ContainerBuilder(); + + $def = new Definition(); + $def->addArgument(new Reference('b')); + + $container + ->register('a', '\stdClass') + ->addArgument($def) + ; + + $this->process($container); + } + + private function process(ContainerBuilder $container) + { + $pass = new CheckExceptionOnInvalidReferenceBehaviorPass(); + $pass->process($container); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/DependencyInjection/ContainerTest.php b/tests/Symfony/Tests/Component/DependencyInjection/ContainerTest.php index 5dc890b433..fc307babbb 100644 --- a/tests/Symfony/Tests/Component/DependencyInjection/ContainerTest.php +++ b/tests/Symfony/Tests/Component/DependencyInjection/ContainerTest.php @@ -161,8 +161,7 @@ class ContainerTest extends \PHPUnit_Framework_TestCase $sc->get(''); $this->fail('->get() throws a \InvalidArgumentException exception if the service is empty'); } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->get() throws a \InvalidArgumentException exception if the service is empty'); - $this->assertEquals('The service "" does not exist.', $e->getMessage(), '->get() throws a \InvalidArgumentException exception if the service is empty'); + $this->assertInstanceOf('Symfony\Component\DependencyInjection\Exception\NonExistentServiceException', $e, '->get() throws a NonExistentServiceException exception if the service is empty'); } $this->assertNull($sc->get('', ContainerInterface::NULL_ON_INVALID_REFERENCE)); }