Skip to content

Commit

Permalink
feature #20167 [DependencyInjection] Make method (setter) autowiring …
Browse files Browse the repository at this point in the history
…configurable (dunglas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DependencyInjection] Make method (setter) autowiring configurable

| Q | A |
| --- | --- |
| Branch? | master |
| Bug fix? | no |
| New feature? | yes |
| BC breaks? | no |
| Deprecations? | maybe? |
| Tests pass? | yes |
| Fixed tickets | #19631 |
| License | MIT |
| Doc PR | symfony/symfony-docs#7041 |

Follow up of #19631. Implements #19631 (comment):

Edit: the last supported format:

``` yaml
services:
    foo:
        class: Foo
        autowire: ['__construct', 'set*'] # Autowire constructor and all setters
        autowire: true # Converted by loaders in `autowire: ['__construct']` for BC
        autowire: ['foo', 'bar'] # Autowire only `foo` and `bar` methods
```

Outdated:

``` yaml
autowire: true # constructor autowiring
autowire: [__construct, setFoo, setBar] # autowire whitelisted methods only
autowire: '*' # autowire constructor + every setters (following existing rules for setters autowiring)
```
- [x] Allow to specify the list of methods in the XML loader
- [x] Add tests for the YAML loader

Commits
-------

6dd53c7 [DependencyInjection] Introduce method injection for autowiring
  • Loading branch information
fabpot committed Dec 13, 2016
2 parents a3577eb + 6dd53c7 commit 69dcf41
Show file tree
Hide file tree
Showing 13 changed files with 375 additions and 33 deletions.
133 changes: 104 additions & 29 deletions src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php
Expand Up @@ -24,6 +24,9 @@
*/
class AutowirePass implements CompilerPassInterface
{
/**
* @var ContainerBuilder
*/
private $container;
private $reflectionClasses = array();
private $definedTypes = array();
Expand All @@ -41,8 +44,8 @@ public function process(ContainerBuilder $container)
try {
$this->container = $container;
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isAutowired()) {
$this->completeDefinition($id, $definition);
if ($autowiredMethods = $definition->getAutowiredMethods()) {
$this->completeDefinition($id, $definition, $autowiredMethods);
}
}
} finally {
Expand Down Expand Up @@ -72,8 +75,10 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass)
$metadata['__construct'] = self::getResourceMetadataForMethod($constructor);
}

foreach (self::getSetters($reflectionClass) as $reflectionMethod) {
$metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod);
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
if (!$reflectionMethod->isStatic()) {
$metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod);
}
}

return new AutowireServiceResource($reflectionClass->name, $reflectionClass->getFileName(), $metadata);
Expand All @@ -84,10 +89,11 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass)
*
* @param string $id
* @param Definition $definition
* @param string[] $autowiredMethods
*
* @throws RuntimeException
*/
private function completeDefinition($id, Definition $definition)
private function completeDefinition($id, Definition $definition, array $autowiredMethods)
{
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
return;
Expand All @@ -97,12 +103,75 @@ private function completeDefinition($id, Definition $definition)
$this->container->addResource(static::createResourceForClass($reflectionClass));
}

if (!$constructor = $reflectionClass->getConstructor()) {
return;
$methodsCalled = array();
foreach ($definition->getMethodCalls() as $methodCall) {
$methodsCalled[$methodCall[0]] = true;
}

$arguments = $definition->getArguments();
foreach ($constructor->getParameters() as $index => $parameter) {
foreach ($this->getMethodsToAutowire($id, $reflectionClass, $autowiredMethods) as $reflectionMethod) {
if (!isset($methodsCalled[$reflectionMethod->name])) {
$this->autowireMethod($id, $definition, $reflectionMethod);
}
}
}

/**
* Gets the list of methods to autowire.
*
* @param string $id
* @param \ReflectionClass $reflectionClass
* @param string[] $autowiredMethods
*
* @return \ReflectionMethod[]
*/
private function getMethodsToAutowire($id, \ReflectionClass $reflectionClass, array $autowiredMethods)
{
$found = array();
$regexList = array();
foreach ($autowiredMethods as $pattern) {
$regexList[] = '/^'.str_replace('\*', '.*', preg_quote($pattern, '/')).'$/i';
}

foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
if ($reflectionMethod->isStatic()) {
continue;
}

foreach ($regexList as $k => $regex) {
if (preg_match($regex, $reflectionMethod->name)) {
$found[] = $autowiredMethods[$k];
yield $reflectionMethod;

continue 2;
}
}
}

if ($notFound = array_diff($autowiredMethods, $found)) {
$compiler = $this->container->getCompiler();
$compiler->addLogMessage($compiler->getLoggingFormatter()->formatUnusedAutowiringPatterns($this, $id, $notFound));
}
}

/**
* Autowires the constructor or a setter.
*
* @param string $id
* @param Definition $definition
* @param \ReflectionMethod $reflectionMethod
*
* @throws RuntimeException
*/
private function autowireMethod($id, Definition $definition, \ReflectionMethod $reflectionMethod)
{
if ($isConstructor = $reflectionMethod->isConstructor()) {
$arguments = $definition->getArguments();
} else {
$arguments = array();
}

$addMethodCall = false; // Whether the method should be added to the definition as a call or as arguments
foreach ($reflectionMethod->getParameters() as $index => $parameter) {
if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
continue;
}
Expand All @@ -111,7 +180,11 @@ private function completeDefinition($id, Definition $definition)
if (!$typeHint = $parameter->getClass()) {
// no default value? Then fail
if (!$parameter->isOptional()) {
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id));
if ($isConstructor) {
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id));
}

return;
}

// specifically pass the default value
Expand All @@ -126,24 +199,35 @@ private function completeDefinition($id, Definition $definition)

if (isset($this->types[$typeHint->name])) {
$value = new Reference($this->types[$typeHint->name]);
$addMethodCall = true;
} else {
try {
$value = $this->createAutowiredDefinition($typeHint, $id);
$addMethodCall = true;
} catch (RuntimeException $e) {
if ($parameter->allowsNull()) {
$value = null;
} elseif ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue();
} else {
throw $e;
// The exception code is set to 1 if the exception must be thrown even if it's a setter
if (1 === $e->getCode() || $isConstructor) {
throw $e;
}

return;
}
}
}
} catch (\ReflectionException $e) {
// Typehint against a non-existing class

if (!$parameter->isDefaultValueAvailable()) {
throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e);
if ($isConstructor) {
throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e);
}

return;
}

$value = $parameter->getDefaultValue();
Expand All @@ -155,7 +239,12 @@ private function completeDefinition($id, Definition $definition)
// it's possible index 1 was set, then index 0, then 2, etc
// make sure that we re-order so they're injected as expected
ksort($arguments);
$definition->setArguments($arguments);

if ($isConstructor) {
$definition->setArguments($arguments);
} elseif ($addMethodCall) {
$definition->addMethodCall($reflectionMethod->name, $arguments);
}
}

/**
Expand Down Expand Up @@ -253,7 +342,7 @@ private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
$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));
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()) {
Expand All @@ -269,7 +358,7 @@ private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
$this->populateAvailableType($argumentId, $argumentDefinition);

try {
$this->completeDefinition($argumentId, $argumentDefinition);
$this->completeDefinition($argumentId, $argumentDefinition, array('__construct'));
} catch (RuntimeException $e) {
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
$message = 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 Down Expand Up @@ -320,20 +409,6 @@ private function addServiceToAmbiguousType($id, $type)
$this->ambiguousServiceTypes[$type][] = $id;
}

/**
* @param \ReflectionClass $reflectionClass
*
* @return \ReflectionMethod[]
*/
private static function getSetters(\ReflectionClass $reflectionClass)
{
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
if (!$reflectionMethod->isStatic() && 1 === $reflectionMethod->getNumberOfParameters() && 0 === strpos($reflectionMethod->name, 'set')) {
yield $reflectionMethod;
}
}
}

private static function getResourceMetadataForMethod(\ReflectionMethod $method)
{
$methodArgumentsMetadata = array();
Expand Down
Expand Up @@ -38,6 +38,11 @@ public function formatResolveInheritance(CompilerPassInterface $pass, $childId,
return $this->format($pass, sprintf('Resolving inheritance for "%s" (parent: %s).', $childId, $parentId));
}

public function formatUnusedAutowiringPatterns(CompilerPassInterface $pass, $id, array $patterns)
{
return $this->format($pass, sprintf('Autowiring\'s patterns "%s" for service "%s" don\'t match any method.', implode('", "', $patterns), $id));
}

public function format(CompilerPassInterface $pass, $message)
{
return sprintf('%s: %s', get_class($pass), $message);
Expand Down
37 changes: 34 additions & 3 deletions src/Symfony/Component/DependencyInjection/Definition.php
Expand Up @@ -36,7 +36,7 @@ class Definition
private $abstract = false;
private $lazy = false;
private $decoratedService;
private $autowired = false;
private $autowiredMethods = array();
private $autowiringTypes = array();

protected $arguments;
Expand Down Expand Up @@ -662,19 +662,50 @@ public function setAutowiringTypes(array $types)
*/
public function isAutowired()
{
return $this->autowired;
return !empty($this->autowiredMethods);
}

/**
* Gets autowired methods.
*
* @return string[]
*/
public function getAutowiredMethods()
{
return $this->autowiredMethods;
}

/**
* Sets autowired.
*
* Allowed values:
* - true: constructor autowiring, same as $this->setAutowiredMethods(array('__construct'))
* - false: no autowiring, same as $this->setAutowiredMethods(array())
*
* @param bool $autowired
*
* @return Definition The current instance
*/
public function setAutowired($autowired)
{
$this->autowired = $autowired;
$this->autowiredMethods = $autowired ? array('__construct') : array();

return $this;
}

/**
* Sets autowired methods.
*
* Example of allowed value:
* - array('__construct', 'set*', 'initialize'): autowire whitelisted methods only
*
* @param string[] $autowiredMethods
*
* @return Definition The current instance
*/
public function setAutowiredMethods(array $autowiredMethods)
{
$this->autowiredMethods = $autowiredMethods;

return $this;
}
Expand Down
13 changes: 13 additions & 0 deletions src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
Expand Up @@ -239,6 +239,19 @@ private function parseDefinition(\DOMElement $service, $file)
$definition->addAutowiringType($type->textContent);
}

$autowireTags = array();
foreach ($this->getChildren($service, 'autowire') as $type) {
$autowireTags[] = $type->textContent;
}

if ($autowireTags) {
if ($service->hasAttribute('autowire')) {
throw new InvalidArgumentException(sprintf('The "autowire" attribute cannot be used together with "<autowire>" tags for service "%s" in %s.', (string) $service->getAttribute('id'), $file));
}

$definition->setAutowiredMethods($autowireTags);
}

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
Expand Up @@ -302,7 +302,11 @@ private function parseDefinition($id, $service, $file)
}

if (isset($service['autowire'])) {
$definition->setAutowired($service['autowire']);
if (is_array($service['autowire'])) {
$definition->setAutowiredMethods($service['autowire']);
} else {
$definition->setAutowired($service['autowire']);
}
}

if (isset($service['autowiring_types'])) {
Expand Down
Expand Up @@ -100,6 +100,7 @@
<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:element name="autowire" 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

0 comments on commit 69dcf41

Please sign in to comment.