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