8000 [DependencyInjection] Add autowiring capabilities by dunglas · Pull Request #15613 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[DependencyInjection] Add autowiring capabilities #15613

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 19 commits into from
Closed
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
Next Next commit
[DependencyInjection] Add autowiring capabilities
  • Loading branch information
dunglas committed Oct 1, 2015
commit 9828da296f6f2f45a741fffc8acd28e85f14a0e1
233 changes: 233 additions & 0 deletions src/Symfony/Component/DependencyInjection/Compiler/AutowiringPass.php
FAB3
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<?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 AutowiringPass implements CompilerPassInterface
{
private $container;
Copy link
Contributor

Choose a reason for hiding this comment

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

why storing the container? Can't you just pass it explicitly to your private methods?

Copy link
Member Author

Choose a reason for hiding this comment

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

As this class already has a lot of states, it's just to avoid passing the container as argument of other methods.
I can rewrite this class without states but not sure it has any interest (and other passes also have states).

private $definitions;
private $reflectionClassesToId = array();
private $definedTypes = array();
private $typesToId;
private $notGuessableTypesToId = array();

/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$this->container = $container;
$this->definitions = $container->getDefinitions();
foreach ($this->definitions as $id => $definition) {
$this->completeDefinition($id, $definition, $container);
Copy link
Contributor

Choose a reason for hiding this comment

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

you're passing the container as an argument of this method...

}
Copy link
Member

Choose a reason for hiding this comment

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

please reset all the internal state at the end of the method to release memory (many things are not needed at all anymore, and there is no reason to kep a circular reference between the ContainerBuilder and this pass)

Copy link
Contributor

Choose a reason for hiding this comment

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

why aren't you using $container->findTaggedServiceIds instead ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, I'll take a look at that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Because I need the definition instance and not the tags.

}

/**
* Wires the given definition.
*
* @param string $id
* @param Definition $definition
*
* @throws RuntimeException
*/
private function completeDefinition($id, Definition $definition)
Copy link
Contributor

Choose a reason for hiding this comment

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

... but the container is not part of the signature

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch (garbage from a previous version).

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

{
if (!($reflectionClass = $this->getReflectionClass($id, $definition))) {
return;
}

if (!($constructor = $reflectionClass->getConstructor())) {
return;
}

$arguments = $definition->getArguments();
foreach ($constructor->getParameters() as $index => $parameter) {
Copy link
Member

Choose a reason for hiding this comment

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

getParameters returns an array indexed by name IIRC, not by numbers, so the $argumentExist check will always be false

Copy link
Member Author

Choose a reason for hiding this comment

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

The array returned by ReflectionMethod::getParameters() is numerically indexed: https://3v4l.org/6i1qY

if (!($typeHint = $parameter->getClass()) || $parameter->isOptional()) {
Copy link
Member

Choose a reason for hiding this comment

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

your handling of optional parameters is wrong. If there is another parameter passed explicitly after this optional one, you must pass the parameter default value explicitly. you cannot omit the argument, as it would shift following ones

Copy link
Member

Choose a reason for hiding this comment

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

btw, you need to be careful about variadic parameters in PHP 5.6+ as your code would break on them

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. Btw I'm not sure about the best to for optional parameters: should we always inject the service matching the typehint if available (and the default value if it is not) or always inject the default value (the current behavior).

Probably that always injecting the service if possible is more convenient. What do you think?

continue;
}

$argumentExist = array_key_exists($index, $arguments);
Copy link
Member

Choose a reason for hiding this comment

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

$argumentExist -> $argumentExists ?

if ($argumentExist && '' !== $arguments[$index]) {
Copy link
Member

Choose a reason for hiding this comment

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

why treating empty strings in a special way ?

Copy link
Member Author

Choose a reason for hiding this comment

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

To allow doing things like that:

<service id="foo" class="Foo">
    <argument/> <!-- Still autowired -->
   <argument>My parameter</argument>
</service>

continue;
}

if (null === $this->typesToId) {
$this->populateAvailableTypes();
}

if (isset($this->typesToId[$typeHint->name])) {
$reference = new Reference($this->typesToId[$typeHint->name]);
} else {
$reference = $this->createAutowiredDefinition($typeHint);
}

if ($argumentExist) {
$definition->replaceArgument($index, $reference);
} else {
$definition->addArgument($reference);
}
}
}

/**
* Populates the list of available types.
*/
private function populateAvailableTypes()
{
$this->typesToId = array();

foreach ($this->definitions as $id => $definition) {
$this->populateAvailableType($id, $definition);
}
}

/**
* Populates the of available types for a given definition.
Copy link
Contributor

Choose a reason for hiding this comment

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

the list of

*
* @param string $id
* @param Definition $definition
*/
private function populateAvailableType($id, Definition $definition) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong CS

if (!($class = $definition->getClass())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

$class is a unused var.

return;
}

foreach ($definition->getTypes() as $type) {
$this->definedTypes[$type] = true;
$this->typesToId[$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 if applicable a type and a service id or a class.
*
* @param string $type
* @param string $value A service id or a class name depending of the value of $class
Copy link
Contributor

Choose a reason for hiding this comment

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

Weird description here (depending of the value of $class).

*/
private function set($type, $value)
{
if (isset($this->definedTypes[$type]) || isset($this->notGuessableTypesToId[$type])) {
return;
}

if (isset($this->typesToId[$type])) {
if ($this->typesToId[$type] === $value) {
return;
}

unset($this->typesToId[$type]);

$this->notGuessableTypesToId[$type] = true;
return;
}

$this->typesToId[$type] = $value;
}

/**
* Registers a definition for the type if possible or throws an exception.
*
* @param \ReflectionClass $typeHint
*
* @return Reference A reference to the registered definition
*
* @throws RuntimeException
*/
private function createAutowiredDefinition(\ReflectionClass $typeHint)
{
if (!$typeHint->isInstantiable()) {
throw new RuntimeException(sprintf('Unable to autowire type "%s".', $typeHint->name));
}

$argumentId = sprintf('autowired.%s', $typeHint->name);

$argumentDefinition = $this->container->register($argumentId, $typeHint->name);
$argumentDefinition->setPublic(false);

$this->definitions = $this->container->getDefinitions();
Copy link
Member

Choose a reason for hiding this comment

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

instead of maintaining a property $this->definitions which needs to be updated each time the container is altered to ensure it stays in sync, why not using $this->container->getDefinitions() directly when needed ?

Copy link
Member Author

Choose a reason for hiding this comment

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

It was a performance optimization but I've not tested it and it is probably not significant. I'll do it the easy way.

$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->reflectionClassesToId[$id])) {
return $this->reflectionClassesToId[$id];
Copy link
Member

Choose a reason for hiding this comment

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

the name ReflectionClassToId is weird. The key is the id, so you are going from the id to the reflection class here

Copy link
Member Author

Choose a reason for hiding this comment

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

Right I'll change that. It's garbage from an experiment to also automatically register a private service for a type when the typehint is an interface or an abstract class. It implies iterating over all declared classes to guess their types. I tried using get_declared_class() but has it returns only already autoloaded classes it doesn't work. I've not found another (not too heavy) way.

}

if (!$class = $definition->getClass()) {
return;
}

try {
return $this->reflectionClassesToId[$id] = new \ReflectionClass($class);
Copy link
Member

Choose a reason for hiding this comment

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

you need to resolve the class, in case it is defined using a parameter

} catch (\ReflectionException $e) {
// Skip invalid classes definitions to keep BC
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public function __construct()
new ResolveReferencesToAliasesPass(),
new ResolveInvalidReferencesPass(),
new AnalyzeServiceReferencesPass(true),
new AutowiringPass(),
Copy link
Member

Choose a reason for hiding this comment

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

this must be before the AnalyzeServiceReferencesPass, otherwise the changes done by the autowiring pass won't be taken into account when checking for circular references

new CheckCircularReferencesPass(),
new CheckReferenceValidityPass(),
);
Expand Down
68 changes: 68 additions & 0 deletions src/Symfony/Component/DependencyInjection/Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Definition
private $synchronized = false;
private $lazy = false;
private $decoratedService;
private $types = array();

protected $arguments;

Expand Down Expand Up @@ -818,4 +819,71 @@ public function getConfigurator()
{
return $this->configurator;
}

/**
* Sets types that will default to this definition.
*
* @param string[] $types
*
* @return Definition The current instance
*/
public function setTypes(array $types)
Copy link
Member

Choose a reason for hiding this comment

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

I would have named it setAutowiringTypes() to be consistent with the other methods.

{
$this->types = array();

foreach ($types as $type) {
$this->types[$type] = true;
}

return $this;
}

/**
* Gets types that will default to this definition.
*
* @return string[]
*/
public function getTypes()
{
return array_keys($this->types);
}

/**
* Adds a type that will default to this definition.
*
* @param string $type
*
* @return Definition The current instance
*/
public function addType($type)
{
$this->types[$type] = true;

return $this;
}

/**
* Removes a type.
*
* @param string $type
*
* @return Definition The current instance
*/
public function removeType($type)
{
unset($this->types[$type]);

return $this;
}

/**
* Will this definition default for the given type?
*
* @param string $type
*
* @return bool
*/
public function hasType($type) {
Copy link
Contributor

Choose a reason for hiding this comment

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

cs

return isset($this->types[$type]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ private function parseDefinition(\DOMElement $service, $file)
$definition->addTag($tag->getAttribute('name'), $parameters);
}

foreach ($this->getChildren($service, 'type') as $type) {
$definition->addType($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;
Expand Down
< C2EE /deferred-diff-lines>
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,20 @@ private function parseDefinition($id, $service, $file)
$definition->setDecoratedService($service['decorates'], $renameId, $priority);
}

if (isset($service['types'])) {
if (!is_array($service['types'])) {
throw new InvalidArgumentException(sprintf('Parameter "types" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file));
}

foreach ($service['types'] as $type) {
if (!is_string($type)) {
throw new InvalidArgumentException(sprintf('A "types" attribute must be of type string for service "%s" in %s. Check your YAML syntax.', $id, $file));
}

$definition->addType($type);
}
}

$this->container->setDefinition($id, $definition);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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="type" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="class" type="xsd:string" />
Expand Down
Loading
0