Skip to content

[DependencyInjection] Create an util class to determine services implementing a FQCN #20940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 32 additions & 80 deletions src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand All @@ -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();
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
}

/**
Expand All @@ -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));
Expand All @@ -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'));
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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));
}
}
145 changes: 145 additions & 0 deletions src/Symfony/Component/DependencyInjection/Util/ServiceTypeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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. <egetick@gmail.com>
*/
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works fine only if you have a fallback autoloader throwing a ReflectionException as a last resort to avoid the fatal error. As you extracted the logic, you need to handle this requirement in your new service

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed in the AutowirePass because it fetches parameters type hint but this helper doesn't use anything else than the class name, is it still needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the class may extend a non-existent parent class (if the service relies on an optional dependency which is not installed)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it's updated in 98481b9.

$name = null;
}

return self::$classNames[$class] = $name;
}
}