-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] Make method (setter) autowiring configurable #20167
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 all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,9 @@ | |
*/ | ||
class AutowirePass implements CompilerPassInterface | ||
{ | ||
/** | ||
* @var ContainerBuilder | ||
*/ | ||
private $container; | ||
private $reflectionClasses = array(); | ||
private $definedTypes = array(); | ||
|
@@ -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 { | ||
|
@@ -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) { | ||
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. @weaverryan can you take a look at this change? Because it's now possible to autowire any method (not just only setters), I think that there is now other solution than watching all defined methods. 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. What about allowing masks? (eg 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. We can indeed allow masks in the configuration, but it's not related to this code block (it is not aware of the configuration so it needs to watch all methods anyway). |
||
if (!$reflectionMethod->isStatic()) { | ||
$metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod); | ||
} | ||
} | ||
|
||
return new AutowireServiceResource($reflectionClass->name, $reflectionClass->getFileName(), $metadata); | ||
|
@@ -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; | ||
|
@@ -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(); | ||
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. not required 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 is, otherwise the array may be uninitialized (if the class has no method for instance). |
||
$regexList = array(); | ||
foreach ($autowiredMethods as $pattern) { | ||
$regexList[] = '/^'.str_replace('\*', '.*', preg_quote($pattern, '/')).'$/i'; | ||
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. Perhaps use 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 say 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.
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. Should be consider allowing some other basic glob patterns, ie. 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. No need IMHO |
||
} | ||
|
||
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 { | ||
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. What about removing the $arguments = array();
if ($isConstructor = $reflectionMethod->isConstructor()) {
$arguments = $definition->getArguments();
} 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 adds an extra assignation. Not sure it worth it. 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. As you want, that's just a detail after all ☺ |
||
$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; | ||
} | ||
|
@@ -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)); | ||
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. What about reversing the if (!$isConstructor) {
return;
}
// ... 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 I prefer using non-negative condition when possible (and it's possible here). |
||
} | ||
|
||
return; | ||
} | ||
|
||
// specifically pass the default value | ||
|
@@ -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) { | ||
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. Can we use a specific getter/setter instead of a magic 1 value for the code? 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. Done (but I find the use of a specific code less intrusive than using accessors, here we add methods to a generic exception for an implementation detail of the |
||
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(); | ||
|
@@ -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); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -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()) { | ||
|
@@ -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); | ||
|
@@ -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(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -662,19 +662,50 @@ public function setAutowiringTypes(array $types) | |
*/ | ||
public function isAutowired() | ||
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. Should I deprecate 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. Not sure it's worth it. |
||
{ | ||
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) | ||
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. Regarding my previous comment, 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. Having another method name also means that we can keep the current one as is, deprecate it, and remove it in 4.0 easily. 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. @fabpot should it not support a boolean value? I like at least Why not just 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 don't have any issue with 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. 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. Final thoughts; my vote goes to return [
'services' => [
'id' => ['class' => '..', 'autowire' => true],
],
]; whereas others do (YAML, XML). 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 one could be deprecated, right? 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 propose to deprecate it in another PR (if it's worth it) because we need to determine how we deal with #20648 |
||
{ | ||
$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; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -239,6 +239,19 @@ private function parseDefinition(\DOMElement $service, $file) | |
$definition->addAutowiringType($type->textContent); | ||
} | ||
|
||
$autowireTags = array(); | ||
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.
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 prefer to keep the current name here because this var contains the value of the XML tag. |
||
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; | ||
|
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.
really required? isn't the type hint on the constructor enough?
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.
Without it PHPStorm's autocompletion doesn't work, but I can remove it :)
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.
There is no constructor btw (it's why it's required).
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.
oh right, the function I looked at is
process
ok then