6cc9dc7586
This PR was merged into the 3.3-dev branch.
Discussion
----------
[DI] Add "by-id" autowiring: a side-effect free variant of it based on the class<>id convention
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
This PR adds a new autowiring mode, based only on the class <> id convention.
This way of autowiring is free from any conflicting behavior, which is what I was looking for to begin with.
The expected DX is a bit more involving than the current way we do autowiring. But it's worth it to me, because it's plain predictable - a lot less "magic" imho.
So in this mode, for each `App\Foo` type hint, a reference to an "App\Foo" service will be created. If no such service exists, an exception will be thrown. To me, this opens a nice DX: when type hinting interfaces (which is the best practice), this will tell you when you need to create the explicit interface <> id mapping that is missing - thus encourage things to be made explicit, but only when required, and gradually, in a way that will favor discoverability by devs.
Of course, this is opt-in, and BC. You'd need to do eg in yaml: `autowire: by_id`.
For consistency, the current mode (`autowire: true`) can be configured using `autowire: by_type`.
Commits
-------
c298f2a90c
[DI] Add "by-id" autowiring: a side-effect free variant of it based on the class<>id convention
538 lines
20 KiB
PHP
538 lines
20 KiB
PHP
<?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\Config\AutowireServiceResource;
|
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
use Symfony\Component\DependencyInjection\Definition;
|
|
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
|
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
|
|
use Symfony\Component\DependencyInjection\Reference;
|
|
use Symfony\Component\DependencyInjection\TypedReference;
|
|
|
|
/**
|
|
* Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
|
|
*
|
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
|
* @author Nicolas Grekas <p@tchwork.com>
|
|
*/
|
|
class AutowirePass extends AbstractRecursivePass
|
|
{
|
|
private $definedTypes = array();
|
|
private $types;
|
|
private $ambiguousServiceTypes = array();
|
|
private $usedTypes = array();
|
|
private $currentDefinition;
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function process(ContainerBuilder $container)
|
|
{
|
|
try {
|
|
parent::process($container);
|
|
|
|
foreach ($this->usedTypes as $type => $id) {
|
|
if (!isset($this->usedTypes[$type]) || !isset($this->ambiguousServiceTypes[$type])) {
|
|
continue;
|
|
}
|
|
|
|
if ($container->has($type) && !$container->findDefinition($type)->isAbstract()) {
|
|
continue;
|
|
}
|
|
|
|
$this->container = $container;
|
|
$classOrInterface = class_exists($type, false) ? 'class' : 'interface';
|
|
|
|
throw new RuntimeException(sprintf('Cannot autowire service "%s": multiple candidate services exist for %s "%s".%s', $id, $classOrInterface, $type, $this->createTypeAlternatives($type)));
|
|
}
|
|
} finally {
|
|
$this->container = null;
|
|
$this->definedTypes = array();
|
|
$this->types = null;
|
|
$this->ambiguousServiceTypes = array();
|
|
$this->usedTypes = array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a resource to help know if this service has changed.
|
|
*
|
|
* @param \ReflectionClass $reflectionClass
|
|
*
|
|
* @return AutowireServiceResource
|
|
*
|
|
* @deprecated since version 3.3, to be removed in 4.0. Use ContainerBuilder::getReflectionClass() instead.
|
|
*/
|
|
public static function createResourceForClass(\ReflectionClass $reflectionClass)
|
|
{
|
|
@trigger_error('The '.__METHOD__.'() method is deprecated since version 3.3 and will be removed in 4.0. Use ContainerBuilder::getReflectionClass() instead.', E_USER_DEPRECATED);
|
|
|
|
$metadata = array();
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function processValue($value, $isRoot = false)
|
|
{
|
|
if ($value instanceof TypedReference && $this->currentDefinition->isAutowired() && !$this->container->has((string) $value)) {
|
|
if ($ref = $this->getAutowiredReference($value->getType(), $value->canBeAutoregistered())) {
|
|
$value = new TypedReference((string) $ref, $value->getType(), $value->getInvalidBehavior(), $value->canBeAutoregistered());
|
|
} else {
|
|
$this->container->log($this, $this->createTypeNotFoundMessage($value->getType(), 'typed reference'));
|
|
}
|
|
}
|
|
if (!$value instanceof Definition) {
|
|
return parent::processValue($value, $isRoot);
|
|
}
|
|
|
|
$parentDefinition = $this->currentDefinition;
|
|
$this->currentDefinition = $value;
|
|
|
|
try {
|
|
if (!$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
|
|
return parent::processValue($value, $isRoot);
|
|
}
|
|
if (!$reflectionClass = $this->container->getReflectionClass($value->getClass())) {
|
|
$this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" does not exist.', $this->currentId, $value->getClass()));
|
|
|
|
return parent::processValue($value, $isRoot);
|
|
}
|
|
|
|
$autowiredMethods = $this->getMethodsToAutowire($reflectionClass);
|
|
$methodCalls = $value->getMethodCalls();
|
|
|
|
if ($constructor = $reflectionClass->getConstructor()) {
|
|
array_unshift($methodCalls, array($constructor->name, $value->getArguments()));
|
|
}
|
|
|
|
$methodCalls = $this->autowireCalls($reflectionClass, $methodCalls, $autowiredMethods);
|
|
|
|
if ($constructor) {
|
|
list(, $arguments) = array_shift($methodCalls);
|
|
|
|
if ($arguments !== $value->getArguments()) {
|
|
$value->setArguments($arguments);
|
|
}
|
|
}
|
|
|
|
if ($methodCalls !== $value->getMethodCalls()) {
|
|
$value->setMethodCalls($methodCalls);
|
|
}
|
|
|
|
return parent::processValue($value, $isRoot);
|
|
} finally {
|
|
$this->currentDefinition = $parentDefinition;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the list of methods to autowire.
|
|
*
|
|
* @param \ReflectionClass $reflectionClass
|
|
*
|
|
* @return \ReflectionMethod[]
|
|
*/
|
|
private function getMethodsToAutowire(\ReflectionClass $reflectionClass)
|
|
{
|
|
$found = array();
|
|
$methodsToAutowire = array();
|
|
|
|
if ($reflectionMethod = $reflectionClass->getConstructor()) {
|
|
$methodsToAutowire[strtolower($reflectionMethod->name)] = $reflectionMethod;
|
|
}
|
|
|
|
foreach ($reflectionClass->getMethods() as $reflectionMethod) {
|
|
$r = $reflectionMethod;
|
|
|
|
while (true) {
|
|
if (false !== $doc = $r->getDocComment()) {
|
|
if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) {
|
|
$methodsToAutowire[strtolower($reflectionMethod->name)] = $reflectionMethod;
|
|
break;
|
|
}
|
|
if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) {
|
|
break;
|
|
}
|
|
}
|
|
try {
|
|
$r = $r->getPrototype();
|
|
} catch (\ReflectionException $e) {
|
|
break; // method has no prototype
|
|
}
|
|
}
|
|
}
|
|
|
|
return $methodsToAutowire;
|
|
}
|
|
|
|
/**
|
|
* @param \ReflectionClass $reflectionClass
|
|
* @param array $methodCalls
|
|
* @param \ReflectionMethod[] $autowiredMethods
|
|
*
|
|
* @return array
|
|
*/
|
|
private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls, array $autowiredMethods)
|
|
{
|
|
foreach ($methodCalls as $i => $call) {
|
|
list($method, $arguments) = $call;
|
|
|
|
if (isset($autowiredMethods[$lcMethod = strtolower($method)]) && $autowiredMethods[$lcMethod]->isPublic()) {
|
|
$reflectionMethod = $autowiredMethods[$lcMethod];
|
|
unset($autowiredMethods[$lcMethod]);
|
|
} else {
|
|
if (!$reflectionClass->hasMethod($method)) {
|
|
$class = $reflectionClass->name;
|
|
throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() does not exist.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
|
}
|
|
$reflectionMethod = $reflectionClass->getMethod($method);
|
|
if (!$reflectionMethod->isPublic()) {
|
|
$class = $reflectionClass->name;
|
|
throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
|
}
|
|
}
|
|
|
|
$arguments = $this->autowireMethod($reflectionMethod, $arguments);
|
|
|
|
if ($arguments !== $call[1]) {
|
|
$methodCalls[$i][1] = $arguments;
|
|
}
|
|
}
|
|
|
|
foreach ($autowiredMethods as $lcMethod => $reflectionMethod) {
|
|
$method = $reflectionMethod->name;
|
|
|
|
if (!$reflectionMethod->isPublic()) {
|
|
$class = $reflectionClass->name;
|
|
throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
|
}
|
|
$methodCalls[] = array($method, $this->autowireMethod($reflectionMethod, array()));
|
|
}
|
|
|
|
return $methodCalls;
|
|
}
|
|
|
|
/**
|
|
* Autowires the constructor or a method.
|
|
*
|
|
* @param \ReflectionMethod $reflectionMethod
|
|
* @param array $arguments
|
|
*
|
|
* @return array The autowired arguments
|
|
*
|
|
* @throws RuntimeException
|
|
*/
|
|
private function autowireMethod(\ReflectionMethod $reflectionMethod, array $arguments)
|
|
{
|
|
$isConstructor = $reflectionMethod->isConstructor();
|
|
$class = $reflectionMethod->class;
|
|
$method = $reflectionMethod->name;
|
|
|
|
if (!$isConstructor && !$arguments && !$reflectionMethod->getNumberOfRequiredParameters()) {
|
|
throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() has only optional arguments, thus must be wired explicitly.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
|
}
|
|
|
|
foreach ($reflectionMethod->getParameters() as $index => $parameter) {
|
|
if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
|
|
continue;
|
|
}
|
|
if (!$isConstructor && $parameter->isOptional() && !array_key_exists($index, $arguments)) {
|
|
break;
|
|
}
|
|
if (method_exists($parameter, 'isVariadic') && $parameter->isVariadic()) {
|
|
continue;
|
|
}
|
|
|
|
$type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
|
|
|
|
if (!$type) {
|
|
// no default value? Then fail
|
|
if (!$parameter->isOptional()) {
|
|
throw new RuntimeException(sprintf('Cannot autowire service "%s": argument $%s of method %s() must have a type-hint or be given a value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
|
|
}
|
|
|
|
if (!array_key_exists($index, $arguments)) {
|
|
// specifically pass the default value
|
|
$arguments[$index] = $parameter->getDefaultValue();
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($value = $this->getAutowiredReference($type)) {
|
|
$this->usedTypes[$type] = $this->currentId;
|
|
} else {
|
|
$failureMessage = $this->createTypeNotFoundMessage($type, sprintf('argument $%s of method %s()', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
|
|
|
|
if ($parameter->isDefaultValueAvailable()) {
|
|
$value = $parameter->getDefaultValue();
|
|
} elseif ($parameter->allowsNull()) {
|
|
$value = null;
|
|
} else {
|
|
throw new RuntimeException($failureMessage);
|
|
}
|
|
$this->container->log($this, $failureMessage);
|
|
}
|
|
|
|
$arguments[$index] = $value;
|
|
}
|
|
|
|
// 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);
|
|
|
|
return $arguments;
|
|
}
|
|
|
|
/**
|
|
* @return Reference|null A reference to the service matching the given type, if any
|
|
*/
|
|
private function getAutowiredReference($type, $autoRegister = true)
|
|
{
|
|
if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) {
|
|
return new Reference($type);
|
|
}
|
|
|
|
if (Definition::AUTOWIRE_BY_ID === $this->currentDefinition->getAutowired()) {
|
|
return;
|
|
}
|
|
|
|
if (null === $this->types) {
|
|
$this->populateAvailableTypes();
|
|
}
|
|
|
|
if (isset($this->types[$type])) {
|
|
$this->container->log($this, sprintf('Service "%s" matches type "%s" and has been autowired into service "%s".', $this->types[$type], $type, $this->currentId));
|
|
|
|
return new Reference($this->types[$type]);
|
|
}
|
|
|
|
if ($autoRegister && $class = $this->container->getReflectionClass($type, true)) {
|
|
return $this->createAutowiredDefinition($class);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
// Never use abstract services
|
|
if ($definition->isAbstract()) {
|
|
return;
|
|
}
|
|
|
|
foreach ($definition->getAutowiringTypes(false) as $type) {
|
|
$this->definedTypes[$type] = true;
|
|
$this->types[$type] = $id;
|
|
unset($this->ambiguousServiceTypes[$type]);
|
|
}
|
|
|
|
if (!$reflectionClass = $this->container->getReflectionClass($definition->getClass(), true)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
|
|
$this->set($reflectionInterface->name, $id);
|
|
}
|
|
|
|
do {
|
|
$this->set($reflectionClass->name, $id);
|
|
} while ($reflectionClass = $reflectionClass->getParentClass());
|
|
}
|
|
|
|
/**
|
|
* 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])) {
|
|
return;
|
|
}
|
|
|
|
// is this already a type/class that is known to match multiple services?
|
|
if (isset($this->ambiguousServiceTypes[$type])) {
|
|
$this->ambiguousServiceTypes[$type][] = $id;
|
|
|
|
return;
|
|
}
|
|
|
|
// check to make sure the type doesn't match multiple services
|
|
if (!isset($this->types[$type]) || $this->types[$type] === $id) {
|
|
$this->types[$type] = $id;
|
|
|
|
return;
|
|
}
|
|
|
|
// keep an array of all services matching this type
|
|
if (!isset($this->ambiguousServiceTypes[$type])) {
|
|
$this->ambiguousServiceTypes[$type] = array($this->types[$type]);
|
|
unset($this->types[$type]);
|
|
}
|
|
$this->ambiguousServiceTypes[$type][] = $id;
|
|
}
|
|
|
|
/**
|
|
* Registers a definition for the type if possible or throws an exception.
|
|
*
|
|
* @param \ReflectionClass $typeHint
|
|
*
|
|
* @return Reference|null A reference to the registered definition
|
|
*
|
|
* @throws RuntimeException
|
|
*/
|
|
private function createAutowiredDefinition(\ReflectionClass $typeHint)
|
|
{
|
|
if (isset($this->ambiguousServiceTypes[$type = $typeHint->name])) {
|
|
$classOrInterface = class_exists($type) ? 'class' : 'interface';
|
|
|
|
throw new RuntimeException(sprintf('Cannot autowire service "%s": multiple candidate services exist for %s "%s".%s', $this->currentId, $classOrInterface, $type, $this->createTypeAlternatives($type)));
|
|
}
|
|
|
|
if (!$typeHint->isInstantiable()) {
|
|
$this->container->log($this, sprintf('Type "%s" is not instantiable thus cannot be auto-registered for service "%s".', $type, $this->currentId));
|
|
|
|
return;
|
|
}
|
|
|
|
$ambiguousServiceTypes = $this->ambiguousServiceTypes;
|
|
$currentDefinition = $this->currentDefinition;
|
|
$definitions = $this->container->getDefinitions();
|
|
$currentId = $this->currentId;
|
|
$this->currentId = $argumentId = sprintf('autowired.%s', $type);
|
|
$this->currentDefinition = $argumentDefinition = new Definition($type);
|
|
$argumentDefinition->setPublic(false);
|
|
$argumentDefinition->setAutowired(true);
|
|
|
|
$this->populateAvailableType($argumentId, $argumentDefinition);
|
|
|
|
try {
|
|
$this->processValue($argumentDefinition, true);
|
|
$this->container->setDefinition($argumentId, $argumentDefinition);
|
|
} catch (RuntimeException $e) {
|
|
// revert any changes done to our internal state
|
|
unset($this->types[$type]);
|
|
$this->ambiguousServiceTypes = $ambiguousServiceTypes;
|
|
$this->container->setDefinitions($definitions);
|
|
$this->container->log($this, $e->getMessage());
|
|
|
|
return;
|
|
} finally {
|
|
$this->currentId = $currentId;
|
|
$this->currentDefinition = $currentDefinition;
|
|
}
|
|
|
|
$this->container->log($this, sprintf('Type "%s" has been auto-registered for service "%s".', $type, $this->currentId));
|
|
|
|
return new Reference($argumentId);
|
|
}
|
|
|
|
private function createTypeNotFoundMessage($type, $label)
|
|
{
|
|
$autowireById = Definition::AUTOWIRE_BY_ID === $this->currentDefinition->getAutowired();
|
|
if (!$classOrInterface = class_exists($type, $autowireById) ? 'class' : (interface_exists($type, false) ? 'interface' : null)) {
|
|
return sprintf('Cannot autowire service "%s": %s has type "%s" but this class does not exist.', $this->currentId, $label, $type);
|
|
}
|
|
if (null === $this->types) {
|
|
$this->populateAvailableTypes();
|
|
}
|
|
if ($autowireById) {
|
|
$message = sprintf('%s references %s "%s" but no such service exists.%s', $label, $classOrInterface, $type, $this->createTypeAlternatives($type));
|
|
} else {
|
|
$message = sprintf('no services were found matching the "%s" %s and it cannot be auto-registered for %s.', $type, $classOrInterface, $label);
|
|
}
|
|
|
|
return sprintf('Cannot autowire service "%s": %s', $this->currentId, $message);
|
|
}
|
|
|
|
private function createTypeAlternatives($type)
|
|
{
|
|
$message = ' This type-hint could be aliased to ';
|
|
|
|
if (isset($this->ambiguousServiceTypes[$type])) {
|
|
$message .= sprintf('one of these existing services: "%s"', implode('", "', $this->ambiguousServiceTypes[$type]));
|
|
} elseif (isset($this->types[$type])) {
|
|
$message .= sprintf('the existing "%s" service', $this->types[$type]);
|
|
} else {
|
|
return;
|
|
}
|
|
$aliases = array();
|
|
|
|
foreach (class_parents($type) + class_implements($type) as $parent) {
|
|
if ($this->container->has($parent)) {
|
|
$aliases[] = $parent;
|
|
}
|
|
}
|
|
|
|
if (1 < count($aliases)) {
|
|
$message .= sprintf('; or be updated to one of the following: "%s"', implode('", "', $aliases));
|
|
} elseif ($aliases) {
|
|
$message .= sprintf('; or be updated to "%s"', $aliases[0]);
|
|
}
|
|
|
|
return $message.'.';
|
|
}
|
|
|
|
/**
|
|
* @deprecated since version 3.3, to be removed in 4.0.
|
|
*/
|
|
private static function getResourceMetadataForMethod(\ReflectionMethod $method)
|
|
{
|
|
$methodArgumentsMetadata = array();
|
|
foreach ($method->getParameters() as $parameter) {
|
|
try {
|
|
$class = $parameter->getClass();
|
|
} catch (\ReflectionException $e) {
|
|
// type-hint is against a non-existent class
|
|
$class = false;
|
|
}
|
|
|
|
$isVariadic = method_exists($parameter, 'isVariadic') && $parameter->isVariadic();
|
|
$methodArgumentsMetadata[] = array(
|
|
'class' => $class,
|
|
'isOptional' => $parameter->isOptional(),
|
|
'defaultValue' => ($parameter->isOptional() && !$isVariadic) ? $parameter->getDefaultValue() : null,
|
|
);
|
|
}
|
|
|
|
return $methodArgumentsMetadata;
|
|
}
|
|
}
|