namespace Symfony\Component\DependencyInjection\Compiler;
+use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\DependencyInjection\Config\AutowireServiceResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
-use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
+use Symfony\Component\DependencyInjection\TypedReference;
/**
- * Guesses constructor arguments of services definitions and try to instantiate services if necessary.
+ * 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 implements CompilerPassInterface
+class AutowirePass extends AbstractRecursivePass
{
- private $container;
- private $reflectionClasses = array();
private $definedTypes = array();
private $types;
- private $ambiguousServiceTypes = array();
+ private $ambiguousServiceTypes;
private $autowired = array();
+ private $lastFailure;
+ private $throwOnAutowiringException;
+ private $autowiringExceptions = array();
+ private $strictMode;
+
+ /**
+ * @param bool $throwOnAutowireException Errors can be retrieved via Definition::getErrors()
+ */
+ public function __construct($throwOnAutowireException = true)
+ {
+ $this->throwOnAutowiringException = $throwOnAutowireException;
+ }
+
+ /**
+ * @deprecated since version 3.4, to be removed in 4.0.
+ *
+ * @return AutowiringFailedException[]
+ */
+ public function getAutowiringExceptions()
+ {
+ @trigger_error('Calling AutowirePass::getAutowiringExceptions() is deprecated since Symfony 3.4 and will be removed in 4.0. Use Definition::getErrors() instead.', E_USER_DEPRECATED);
+
+ return $this->autowiringExceptions;
+ }
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
- $throwingAutoloader = function ($class) { throw new \ReflectionException(sprintf('Class %s does not exist', $class)); };
- spl_autoload_register($throwingAutoloader);
+ // clear out any possibly stored exceptions from before
+ $this->autowiringExceptions = array();
+ $this->strictMode = $container->hasParameter('container.autowiring.strict_mode') && $container->getParameter('container.autowiring.strict_mode');
try {
- $this->container = $container;
- foreach ($container->getDefinitions() as $id => $definition) {
- if ($definition->isAutowired()) {
- $this->completeDefinition($id, $definition);
- }
- }
+ parent::process($container);
} finally {
- spl_autoload_unregister($throwingAutoloader);
-
- // Free memory and remove circular reference to container
- $this->reflectionClasses = array();
$this->definedTypes = array();
$this->types = null;
- $this->ambiguousServiceTypes = array();
+ $this->ambiguousServiceTypes = null;
$this->autowired = array();
}
}
* @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)
{
- $metadata = array();
+ @trigger_error('The '.__METHOD__.'() method is deprecated since Symfony 3.3 and will be removed in 4.0. Use ContainerBuilder::getReflectionClass() instead.', E_USER_DEPRECATED);
- if ($constructor = $reflectionClass->getConstructor()) {
- $metadata['__construct'] = self::getResourceMetadataForMethod($constructor);
- }
+ $metadata = array();
- 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);
}
/**
- * Wires the given definition.
- *
- * @param string $id
- * @param Definition $definition
- *
- * @throws RuntimeException
+ * {@inheritdoc}
*/
- private function completeDefinition($id, Definition $definition)
+ protected function processValue($value, $isRoot = false)
+ {
+ try {
+ return $this->doProcessValue($value, $isRoot);
+ } catch (AutowiringFailedException $e) {
+ if ($this->throwOnAutowiringException) {
+ throw $e;
+ }
+
+ $this->autowiringExceptions[] = $e;
+ $this->container->getDefinition($this->currentId)->addError($e->getMessage());
+
+ return parent::processValue($value, $isRoot);
+ }
+ }
+
+ private function doProcessValue($value, $isRoot = false)
{
- if ($definition->getFactory()) {
- throw new RuntimeException(sprintf('Service "%s" can use either autowiring or a factory, not both.', $id));
+ if ($value instanceof TypedReference) {
+ if ($ref = $this->getAutowiredReference($value, $value->getRequiringClass() ? sprintf('for "%s" in "%s"', $value->getType(), $value->getRequiringClass()) : '')) {
+ return $ref;
+ }
+ $this->container->log($this, $this->createTypeNotFoundMessage($value, 'it'));
}
+ $value = parent::processValue($value, $isRoot);
- if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
- return;
+ if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
+ return $value;
}
+ if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
+ $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));
- if ($this->container->isTrackingResources()) {
- $this->container->addResource(static::createResourceForClass($reflectionClass));
+ return $value;
}
- if (!$constructor = $reflectionClass->getConstructor()) {
- return;
+ $methodCalls = $value->getMethodCalls();
+
+ try {
+ $constructor = $this->getConstructor($value, false);
+ } catch (RuntimeException $e) {
+ throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e);
+ }
+
+ if ($constructor) {
+ array_unshift($methodCalls, array($constructor, $value->getArguments()));
}
- $parameters = $constructor->getParameters();
- if (method_exists('ReflectionMethod', 'isVariadic') && $constructor->isVariadic()) {
+
+ $methodCalls = $this->autowireCalls($reflectionClass, $methodCalls);
+
+ if ($constructor) {
+ list(, $arguments) = array_shift($methodCalls);
+
+ if ($arguments !== $value->getArguments()) {
+ $value->setArguments($arguments);
+ }
+ }
+
+ if ($methodCalls !== $value->getMethodCalls()) {
+ $value->setMethodCalls($methodCalls);
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param \ReflectionClass $reflectionClass
+ * @param array $methodCalls
+ *
+ * @return array
+ */
+ private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls)
+ {
+ foreach ($methodCalls as $i => $call) {
+ list($method, $arguments) = $call;
+
+ if ($method instanceof \ReflectionFunctionAbstract) {
+ $reflectionMethod = $method;
+ } else {
+ $reflectionMethod = $this->getReflectionMethod(new Definition($reflectionClass->name), $method);
+ }
+
+ $arguments = $this->autowireMethod($reflectionMethod, $arguments);
+
+ if ($arguments !== $call[1]) {
+ $methodCalls[$i][1] = $arguments;
+ }
+ }
+
+ return $methodCalls;
+ }
+
+ /**
+ * Autowires the constructor or a method.
+ *
+ * @param \ReflectionFunctionAbstract $reflectionMethod
+ * @param array $arguments
+ *
+ * @return array The autowired arguments
+ *
+ * @throws AutowiringFailedException
+ */
+ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments)
+ {
+ $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
+ $method = $reflectionMethod->name;
+ $parameters = $reflectionMethod->getParameters();
+ if (method_exists('ReflectionMethod', 'isVariadic') && $reflectionMethod->isVariadic()) {
array_pop($parameters);
}
- $arguments = $definition->getArguments();
foreach ($parameters as $index => $parameter) {
if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
continue;
}
- try {
- if (!$typeHint = $parameter->getClass()) {
- if (isset($arguments[$index])) {
- continue;
- }
-
- // 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));
- }
-
- // specifically pass the default value
- $arguments[$index] = $parameter->getDefaultValue();
+ $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
+ if (!$type) {
+ if (isset($arguments[$index])) {
continue;
}
- if (isset($this->autowired[$typeHint->name])) {
- $arguments[$index] = $this->autowired[$typeHint->name] ? new Reference($this->autowired[$typeHint->name]) : null;
- continue;
- }
+ // no default value? Then fail
+ if (!$parameter->isDefaultValueAvailable()) {
+ // For core classes, isDefaultValueAvailable() can
+ // be false when isOptional() returns true. If the
+ // argument *is* optional, allow it to be missing
+ if ($parameter->isOptional()) {
+ continue;
+ }
+ $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, false);
+ $type = $type ? sprintf('is type-hinted "%s"', $type) : 'has no type-hint';
- if (null === $this->types) {
- $this->populateAvailableTypes();
+ throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type));
}
- if (isset($this->types[$typeHint->name])) {
- $value = new Reference($this->types[$typeHint->name]);
- } else {
- try {
- $value = $this->createAutowiredDefinition($typeHint, $id);
- } catch (RuntimeException $e) {
- if ($parameter->isDefaultValueAvailable()) {
- $value = $parameter->getDefaultValue();
- } elseif ($parameter->allowsNull()) {
- $value = null;
- } else {
- throw $e;
- }
- $this->autowired[$typeHint->name] = false;
- }
- }
- } catch (\ReflectionException $e) {
- // Typehint against a non-existing class
+ // specifically pass the default value
+ $arguments[$index] = $parameter->getDefaultValue();
- 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);
- }
+ continue;
+ }
+
+ if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$method))) {
+ $failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
- $value = $parameter->getDefaultValue();
+ if ($parameter->isDefaultValueAvailable()) {
+ $value = $parameter->getDefaultValue();
+ } elseif (!$parameter->allowsNull()) {
+ throw new AutowiringFailedException($this->currentId, $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);
- $definition->setArguments($arguments);
+
+ return $arguments;
+ }
+
+ /**
+ * @return TypedReference|null A reference to the service matching the given type, if any
+ */
+ private function getAutowiredReference(TypedReference $reference, $deprecationMessage)
+ {
+ $this->lastFailure = null;
+ $type = $reference->getType();
+
+ if ($type !== $this->container->normalizeId($reference) || ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract())) {
+ return $reference;
+ }
+
+ if (null === $this->types) {
+ $this->populateAvailableTypes($this->strictMode);
+ }
+
+ if (isset($this->definedTypes[$type])) {
+ return new TypedReference($this->types[$type], $type);
+ }
+
+ if (!$this->strictMode && isset($this->types[$type])) {
+ $message = 'Autowiring services based on the types they implement is deprecated since Symfony 3.3 and won\'t be supported in version 4.0.';
+ if ($aliasSuggestion = $this->getAliasesSuggestionForType($type = $reference->getType(), $deprecationMessage)) {
+ $message .= ' '.$aliasSuggestion;
+ } else {
+ $message .= sprintf(' You should %s the "%s" service to "%s" instead.', isset($this->types[$this->types[$type]]) ? 'alias' : 'rename (or alias)', $this->types[$type], $type);
+ }
+
+ @trigger_error($message, E_USER_DEPRECATED);
+
+ return new TypedReference($this->types[$type], $type);
+ }
+
+ if (!$reference->canBeAutoregistered() || isset($this->types[$type]) || isset($this->ambiguousServiceTypes[$type])) {
+ return;
+ }
+
+ if (isset($this->autowired[$type])) {
+ return $this->autowired[$type] ? new TypedReference($this->autowired[$type], $type) : null;
+ }
+
+ if (!$this->strictMode) {
+ return $this->createAutowiredDefinition($type);
+ }
}
/**
* Populates the list of available types.
*/
- private function populateAvailableTypes()
+ private function populateAvailableTypes($onlyAutowiringTypes = false)
{
$this->types = array();
+ if (!$onlyAutowiringTypes) {
+ $this->ambiguousServiceTypes = array();
+ }
foreach ($this->container->getDefinitions() as $id => $definition) {
- $this->populateAvailableType($id, $definition);
+ $this->populateAvailableType($id, $definition, $onlyAutowiringTypes);
}
}
* @param string $id
* @param Definition $definition
*/
- private function populateAvailableType($id, Definition $definition)
+ private function populateAvailableType($id, Definition $definition, $onlyAutowiringTypes)
{
// Never use abstract services
if ($definition->isAbstract()) {
return;
}
- foreach ($definition->getAutowiringTypes() as $type) {
+ foreach ($definition->getAutowiringTypes(false) as $type) {
$this->definedTypes[$type] = true;
$this->types[$type] = $id;
unset($this->ambiguousServiceTypes[$type]);
}
- if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
+ if ($onlyAutowiringTypes) {
+ return;
+ }
+
+ if (preg_match('/^\d+_[^~]++~[._a-zA-Z\d]{7}$/', $id) || $definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) {
return;
}
/**
* Registers a definition for the type if possible or throws an exception.
*
- * @param \ReflectionClass $typeHint
- * @param string $id
- *
- * @return Reference A reference to the registered definition
+ * @param string $type
*
- * @throws RuntimeException
+ * @return TypedReference|null A reference to the registered definition
*/
- private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
+ private function createAutowiredDefinition($type)
{
- if (isset($this->ambiguousServiceTypes[$typeHint->name])) {
- $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));
- }
-
- if (!$typeHint->isInstantiable()) {
- $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
- throw new RuntimeException(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));
+ if (!($typeHint = $this->container->getReflectionClass($type, false)) || !$typeHint->isInstantiable()) {
+ return;
}
- $this->autowired[$typeHint->name] = $argumentId = sprintf('autowired.%s', $typeHint->name);
-
- $argumentDefinition = $this->container->register($argumentId, $typeHint->name);
+ $currentId = $this->currentId;
+ $this->currentId = $type;
+ $this->autowired[$type] = $argumentId = sprintf('autowired.%s', $type);
+ $argumentDefinition = new Definition($type);
$argumentDefinition->setPublic(false);
+ $argumentDefinition->setAutowired(true);
try {
- $this->completeDefinition($argumentId, $argumentDefinition);
- } 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);
- throw new RuntimeException($message, 0, $e);
+ $originalThrowSetting = $this->throwOnAutowiringException;
+ $this->throwOnAutowiringException = true;
+ $this->processValue($argumentDefinition, true);
+ $this->container->setDefinition($argumentId, $argumentDefinition);
+ } catch (AutowiringFailedException $e) {
+ $this->autowired[$type] = false;
+ $this->lastFailure = $e->getMessage();
+ $this->container->log($this, $this->lastFailure);
+
+ return;
+ } finally {
+ $this->throwOnAutowiringException = $originalThrowSetting;
+ $this->currentId = $currentId;
}
- return new Reference($argumentId);
+ @trigger_error(sprintf('Relying on service auto-registration for type "%s" is deprecated since Symfony 3.4 and won\'t be supported in 4.0. Create a service named "%s" instead.', $type, $type), E_USER_DEPRECATED);
+
+ $this->container->log($this, sprintf('Type "%s" has been auto-registered for service "%s".', $type, $this->currentId));
+
+ return new TypedReference($argumentId, $type);
}
- /**
- * Retrieves the reflection class associated with the given service.
- *
- * @param string $id
- * @param Definition $definition
- *
- * @return \ReflectionClass|false
- */
- private function getReflectionClass($id, Definition $definition)
+ private function createTypeNotFoundMessage(TypedReference $reference, $label)
{
- if (isset($this->reflectionClasses[$id])) {
- return $this->reflectionClasses[$id];
- }
+ if (!$r = $this->container->getReflectionClass($type = $reference->getType(), false)) {
+ // either $type does not exist or a parent class does not exist
+ try {
+ $resource = new ClassExistenceResource($type, false);
+ // isFresh() will explode ONLY if a parent class/trait does not exist
+ $resource->isFresh(0);
+ $parentMsg = false;
+ } catch (\ReflectionException $e) {
+ $parentMsg = $e->getMessage();
+ }
+
+ $message = sprintf('has type "%s" but this class %s.', $type, $parentMsg ? sprintf('is missing a parent class (%s)', $parentMsg) : 'was not found');
+ } else {
+ $alternatives = $this->createTypeAlternatives($reference);
+ $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
+ $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives);
- // Cannot use reflection if the class isn't set
- if (!$class = $definition->getClass()) {
- return false;
+ if ($r->isInterface() && !$alternatives) {
+ $message .= ' Did you create a class that implements this interface?';
+ }
}
- $class = $this->container->getParameterBag()->resolveValue($class);
+ $message = sprintf('Cannot autowire service "%s": %s %s', $this->currentId, $label, $message);
- if ($deprecated = $definition->isDeprecated()) {
- $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) {
- return (E_USER_DEPRECATED === $level || !$prevErrorHandler) ? false : $prevErrorHandler($level, $message, $file, $line);
- });
+ if (null !== $this->lastFailure) {
+ $message = $this->lastFailure."\n".$message;
+ $this->lastFailure = null;
}
- $e = null;
+ return $message;
+ }
- try {
- $reflector = new \ReflectionClass($class);
- } catch (\Exception $e) {
- } catch (\Throwable $e) {
+ private function createTypeAlternatives(TypedReference $reference)
+ {
+ // try suggesting available aliases first
+ if ($message = $this->getAliasesSuggestionForType($type = $reference->getType())) {
+ return ' '.$message;
}
-
- if ($deprecated) {
- restore_error_handler();
+ if (null === $this->ambiguousServiceTypes) {
+ $this->populateAvailableTypes();
}
- if (null !== $e) {
- if (!$e instanceof \ReflectionException) {
- throw $e;
- }
- $reflector = false;
+ 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]);
+ } elseif ($reference->getRequiringClass() && !$reference->canBeAutoregistered() && !$this->strictMode) {
+ return ' It cannot be auto-registered because it is from a different root namespace.';
+ } else {
+ return;
}
- return $this->reflectionClasses[$id] = $reflector;
+ return sprintf(' You should maybe alias this %s to %s.', class_exists($type, false) ? 'class' : 'interface', $message);
}
/**
- * @param \ReflectionClass $reflectionClass
- *
- * @return \ReflectionMethod[]
+ * @deprecated since version 3.3, to be removed in 4.0.
*/
- 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();
return $methodArgumentsMetadata;
}
+
+ private function getAliasesSuggestionForType($type, $extraContext = null)
+ {
+ $aliases = array();
+ foreach (class_parents($type) + class_implements($type) as $parent) {
+ if ($this->container->has($parent) && !$this->container->findDefinition($parent)->isAbstract()) {
+ $aliases[] = $parent;
+ }
+ }
+
+ $extraContext = $extraContext ? ' '.$extraContext : '';
+ if (1 < $len = count($aliases)) {
+ $message = sprintf('Try changing the type-hint%s to one of its parents: ', $extraContext);
+ for ($i = 0, --$len; $i < $len; ++$i) {
+ $message .= sprintf('%s "%s", ', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
+ }
+ $message .= sprintf('or %s "%s".', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
+
+ return $message;
+ }
+
+ if ($aliases) {
+ return sprintf('Try changing the type-hint%s to "%s" instead.', $extraContext, $aliases[0]);
+ }
+ }
}