From 6d7a9d752dafefe64d9b31bfca3ace77e96af380 Mon Sep 17 00:00:00 2001 From: Johannes Schmitt Date: Fri, 15 Apr 2011 00:11:50 +0200 Subject: [PATCH 1/2] [DependencyInjection] adds emulation of "exception-on-invalid-reference" behavior This pass requires that all of references are valid at the end of the compilation process. --- UPDATE.md | 5 ++ .../Debug/TraceableEventDispatcher.php | 4 +- ...xceptionOnInvalidReferenceBehaviorPass.php | 46 +++++++++++++++++++ .../Compiler/PassConfig.php | 1 + .../DependencyInjection/Container.php | 3 +- .../Exception/NonExistentServiceException.php | 38 +++++++++++++++ ...tionOnInvalidReferenceBehaviorPassTest.php | 42 +++++++++++++++++ .../DependencyInjection/ContainerTest.php | 3 +- 8 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php create mode 100644 src/Symfony/Component/DependencyInjection/Exception/NonExistentServiceException.php create mode 100644 tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php diff --git a/UPDATE.md b/UPDATE.md index a4c65143aa..38664a4f34 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -20,6 +20,11 @@ PR11 to PR12 MyBundle +* 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/Bundle/FrameworkBundle/Debug/TraceableEventDispatcher.php b/src/Symfony/Bundle/FrameworkBundle/Debug/TraceableEventDispatcher.php index fc94368cd9..bdea0f0456 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Bundle/FrameworkBundle/Debug/TraceableEventDispatcher.php @@ -123,10 +123,10 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements /** * Returns information about the listener - * + * * @param object $listener The listener * @param string $eventName The event name - * + * * @return array Informations about the listener */ private function getListenerInfo($listener, $eventName) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php new file mode 100644 index 0000000000..39894dbc8b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -0,0 +1,46 @@ + + */ +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->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 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 1cd5733aaa..06f05cbbd5 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..9a5f8cae3c --- /dev/null +++ b/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php @@ -0,0 +1,42 @@ +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); + } + + 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)); } From fd5caa9546eda2ba1c12ae8fb533f921f4611f8e Mon Sep 17 00:00:00 2001 From: Johannes Schmitt Date: Fri, 15 Apr 2011 08:47:28 +0200 Subject: [PATCH 2/2] [DependencyInjection] also check references of inlined services --- ...xceptionOnInvalidReferenceBehaviorPass.php | 15 +++++++++++--- ...tionOnInvalidReferenceBehaviorPassTest.php | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php index 39894dbc8b..55bef03c10 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -2,6 +2,8 @@ namespace Symfony\Component\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Exception\NonExistentServiceException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -23,17 +25,24 @@ class CheckExceptionOnInvalidReferenceBehaviorPass implements CompilerPassInterf foreach ($container->getDefinitions() as $id => $definition) { $this->sourceId = $id; - $this->processReferences($definition->getArguments()); - $this->processReferences($definition->getMethodCalls()); - $this->processReferences($definition->getProperties()); + $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; diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php index 9a5f8cae3c..4db6b8a990 100644 --- a/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php +++ b/tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php @@ -2,6 +2,8 @@ namespace Symfony\Tests\Component\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -34,6 +36,24 @@ class CheckExceptionOnInvalidReferenceBehaviorPassTest extends \PHPUnit_Framewor $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();