Browse Source

[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.
Johannes Schmitt 14 năm trước cách đây
mục cha
commit
6d7a9d752d

+ 5 - 0
UPDATE.md

@@ -20,6 +20,11 @@ PR11 to PR12
 
         <bundle>MyBundle</bundle>
 
+* 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
 ------------
 

+ 2 - 2
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)

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

@@ -0,0 +1,46 @@
+<?php
+
+namespace Symfony\Component\DependencyInjection\Compiler;
+
+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->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);
+                }
+            }
+        }
+    }
+}

+ 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;
+    }
+}

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

@@ -0,0 +1,42 @@
+<?php
+
+namespace Symfony\Tests\Component\DependencyInjection\Compiler;
+
+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);
+    }
+
+    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));
     }