-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Changes from 1 commit
9828da2
195f6c0
8d23c9d
27f4de8
5575ec1
edce23d
07d475e
afe009a
7ab5b08
a7ca3f2
57c494c
e642aa8
d00c7e3
55bb42c
7806487
585616d
c717c69
3b7f553
936a16a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
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; | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you're passing the container as an argument of this method... |
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why aren't you using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I'll take a look at that. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... but the container is not part of the signature There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch (garbage from a previous version). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The array returned by |
||
if (!($typeHint = $parameter->getClass()) || $parameter->isOptional()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if ($argumentExist && '' !== $arguments[$index]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why treating empty strings in a special way ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | FAB3||
} | ||
} | ||
|
||
/** | ||
* 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
* | ||
* @param string $id | ||
* @param Definition $definition | ||
*/ | ||
private function populateAvailableType($id, Definition $definition) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong CS |
||
if (!($class = $definition->getClass())) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weird description here ( |
||
*/ | ||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of maintaining a property There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
if (!$class = $definition->getClass()) { | ||
return; | ||
} | ||
|
||
try { | ||
return $this->reflectionClassesToId[$id] = new \ReflectionClass($class); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -51,6 +51,7 @@ public function __construct() | |
new ResolveReferencesToAliasesPass(), | ||
new ResolveInvalidReferencesPass(), | ||
new AnalyzeServiceReferencesPass(true), | ||
new AutowiringPass(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this must be before the |
||
new CheckCircularReferencesPass(), | ||
new CheckReferenceValidityPass(), | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,7 @@ class Definition | |
private $synchronized = false; | ||
private $lazy = false; | ||
private $decoratedService; | ||
private $types = array(); | ||
|
||
protected $arguments; | ||
|
||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would have named it |
||
{ | ||
$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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cs |
||
return isset($this->types[$type]); | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).