浏览代码

Merge remote branch 'schmittjoh/referenceValidation'

* schmittjoh/referenceValidation:
  [DependencyInjection] also check references of inlined services
  [DependencyInjection] adds emulation of "exception-on-invalid-reference" behavior
Fabien Potencier 14 年之前
父节点
当前提交
8b2b8e16dc

+ 5 - 0
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
 ------------
 

+ 55 - 0
src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php

@@ -0,0 +1,55 @@
+<?php
+
+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;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * Checks that all references are pointing to a valid service.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+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);
+                }
+            }
+        }
+    }
+}

+ 1 - 0
src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

@@ -66,6 +66,7 @@ class PassConfig
                 new AnalyzeServiceReferencesPass(),
                 new RemoveUnusedDefinitionsPass(),
             )),
+            new CheckExceptionOnInvalidReferenceBehaviorPass(),
         );
     }
 

+ 2 - 1
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);
         }
     }
 

+ 38 - 0
src/Symfony/Component/DependencyInjection/Exception/NonExistentServiceException.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace Symfony\Component\DependencyInjection\Exception;
+
+/**
+ * This exception is thrown when a non-existent service is requested.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+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;
+    }
+}

+ 62 - 0
tests/Symfony/Tests/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPassTest.php

@@ -0,0 +1,62 @@
+<?php
+
+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;
+
+class CheckExceptionOnInvalidReferenceBehaviorPassTest extends \PHPUnit_Framework_TestCase
+{
+    public function testProcess()
+    {
+        $container = new ContainerBuilder();
+
+        $container
+            ->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);
+    }
+}

+ 1 - 2
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));
     }