Просмотр исходного кода

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 лет назад
Родитель
Сommit
8b2b8e16dc

+ 5 - 0
UPDATE.md

@@ -63,6 +63,11 @@ PR11 to PR12
   arbitrary accounts when the SwitchUserListener was activated. Configurations
   arbitrary accounts when the SwitchUserListener was activated. Configurations
   which do not use the SwitchUserListener are not affected.
   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
 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 AnalyzeServiceReferencesPass(),
                 new RemoveUnusedDefinitionsPass(),
                 new RemoveUnusedDefinitionsPass(),
             )),
             )),
+            new CheckExceptionOnInvalidReferenceBehaviorPass(),
         );
         );
     }
     }
 
 

+ 2 - 1
src/Symfony/Component/DependencyInjection/Container.php

@@ -11,6 +11,7 @@
 
 
 namespace Symfony\Component\DependencyInjection;
 namespace Symfony\Component\DependencyInjection;
 
 
+use Symfony\Component\DependencyInjection\Exception\NonExistentServiceException;
 use Symfony\Component\DependencyInjection\Exception\CircularReferenceException;
 use Symfony\Component\DependencyInjection\Exception\CircularReferenceException;
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
@@ -237,7 +238,7 @@ class Container implements ContainerInterface
         }
         }
 
 
         if (self::EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior) {
         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('');
             $sc->get('');
             $this->fail('->get() throws a \InvalidArgumentException exception if the service is empty');
             $this->fail('->get() throws a \InvalidArgumentException exception if the service is empty');
         } catch (\Exception $e) {
         } 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));
         $this->assertNull($sc->get('', ContainerInterface::NULL_ON_INVALID_REFERENCE));
     }
     }