diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 7aa2d2a64e80d..d188f4b392e2b 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Util\ServiceTypeHelper; /** * Guesses constructor arguments of services definitions and try to instantiate services if necessary. @@ -29,18 +30,20 @@ class AutowirePass implements CompilerPassInterface */ private $container; private $reflectionClasses = array(); - private $definedTypes = array(); private $types; - private $ambiguousServiceTypes = array(); + private $typeHelper; /** * {@inheritdoc} */ public function process(ContainerBuilder $container) { - $throwingAutoloader = function ($class) { throw new \ReflectionException(sprintf('Class %s does not exist', $class)); }; + $throwingAutoloader = function ($class) { + throw new \ReflectionException(sprintf('Class %s does not exist', $class)); + }; spl_autoload_register($throwingAutoloader); + $this->typeHelper = new ServiceTypeHelper($container); try { $this->container = $container; foreach ($container->getDefinitions() as $id => $definition) { @@ -52,11 +55,10 @@ public function process(ContainerBuilder $container) spl_autoload_unregister($throwingAutoloader); // Free memory and remove circular reference to container + $this->typeHelper = null; $this->container = null; $this->reflectionClasses = array(); - $this->definedTypes = array(); $this->types = null; - $this->ambiguousServiceTypes = array(); } } @@ -193,12 +195,8 @@ private function autowireMethod($id, Definition $definition, \ReflectionMethod $ continue; } - if (null === $this->types) { - $this->populateAvailableTypes(); - } - - if (isset($this->types[$typeHint->name])) { - $value = new Reference($this->types[$typeHint->name]); + if (null !== ($injectedService = $this->getOfType($typeHint->name, $id))) { + $value = new Reference($injectedService); $addMethodCall = true; } else { try { @@ -247,13 +245,34 @@ private function autowireMethod($id, Definition $definition, \ReflectionMethod $ } } + private function getOfType($type, $serviceId) + { + if (null === $this->types) { + $this->populateAvailableTypes(); + } + + if (isset($this->types[$type])) { + return $this->types[$type]; + } + + $services = $this->typeHelper->getOfType($type); + if (1 === count($services)) { + return $services[0]; + } + if (1 < count($services)) { + $classOrInterface = class_exists($type) ? 'class' : 'interface'; + $matchingServices = implode(', ', $services); + + throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $type, $serviceId, $classOrInterface, $matchingServices), 1); + } + } + /** * Populates the list of available types. */ private function populateAvailableTypes() { $this->types = array(); - foreach ($this->container->getDefinitions() as $id => $definition) { $this->populateAvailableType($id, $definition); } @@ -273,57 +292,8 @@ private function populateAvailableType($id, Definition $definition) } foreach ($definition->getAutowiringTypes() as $type) { - $this->definedTypes[$type] = true; $this->types[$type] = $id; } - - if (!$reflectionClass = $this->getReflectionClass($id, $definition)) { - return; - } - - foreach ($reflectionClass->getInterfaces() as $reflectionInterface) { - $this->set($reflectionInterface->name, $id); - } - - do { - $this->set($reflectionClass->name, $id); - } while ($reflectionClass = $reflectionClass->getParentClass()); - } - - /** - * Associates a type and a service id if applicable. - * - * @param string $type - * @param string $id - */ - private function set($type, $id) - { - if (isset($this->definedTypes[$type])) { - return; - } - - // is this already a type/class that is known to match multiple services? - if (isset($this->ambiguousServiceTypes[$type])) { - $this->addServiceToAmbiguousType($id, $type); - - return; - } - - // check to make sure the type doesn't match multiple services - if (isset($this->types[$type])) { - if ($this->types[$type] === $id) { - return; - } - - // keep an array of all services matching this type - $this->addServiceToAmbiguousType($id, $type); - - unset($this->types[$type]); - - return; - } - - $this->types[$type] = $id; } /** @@ -338,13 +308,6 @@ private function set($type, $id) */ private function createAutowiredDefinition(\ReflectionClass $typeHint, $id) { - if (isset($this->ambiguousServiceTypes[$typeHint->name])) { - $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; - $matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]); - - throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices), 1); - } - if (!$typeHint->isInstantiable()) { $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface)); @@ -355,7 +318,7 @@ private function createAutowiredDefinition(\ReflectionClass $typeHint, $id) $argumentDefinition = $this->container->register($argumentId, $typeHint->name); $argumentDefinition->setPublic(false); - $this->populateAvailableType($argumentId, $argumentDefinition); + $this->typeHelper->reset(); try { $this->completeDefinition($argumentId, $argumentDefinition, array('__construct')); @@ -398,17 +361,6 @@ private function getReflectionClass($id, Definition $definition) return $this->reflectionClasses[$id] = $reflector; } - private function addServiceToAmbiguousType($id, $type) - { - // keep an array of all services matching this type - if (!isset($this->ambiguousServiceTypes[$type])) { - $this->ambiguousServiceTypes[$type] = array( - $this->types[$type], - ); - } - $this->ambiguousServiceTypes[$type][] = $id; - } - private static function getResourceMetadataForMethod(\ReflectionMethod $method) { $methodArgumentsMetadata = array(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/BadParent.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/BadParent.php new file mode 100644 index 0000000000000..2d2861db517c9 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/BadParent.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Fixtures; + +class BadParent extends ThisDoesNotExist +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Util/ServiceTypeHelperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Util/ServiceTypeHelperTest.php new file mode 100644 index 0000000000000..d3c51ee5286ce --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Util/ServiceTypeHelperTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Util; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Tests\Fixtures\BadParent; +use Symfony\Component\DependencyInjection\Util\ServiceTypeHelper; + +class ServiceTypeHelperTest extends \PHPUnit_Framework_TestCase +{ + public function testIgnoreServiceWithClassNotExisting() + { + $container = new ContainerBuilder(); + $container->register('class_not_exist', 'NotExistingClass'); + + $helper = new ServiceTypeHelper($container); + $this->assertEmpty($helper->getOfType('NotExistingClass')); + } + + public function testIgnoreServiceWithParentNotExisting() + { + $container = new ContainerBuilder(); + $container->register('bad_parent', BadParent::class); + + $helper = new ServiceTypeHelper($container); + $this->assertEmpty($helper->getOfType(BadParent::class)); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Util/ServiceTypeHelper.php b/src/Symfony/Component/DependencyInjection/Util/ServiceTypeHelper.php new file mode 100644 index 0000000000000..c4e7ff8f35cde --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Util/ServiceTypeHelper.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Util; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +/** + * Help finding services corresponding to a type. + * Be aware that the map is constructed once, at the first call to {@link getOfType()}. + * + * @author Guilhem N. + */ +final class ServiceTypeHelper +{ + private static $classNames = array(); + private $container; + private $typeMap; + + public function __construct(ContainerBuilder $container) + { + $this->container = $container; + } + + /** + * Resolves services implementing a type. + * + * @param string $type a class or an interface + * + * @return string[] the services implementing the type + */ + public function getOfType($type) + { + if (null === $this->typeMap) { + $this->populateAvailableTypes(); + } + + if (!isset($this->typeMap[$type])) { + return array(); + } + + return $this->typeMap[$type]; + } + + /** + * Resets the type map. + */ + public function reset() + { + $this->typeMap = null; + } + + /** + * Populates the list of available types. + */ + private function populateAvailableTypes() + { + $throwingAutoloader = function ($class) { + throw new \ReflectionException(sprintf('Class %s does not exist', $class)); + }; + spl_autoload_register($throwingAutoloader); + + try { + $this->typeMap = array(); + foreach ($this->container->getDefinitions() as $id => $definition) { + $this->populateAvailableType($id, $definition); + } + } finally { + spl_autoload_unregister($throwingAutoloader); + } + } + + /** + * Populates the list of available types for a given definition. + * + * @param string $id + * @param Definition $definition + */ + private function populateAvailableType($id, Definition $definition) + { + // Never use abstract services + if ($definition->isAbstract()) { + return; + } + + if (null === ($class = $this->getClass($definition))) { + return; + } + + $types = array(); + if ($interfaces = class_implements($class)) { + $types = $interfaces; + } + + do { + $types[] = $class; + } while ($class = get_parent_class($class)); + + foreach ($types as $type) { + if (!isset($this->typeMap[$type])) { + $this->typeMap[$type] = array(); + } + + $this->typeMap[$type][] = $id; + } + } + + /** + * Retrieves the class associated with the given service. + * + * @param Definition $definition + * + * @return string|null + */ + private function getClass(Definition $definition) + { + // Cannot use reflection if the class isn't set + if (!$class = $definition->getClass()) { + return; + } + + // Normalize the class name (`\Foo` -> `Foo`) + $class = $this->container->getParameterBag()->resolveValue($class); + if (array_key_exists($class, self::$classNames)) { + return self::$classNames[$class]; + } + + try { + $name = (new \ReflectionClass($class))->name; + } catch (\ReflectionException $e) { + $name = null; + } + + return self::$classNames[$class] = $name; + } +}