[DependencyInjection] Add autowiring capabilities

This commit is contained in:
Kévin Dunglas 2015-08-24 03:36:41 +02:00 committed by Fabien Potencier
parent bee1faaa95
commit aee57315c5
17 changed files with 827 additions and 0 deletions

View File

@ -0,0 +1,263 @@
<?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\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
/**
* Guesses constructor arguments of services definitions and try to instantiate services if necessary.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AutowirePass implements CompilerPassInterface
{
private $container;
private $reflectionClasses = array();
private $definedTypes = array();
private $types;
private $notGuessableTypes = array();
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$this->container = $container;
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isAutowired()) {
$this->completeDefinition($id, $definition);
}
}
// Free memory and remove circular reference to container
$this->container = null;
$this->reflectionClasses = array();
$this->definedTypes = array();
$this->types = null;
$this->notGuessableTypes = array();
}
/**
* Wires the given definition.
*
* @param string $id
* @param Definition $definition
*
* @throws RuntimeException
*/
private function completeDefinition($id, Definition $definition)
{
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
return;
}
$this->container->addClassResource($reflectionClass);
if (!$constructor = $reflectionClass->getConstructor()) {
return;
}
$arguments = $definition->getArguments();
foreach ($constructor->getParameters() as $index => $parameter) {
$argumentExists = array_key_exists($index, $arguments);
if ($argumentExists && '' !== $arguments[$index]) {
continue;
}
try {
if (!$typeHint = $parameter->getClass()) {
continue;
}
if (null === $this->types) {
$this->populateAvailableTypes();
}
if (isset($this->types[$typeHint->name])) {
$value = new Reference($this->types[$typeHint->name]);
} else {
try {
$value = $this->createAutowiredDefinition($typeHint, $id);
} catch (RuntimeException $e) {
if (!$parameter->isDefaultValueAvailable()) {
throw $e;
}
$value = $parameter->getDefaultValue();
}
}
} catch (\ReflectionException $reflectionException) {
// Typehint against a non-existing class
if (!$parameter->isDefaultValueAvailable()) {
continue;
}
$value = $parameter->getDefaultValue();
}
if ($argumentExists) {
$definition->replaceArgument($index, $value);
} else {
$definition->addArgument($value);
}
}
}
/**
* Populates the list of available types.
*/
private function populateAvailableTypes()
{
$this->types = array();
foreach ($this->container->getDefinitions() as $id => $definition) {
$this->populateAvailableType($id, $definition);
}
}
/**
* Populates the list of available types for a given definition.
*
* @param string $id
* @param Definition $definition
*/
private function populateAvailableType($id, Definition $definition)
{
if (!$definition->getClass()) {
return;
}
foreach ($definition->getAutowiringTypes() as $type) {
$this->definedTypes[$type] = true;
$this->types[$type] = $id;
}
if ($reflectionClass = $this->getReflectionClass($id, $definition)) {
$this->extractInterfaces($id, $reflectionClass);
$this->extractAncestors($id, $reflectionClass);
}
}
/**
* Extracts the list of all interfaces implemented by a class.
*
* @param string $id
* @param \ReflectionClass $reflectionClass
*/
private function extractInterfaces($id, \ReflectionClass $reflectionClass)
{
foreach ($reflectionClass->getInterfaces() as $interfaceName => $reflectionInterface) {
$this->set($interfaceName, $id);
$this->extractInterfaces($id, $reflectionInterface);
}
}
/**
* Extracts all inherited types of a class.
*
* @param string $id
* @param \ReflectionClass $reflectionClass
*/
private function extractAncestors($id, \ReflectionClass $reflectionClass)
{
$this->set($reflectionClass->name, $id);
if ($reflectionParentClass = $reflectionClass->getParentClass()) {
$this->extractAncestors($id, $reflectionParentClass);
}
}
/**
* 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]) || isset($this->notGuessableTypes[$type])) {
return;
}
if (isset($this->types[$type])) {
if ($this->types[$type] === $id) {
return;
}
unset($this->types[$type]);
$this->notGuessableTypes[$type] = true;
return;
}
$this->types[$type] = $id;
}
/**
* Registers a definition for the type if possible or throws an exception.
*
* @param \ReflectionClass $typeHint
* @param string $id
*
* @return Reference A reference to the registered definition
*
* @throws RuntimeException
*/
private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
{
if (!$typeHint->isInstantiable()) {
throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s".', $typeHint->name, $id));
}
$argumentId = sprintf('autowired.%s', $typeHint->name);
$argumentDefinition = $this->container->register($argumentId, $typeHint->name);
$argumentDefinition->setPublic(false);
$this->populateAvailableType($argumentId, $argumentDefinition);
$this->completeDefinition($argumentId, $argumentDefinition);
return new Reference($argumentId);
}
/**
* Retrieves the reflection class associated with the given service.
*
* @param string $id
* @param Definition $definition
*
* @return \ReflectionClass|null
*/
private function getReflectionClass($id, Definition $definition)
{
if (isset($this->reflectionClasses[$id])) {
return $this->reflectionClasses[$id];
}
if (!$class = $definition->getClass()) {
return;
}
$class = $this->container->getParameterBag()->resolveValue($class);
try {
return $this->reflectionClasses[$id] = new \ReflectionClass($class);
} catch (\ReflectionException $reflectionException) {
// return null
}
}
}

View File

@ -50,6 +50,7 @@ class PassConfig
new CheckDefinitionValidityPass(),
new ResolveReferencesToAliasesPass(),
new ResolveInvalidReferencesPass(),
new AutowirePass(),
new AnalyzeServiceReferencesPass(true),
new CheckCircularReferencesPass(),
new CheckReferenceValidityPass(),

View File

@ -41,6 +41,8 @@ class Definition
private $synchronized = false;
private $lazy = false;
private $decoratedService;
private $autowired = false;
private $autowiringTypes = array();
protected $arguments;
@ -818,4 +820,96 @@ class Definition
{
return $this->configurator;
}
/**
* Sets types that will default to this definition.
*
* @param string[] $types
*
* @return Definition The current instance
*/
public function setAutowiringTypes(array $types)
{
$this->autowiringTypes = array();
foreach ($types as $type) {
$this->autowiringTypes[$type] = true;
}
return $this;
}
/**
* Is the definition autowired?
*
* @return bool
*/
public function isAutowired()
{
return $this->autowired;
}
/**
* Sets autowired.
*
* @param $autowired
*
* @return Definition The current instance
*/
public function setAutowired($autowired)
{
$this->autowired = $autowired;
return $this;
}
/**
* Gets autowiring types that will default to this definition.
*
* @return string[]
*/
public function getAutowiringTypes()
{
return array_keys($this->autowiringTypes);
}
/**
* Adds a type that will default to this definition.
*
* @param string $type
*
* @return Definition The current instance
*/
public function addAutowiringType($type)
{
$this->autowiringTypes[$type] = true;
return $this;
}
/**
* Removes a type.
*
* @param string $type
*
* @return Definition The current instance
*/
public function removeAutowiringType($type)
{
unset($this->autowiringTypes[$type]);
return $this;
}
/**
* Will this definition default for the given type?
*
* @param string $type
*
* @return bool
*/
public function hasAutowiringType($type)
{
return isset($this->autowiringTypes[$type]);
}
}

View File

@ -157,6 +157,10 @@ class XmlFileLoader extends FileLoader
}
}
if ($value = $service->getAttribute('autowire')) {
$definition->setAutowired(XmlUtils::phpize($value));
}
if ($value = $service->getAttribute('scope')) {
$triggerDeprecation = 'request' !== (string) $service->getAttribute('id');
@ -247,6 +251,10 @@ class XmlFileLoader extends FileLoader
$definition->addTag($tag->getAttribute('name'), $parameters);
}
foreach ($this->getChildren($service, 'autowiring-type') as $type) {
$definition->addAutowiringType($type->textContent);
}
if ($value = $service->getAttribute('decorates')) {
$renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
$priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0;

View File

@ -299,6 +299,28 @@ class YamlFileLoader extends FileLoader
$definition->setDecoratedService($service['decorates'], $renameId, $priority);
}
if (isset($service['autowire'])) {
$definition->setAutowired($service['autowire']);
}
if (isset($service['autowiring_types'])) {
if (is_string($service['autowiring_types'])) {
$definition->addAutowiringType($service['autowiring_types']);
} else {
if (!is_array($service['autowiring_types'])) {
throw new InvalidArgumentException(sprintf('Parameter "autowiring_types" must be a string or an array for service "%s" in %s. Check your YAML syntax.', $id, $file));
}
foreach ($service['autowiring_types'] as $autowiringType) {
if (!is_string($autowiringType)) {
throw new InvalidArgumentException(sprintf('A "autowiring_types" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file));
}
$definition->addAutowiringType($autowiringType);
}
}
}
$this->container->setDefinition($id, $definition);
}

View File

@ -85,6 +85,7 @@
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="autowiring-type" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="class" type="xsd:string" />
@ -103,6 +104,7 @@
<xsd:attribute name="decorates" type="xsd:string" />
<xsd:attribute name="decoration-inner-name" type="xsd:string" />
<xsd:attribute name="decoration-priority" type="xsd:integer" />
<xsd:attribute name="autowire" type="boolean" />
</xsd:complexType>
<xsd:complexType name="tag">

View File

@ -0,0 +1,300 @@
<?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\Compiler;
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AutowirePassTest extends \PHPUnit_Framework_TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container->register('foo', __NAMESPACE__.'\Foo');
$barDefinition = $container->register('bar', __NAMESPACE__.'\Bar');
$barDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(1, $container->getDefinition('bar')->getArguments());
$this->assertEquals('foo', (string) $container->getDefinition('bar')->getArgument(0));
}
public function testProcessAutowireParent()
{
$container = new ContainerBuilder();
$container->register('b', __NAMESPACE__.'\B');
$cDefinition = $container->register('c', __NAMESPACE__.'\C');
$cDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(1, $container->getDefinition('c')->getArguments());
$this->assertEquals('b', (string) $container->getDefinition('c')->getArgument(0));
}
public function testProcessAutowireInterface()
{
$container = new ContainerBuilder();
$container->register('f', __NAMESPACE__.'\F');
$gDefinition = $container->register('g', __NAMESPACE__.'\G');
$gDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(2, $container->getDefinition('g')->getArguments());
$this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(0));
$this->assertEquals('f', (string) $container->getDefinition('g')->getArgument(1));
}
public function testCompleteExistingDefinition()
{
$container = new ContainerBuilder();
$container->register('b', __NAMESPACE__.'\B');
$container->register('f', __NAMESPACE__.'\F');
$hDefinition = $container->register('h', __NAMESPACE__.'\H')->addArgument(new Reference('b'));
$hDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(2, $container->getDefinition('h')->getArguments());
$this->assertEquals('b', (string) $container->getDefinition('h')->getArgument(0));
$this->assertEquals('f', (string) $container->getDefinition('h')->getArgument(1));
}
public function testCompleteExistingDefinitionWithNotDefinedArguments()
{
$container = new ContainerBuilder();
$container->register('b', __NAMESPACE__.'\B');
$container->register('f', __NAMESPACE__.'\F');
$hDefinition = $container->register('h', __NAMESPACE__.'\H')->addArgument('')->addArgument('');
$hDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(2, $container->getDefinition('h')->getArguments());
$this->assertEquals('b', (string) $container->getDefinition('h')->getArgument(0));
$this->assertEquals('f', (string) $container->getDefinition('h')->getArgument(1));
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" for the service "a".
*/
public function testTypeCollision()
{
$container = new ContainerBuilder();
$container->register('c1', __NAMESPACE__.'\CollisionA');
$container->register('c2', __NAMESPACE__.'\CollisionB');
$aDefinition = $container->register('a', __NAMESPACE__.'\CannotBeAutowired');
$aDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
}
public function testWithTypeSet()
{
$container = new ContainerBuilder();
$container->register('c1', __NAMESPACE__.'\CollisionA');
$container->register('c2', __NAMESPACE__.'\CollisionB')->addAutowiringType(__NAMESPACE__.'\CollisionInterface');
$aDefinition = $container->register('a', __NAMESPACE__.'\CannotBeAutowired');
$aDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(1, $container->getDefinition('a')->getArguments());
$this->assertEquals('c2', (string) $container->getDefinition('a')->getArgument(0));
}
public function testCreateDefinition()
{
$container = new ContainerBuilder();
$coopTilleulsDefinition = $container->register('coop_tilleuls', __NAMESPACE__.'\LesTilleuls');
$coopTilleulsDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(1, $container->getDefinition('coop_tilleuls')->getArguments());
$this->assertEquals('autowired.symfony\component\dependencyinjection\tests\compiler\dunglas', $container->getDefinition('coop_tilleuls')->getArgument(0));
$dunglasDefinition = $container->getDefinition('autowired.symfony\component\dependencyinjection\tests\compiler\dunglas');
$this->assertEquals(__NAMESPACE__.'\Dunglas', $dunglasDefinition->getClass());
$this->assertFalse($dunglasDefinition->isPublic());
$this->assertCount(1, $dunglasDefinition->getArguments());
$this->assertEquals('autowired.symfony\component\dependencyinjection\tests\compiler\lille', $dunglasDefinition->getArgument(0));
$lilleDefinition = $container->getDefinition('autowired.symfony\component\dependencyinjection\tests\compiler\lille');
$this->assertEquals(__NAMESPACE__.'\Lille', $lilleDefinition->getClass());
}
public function testResolveParameter()
{
$container = new ContainerBuilder();
$container->setParameter('class_name', __NAMESPACE__.'\Foo');
$container->register('foo', '%class_name%');
$barDefinition = $container->register('bar', __NAMESPACE__.'\Bar');
$barDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertEquals('foo', $container->getDefinition('bar')->getArgument(0));
}
public function testOptionalParameter()
{
$container = new ContainerBuilder();
$container->register('a', __NAMESPACE__.'\A');
$container->register('foo', __NAMESPACE__.'\Foo');
$optDefinition = $container->register('opt', __NAMESPACE__.'\OptionalParameter');
$optDefinition->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$definition = $container->getDefinition('opt');
$this->assertNull($definition->getArgument(0));
$this->assertEquals('a', $definition->getArgument(1));
$this->assertEquals('foo', $definition->getArgument(2));
}
public function testDontTriggeruAutowiring()
{
$container = new ContainerBuilder();
$container->register('foo', __NAMESPACE__.'\Foo');
$container->register('bar', __NAMESPACE__.'\Bar');
$pass = new AutowirePass();
$pass->process($container);
$this->assertCount(0, $container->getDefinition('bar')->getArguments());
}
}
class Foo
{
}
class Bar
{
public function __construct(Foo $foo)
{
}
}
class A
{
}
class B extends A
{
}
class C
{
public function __construct(A $a)
{
}
}
interface DInterface
{
}
interface EInterface extends DInterface
{
}
class F implements EInterface
{
}
class G
{
public function __construct(DInterface $d, EInterface $e)
{
}
}
class H
{
public function __construct(B $b, DInterface $d)
{
}
}
interface CollisionInterface
{
}
class CollisionA implements CollisionInterface
{
}
class CollisionB implements CollisionInterface
{
}
class CannotBeAutowired
{
public function __construct(CollisionInterface $collision)
{
}
}
class Lille
{
}
class Dunglas
{
public function __construct(Lille $l)
{
}
}
class LesTilleuls
{
public function __construct(Dunglas $k)
{
}
}
class OptionalParameter
{
public function __construct(CollisionInterface $c = null, A $a, Foo $f = null)
{
}
}

View File

@ -890,6 +890,19 @@ class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($classInList);
}
public function testAutowiring()
{
$container = new ContainerBuilder();
$container->register('a', __NAMESPACE__.'\A');
$bDefinition = $container->register('b', __NAMESPACE__.'\B');
$bDefinition->setAutowired(true);
$container->compile();
$this->assertEquals('a', (string) $container->getDefinition('b')->getArgument(0));
}
}
class FooClass
@ -903,3 +916,14 @@ class ProjectContainer extends ContainerBuilder
throw new InactiveScopeException('foo', 'request');
}
}
class A
{
}
class B
{
public function __construct(A $a)
{
}
}

View File

@ -394,4 +394,25 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
$this->assertSame($def, $def->setProperty('foo', 'bar'));
$this->assertEquals(array('foo' => 'bar'), $def->getProperties());
}
public function testAutowired()
{
$def = new Definition('stdClass');
$this->assertFalse($def->isAutowired());
$def->setAutowired(true);
$this->assertTrue($def->isAutowired());
}
public function testTypes()
{
$def = new Definition('stdClass');
$this->assertEquals(array(), $def->getAutowiringTypes());
$this->assertSame($def, $def->setAutowiringTypes(array('Foo')));
$this->assertEquals(array('Foo'), $def->getAutowiringTypes());
$this->assertSame($def, $def->addAutowiringType('Bar'));
$this->assertTrue($def->hasAutowiringType('Bar'));
$this->assertSame($def, $def->removeAutowiringType('Foo'));
$this->assertEquals(array('Bar'), $def->getAutowiringTypes());
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="foo" class="Foo">
<autowiring-type>Bar</autowiring-type>
<autowiring-type>Baz</autowiring-type>
</service>
</services>
</container>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="bar" class="Bar" autowire="true" />
</services>
</container>

View File

@ -0,0 +1,5 @@
services:
foo_service:
class: FooClass
# types is not an array
autowiring_types: 1

View File

@ -0,0 +1,5 @@
services:
foo_service:
class: FooClass
# autowiring_types is not a string
autowiring_types: [ 1 ]

View File

@ -0,0 +1,8 @@
services:
foo_service:
class: FooClass
autowiring_types: [ Foo, Bar ]
baz_service:
class: Baz
autowiring_types: Foo

View File

@ -0,0 +1,4 @@
services:
bar_service:
class: BarClass
autowire: true

View File

@ -494,4 +494,22 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertSame('Baz', $barConfigurator[0]->getClass());
$this->assertSame('configureBar', $barConfigurator[1]);
}
public function testType()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services22.xml');
$this->assertEquals(array('Bar', 'Baz'), $container->getDefinition('foo')->getAutowiringTypes());
}
public function testAutowire()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services23.xml');
$this->assertTrue($container->getDefinition('bar')->isAutowired());
}
}

View File

@ -282,4 +282,41 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array(true), $definition->getArguments());
$this->assertEquals(array('manager' => array(array('alias' => 'user'))), $definition->getTags());
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testTypesNotArray()
{
$loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('bad_types1.yml');
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testTypeNotString()
{
$loader = new YamlFileLoader(new ContainerBuilder(), new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('bad_types2.yml');
}
public function testTypes()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services22.yml');
$this->assertEquals(array('Foo', 'Bar'), $container->getDefinition('foo_service')->getAutowiringTypes());
$this->assertEquals(array('Foo'), $container->getDefinition('baz_service')->getAutowiringTypes());
}
public function testAutowire()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services23.yml');
$this->assertTrue($container->getDefinition('bar_service')->isAutowired());
}
}