Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\DependencyInjection\Compiler; Chris@0: Chris@0: use Symfony\Component\DependencyInjection\Config\AutowireServiceResource; Chris@0: use Symfony\Component\DependencyInjection\ContainerBuilder; Chris@0: use Symfony\Component\DependencyInjection\Definition; Chris@0: use Symfony\Component\DependencyInjection\Exception\RuntimeException; Chris@0: use Symfony\Component\DependencyInjection\Reference; Chris@0: Chris@0: /** Chris@0: * Guesses constructor arguments of services definitions and try to instantiate services if necessary. Chris@0: * Chris@0: * @author Kévin Dunglas Chris@0: */ Chris@0: class AutowirePass implements CompilerPassInterface Chris@0: { Chris@0: private $container; Chris@0: private $reflectionClasses = array(); Chris@0: private $definedTypes = array(); Chris@0: private $types; Chris@0: private $ambiguousServiceTypes = array(); Chris@0: private $autowired = array(); Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function process(ContainerBuilder $container) Chris@0: { Chris@0: $throwingAutoloader = function ($class) { throw new \ReflectionException(sprintf('Class %s does not exist', $class)); }; Chris@0: spl_autoload_register($throwingAutoloader); Chris@0: Chris@0: try { Chris@0: $this->container = $container; Chris@0: foreach ($container->getDefinitions() as $id => $definition) { Chris@0: if ($definition->isAutowired()) { Chris@0: $this->completeDefinition($id, $definition); Chris@0: } Chris@0: } Chris@0: } finally { Chris@0: spl_autoload_unregister($throwingAutoloader); Chris@0: Chris@0: // Free memory and remove circular reference to container Chris@0: $this->reflectionClasses = array(); Chris@0: $this->definedTypes = array(); Chris@0: $this->types = null; Chris@0: $this->ambiguousServiceTypes = array(); Chris@0: $this->autowired = array(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Creates a resource to help know if this service has changed. Chris@0: * Chris@0: * @param \ReflectionClass $reflectionClass Chris@0: * Chris@0: * @return AutowireServiceResource Chris@0: */ Chris@0: public static function createResourceForClass(\ReflectionClass $reflectionClass) Chris@0: { Chris@0: $metadata = array(); Chris@0: Chris@0: if ($constructor = $reflectionClass->getConstructor()) { Chris@0: $metadata['__construct'] = self::getResourceMetadataForMethod($constructor); Chris@0: } Chris@0: Chris@0: foreach (self::getSetters($reflectionClass) as $reflectionMethod) { Chris@0: $metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod); Chris@0: } Chris@0: Chris@0: return new AutowireServiceResource($reflectionClass->name, $reflectionClass->getFileName(), $metadata); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Wires the given definition. Chris@0: * Chris@0: * @param string $id Chris@0: * @param Definition $definition Chris@0: * Chris@0: * @throws RuntimeException Chris@0: */ Chris@0: private function completeDefinition($id, Definition $definition) Chris@0: { Chris@0: if ($definition->getFactory()) { Chris@0: throw new RuntimeException(sprintf('Service "%s" can use either autowiring or a factory, not both.', $id)); Chris@0: } Chris@0: Chris@0: if (!$reflectionClass = $this->getReflectionClass($id, $definition)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($this->container->isTrackingResources()) { Chris@0: $this->container->addResource(static::createResourceForClass($reflectionClass)); Chris@0: } Chris@0: Chris@0: if (!$constructor = $reflectionClass->getConstructor()) { Chris@0: return; Chris@0: } Chris@0: $parameters = $constructor->getParameters(); Chris@0: if (method_exists('ReflectionMethod', 'isVariadic') && $constructor->isVariadic()) { Chris@0: array_pop($parameters); Chris@0: } Chris@0: Chris@0: $arguments = $definition->getArguments(); Chris@0: foreach ($parameters as $index => $parameter) { Chris@0: if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: try { Chris@0: if (!$typeHint = $parameter->getClass()) { Chris@0: if (isset($arguments[$index])) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: // no default value? Then fail Chris@0: if (!$parameter->isOptional()) { Chris@0: 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)); Chris@0: } Chris@0: Chris@0: // specifically pass the default value Chris@0: $arguments[$index] = $parameter->getDefaultValue(); Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (isset($this->autowired[$typeHint->name])) { Chris@0: $arguments[$index] = $this->autowired[$typeHint->name] ? new Reference($this->autowired[$typeHint->name]) : null; Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (null === $this->types) { Chris@0: $this->populateAvailableTypes(); Chris@0: } Chris@0: Chris@0: if (isset($this->types[$typeHint->name])) { Chris@0: $value = new Reference($this->types[$typeHint->name]); Chris@0: } else { Chris@0: try { Chris@0: $value = $this->createAutowiredDefinition($typeHint, $id); Chris@0: } catch (RuntimeException $e) { Chris@0: if ($parameter->isDefaultValueAvailable()) { Chris@0: $value = $parameter->getDefaultValue(); Chris@0: } elseif ($parameter->allowsNull()) { Chris@0: $value = null; Chris@0: } else { Chris@0: throw $e; Chris@0: } Chris@0: $this->autowired[$typeHint->name] = false; Chris@0: } Chris@0: } Chris@0: } catch (\ReflectionException $e) { Chris@0: // Typehint against a non-existing class Chris@0: Chris@0: if (!$parameter->isDefaultValueAvailable()) { Chris@0: 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); Chris@0: } Chris@0: Chris@0: $value = $parameter->getDefaultValue(); Chris@0: } Chris@0: Chris@0: $arguments[$index] = $value; Chris@0: } Chris@0: Chris@0: if ($parameters && !isset($arguments[++$index])) { Chris@0: while (0 <= --$index) { Chris@0: $parameter = $parameters[$index]; Chris@0: if (!$parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== $arguments[$index]) { Chris@0: break; Chris@0: } Chris@0: unset($arguments[$index]); Chris@0: } Chris@0: } Chris@0: Chris@0: // it's possible index 1 was set, then index 0, then 2, etc Chris@0: // make sure that we re-order so they're injected as expected Chris@0: ksort($arguments); Chris@0: $definition->setArguments($arguments); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Populates the list of available types. Chris@0: */ Chris@0: private function populateAvailableTypes() Chris@0: { Chris@0: $this->types = array(); Chris@0: Chris@0: foreach ($this->container->getDefinitions() as $id => $definition) { Chris@0: $this->populateAvailableType($id, $definition); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Populates the list of available types for a given definition. Chris@0: * Chris@0: * @param string $id Chris@0: * @param Definition $definition Chris@0: */ Chris@0: private function populateAvailableType($id, Definition $definition) Chris@0: { Chris@0: // Never use abstract services Chris@0: if ($definition->isAbstract()) { Chris@0: return; Chris@0: } Chris@0: Chris@0: foreach ($definition->getAutowiringTypes() as $type) { Chris@0: $this->definedTypes[$type] = true; Chris@0: $this->types[$type] = $id; Chris@0: unset($this->ambiguousServiceTypes[$type]); Chris@0: } Chris@0: Chris@0: if (!$reflectionClass = $this->getReflectionClass($id, $definition)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: foreach ($reflectionClass->getInterfaces() as $reflectionInterface) { Chris@0: $this->set($reflectionInterface->name, $id); Chris@0: } Chris@0: Chris@0: do { Chris@0: $this->set($reflectionClass->name, $id); Chris@0: } while ($reflectionClass = $reflectionClass->getParentClass()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Associates a type and a service id if applicable. Chris@0: * Chris@0: * @param string $type Chris@0: * @param string $id Chris@0: */ Chris@0: private function set($type, $id) Chris@0: { Chris@0: if (isset($this->definedTypes[$type])) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // is this already a type/class that is known to match multiple services? Chris@0: if (isset($this->ambiguousServiceTypes[$type])) { Chris@0: $this->ambiguousServiceTypes[$type][] = $id; Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: // check to make sure the type doesn't match multiple services Chris@0: if (!isset($this->types[$type]) || $this->types[$type] === $id) { Chris@0: $this->types[$type] = $id; Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: // keep an array of all services matching this type Chris@0: if (!isset($this->ambiguousServiceTypes[$type])) { Chris@0: $this->ambiguousServiceTypes[$type] = array($this->types[$type]); Chris@0: unset($this->types[$type]); Chris@0: } Chris@0: $this->ambiguousServiceTypes[$type][] = $id; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Registers a definition for the type if possible or throws an exception. Chris@0: * Chris@0: * @param \ReflectionClass $typeHint Chris@0: * @param string $id Chris@0: * Chris@0: * @return Reference A reference to the registered definition Chris@0: * Chris@0: * @throws RuntimeException Chris@0: */ Chris@0: private function createAutowiredDefinition(\ReflectionClass $typeHint, $id) Chris@0: { Chris@0: if (isset($this->ambiguousServiceTypes[$typeHint->name])) { Chris@0: $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; Chris@0: $matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]); Chris@0: Chris@0: 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)); Chris@0: } Chris@0: Chris@0: if (!$typeHint->isInstantiable()) { Chris@0: $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; Chris@0: 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)); Chris@0: } Chris@0: Chris@0: $this->autowired[$typeHint->name] = $argumentId = sprintf('autowired.%s', $typeHint->name); Chris@0: Chris@0: $argumentDefinition = $this->container->register($argumentId, $typeHint->name); Chris@0: $argumentDefinition->setPublic(false); Chris@0: Chris@0: try { Chris@0: $this->completeDefinition($argumentId, $argumentDefinition); Chris@0: } catch (RuntimeException $e) { Chris@0: $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; Chris@0: $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); Chris@0: throw new RuntimeException($message, 0, $e); Chris@0: } Chris@0: Chris@0: return new Reference($argumentId); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves the reflection class associated with the given service. Chris@0: * Chris@0: * @param string $id Chris@0: * @param Definition $definition Chris@0: * Chris@0: * @return \ReflectionClass|false Chris@0: */ Chris@0: private function getReflectionClass($id, Definition $definition) Chris@0: { Chris@0: if (isset($this->reflectionClasses[$id])) { Chris@0: return $this->reflectionClasses[$id]; Chris@0: } Chris@0: Chris@0: // Cannot use reflection if the class isn't set Chris@0: if (!$class = $definition->getClass()) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $class = $this->container->getParameterBag()->resolveValue($class); Chris@0: Chris@0: if ($deprecated = $definition->isDeprecated()) { Chris@0: $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) { Chris@0: return (E_USER_DEPRECATED === $level || !$prevErrorHandler) ? false : $prevErrorHandler($level, $message, $file, $line); Chris@0: }); Chris@0: } Chris@0: Chris@0: $e = null; Chris@0: Chris@0: try { Chris@0: $reflector = new \ReflectionClass($class); Chris@0: } catch (\Exception $e) { Chris@0: } catch (\Throwable $e) { Chris@0: } Chris@0: Chris@0: if ($deprecated) { Chris@0: restore_error_handler(); Chris@0: } Chris@0: Chris@0: if (null !== $e) { Chris@0: if (!$e instanceof \ReflectionException) { Chris@0: throw $e; Chris@0: } Chris@0: $reflector = false; Chris@0: } Chris@0: Chris@0: return $this->reflectionClasses[$id] = $reflector; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param \ReflectionClass $reflectionClass Chris@0: * Chris@0: * @return \ReflectionMethod[] Chris@0: */ Chris@0: private static function getSetters(\ReflectionClass $reflectionClass) Chris@0: { Chris@0: foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { Chris@0: if (!$reflectionMethod->isStatic() && 1 === $reflectionMethod->getNumberOfParameters() && 0 === strpos($reflectionMethod->name, 'set')) { Chris@0: yield $reflectionMethod; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: private static function getResourceMetadataForMethod(\ReflectionMethod $method) Chris@0: { Chris@0: $methodArgumentsMetadata = array(); Chris@0: foreach ($method->getParameters() as $parameter) { Chris@0: try { Chris@0: $class = $parameter->getClass(); Chris@0: } catch (\ReflectionException $e) { Chris@0: // type-hint is against a non-existent class Chris@0: $class = false; Chris@0: } Chris@0: Chris@0: $isVariadic = method_exists($parameter, 'isVariadic') && $parameter->isVariadic(); Chris@0: $methodArgumentsMetadata[] = array( Chris@0: 'class' => $class, Chris@0: 'isOptional' => $parameter->isOptional(), Chris@0: 'defaultValue' => ($parameter->isOptional() && !$isVariadic) ? $parameter->getDefaultValue() : null, Chris@0: ); Chris@0: } Chris@0: Chris@0: return $methodArgumentsMetadata; Chris@0: } Chris@0: }