From da68e66a9913cd0bef7cbf98bdd01ebf13220457 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 15 Jun 2020 16:43:28 +0200 Subject: [PATCH] Fix support for PHP8 union types --- .../Compiler/CheckTypeDeclarationsPass.php | 23 +++++++++++++--- .../DependencyInjection/Dumper/Preloader.php | 27 ++++++++++++------- .../InvalidParameterTypeException.php | 5 +++- .../RegisterListenersPass.php | 2 +- .../ArgumentMetadataFactory.php | 2 +- .../EventListener/ErrorListener.php | 2 +- .../DependencyInjection/MessengerPass.php | 15 ++++++++++- .../OptionsResolver/OptionsResolver.php | 2 +- .../Contracts/Service/ServiceLocatorTrait.php | 2 +- .../Service/ServiceSubscriberTrait.php | 2 +- 10 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index cf255640c2..ae1a2ec204 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -153,9 +153,26 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass /** * @throws InvalidParameterTypeException When a parameter is not compatible with the declared type */ - private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter, ?string $envPlaceholderUniquePrefix): void + private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter, ?string $envPlaceholderUniquePrefix, string $type = null): void { - $type = $parameter->getType()->getName(); + if (null === $type) { + $type = $parameter->getType(); + + if ($type instanceof \ReflectionUnionType) { + foreach ($type->getTypes() as $type) { + try { + $this->checkType($checkedDefinition, $value, $parameter, $envPlaceholderUniquePrefix, $type); + + return; + } catch (InvalidParameterTypeException $e) { + } + } + + throw new InvalidParameterTypeException($this->currentId, $e->getCode(), $parameter); + } + + $type = $type->getName(); + } if ($value instanceof Reference) { if (!$this->container->has($value = (string) $value)) { @@ -266,7 +283,7 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass return; } - $checkFunction = sprintf('is_%s', $parameter->getType()->getName()); + $checkFunction = sprintf('is_%s', $type); if (!$parameter->getType()->isBuiltin() || !$checkFunction($value)) { throw new InvalidParameterTypeException($this->currentId, \is_object($value) ? $class : \gettype($value), $parameter); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/Preloader.php b/src/Symfony/Component/DependencyInjection/Dumper/Preloader.php index abb7d90ff5..f9d2716e7d 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/Preloader.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/Preloader.php @@ -48,7 +48,7 @@ class Preloader } } - private static function doPreload(string $class, array &$preloaded) + private static function doPreload(string $class, array &$preloaded): void { if (isset($preloaded[$class]) || \in_array($class, ['self', 'static', 'parent'], true)) { return; @@ -68,9 +68,7 @@ class Preloader if (\PHP_VERSION_ID >= 70400) { foreach ($r->getProperties(\ReflectionProperty::IS_PUBLIC) as $p) { - if (($t = $p->getType()) && !$t->isBuiltin()) { - self::doPreload($t->getName(), $preloaded); - } + self::preloadType($p->getType(), $preloaded); } } @@ -84,17 +82,26 @@ class Preloader } } - if (($t = $p->getType()) && !$t->isBuiltin()) { - self::doPreload($t->getName(), $preloaded); - } + self::preloadType($p->getType(), $preloaded); } - if (($t = $m->getReturnType()) && !$t->isBuiltin()) { - self::doPreload($t->getName(), $preloaded); - } + self::preloadType($p->getReturnType(), $preloaded); } } catch (\ReflectionException $e) { // ignore missing classes } } + + private static function preloadType(?\ReflectionType $t, array &$preloaded): void + { + if (!$t || $t->isBuiltin()) { + return; + } + + foreach ($t instanceof \ReflectionUnionType ? $t->getTypes() : [$t] as $t) { + if (!$t->isBuiltin()) { + self::doPreload($t instanceof \ReflectionNamedType ? $t->getName() : $t, $preloaded); + } + } + } } diff --git a/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php b/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php index 206561fa95..05fdb4a779 100644 --- a/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php +++ b/src/Symfony/Component/DependencyInjection/Exception/InvalidParameterTypeException.php @@ -21,6 +21,9 @@ class InvalidParameterTypeException extends InvalidArgumentException { public function __construct(string $serviceId, string $type, \ReflectionParameter $parameter) { - parent::__construct(sprintf('Invalid definition for service "%s": argument %d of "%s::%s" accepts "%s", "%s" passed.', $serviceId, 1 + $parameter->getPosition(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), $parameter->getType()->getName(), $type)); + $acceptedType = $parameter->getType(); + $acceptedType = $acceptedType instanceof \ReflectionNamedType ? $acceptedType->getName() : (string) $acceptedType; + + parent::__construct(sprintf('Invalid definition for service "%s": argument %d of "%s::%s" accepts "%s", "%s" passed.', $serviceId, 1 + $parameter->getPosition(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), $acceptedType, $type), $type); } } diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 3ae1136c4c..4b27d3b23a 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -138,7 +138,7 @@ class RegisterListenersPass implements CompilerPassInterface || !($r = $container->getReflectionClass($class, false)) || !$r->hasMethod($method) || 1 > ($m = $r->getMethod($method))->getNumberOfParameters() - || !($type = $m->getParameters()[0]->getType()) + || !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType || $type->isBuiltin() || Event::class === ($name = $type->getName()) || LegacyEvent::class === $name diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 9370174c25..6c192a6f5f 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -45,7 +45,7 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface */ private function getType(\ReflectionParameter $parameter, \ReflectionFunctionAbstract $function): ?string { - if (!$type = $parameter->getType()) { + if (!($type = $parameter->getType()) instanceof \ReflectionNamedType) { return null; } $name = $type->getName(); diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index 26c361f754..1ca6c9b458 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -99,7 +99,7 @@ class ErrorListener implements EventSubscriberInterface $r = new \ReflectionFunction(\Closure::fromCallable($event->getController())); $r = $r->getParameters()[$k] ?? null; - if ($r && (!$r->hasType() || \in_array($r->getType()->getName(), [FlattenException::class, LegacyFlattenException::class], true))) { + if ($r && (!($r = $r->getType()) instanceof \ReflectionNamedType || \in_array($r->getName(), [FlattenException::class, LegacyFlattenException::class], true))) { $arguments = $event->getArguments(); $arguments[$k] = FlattenException::createFromThrowable($e); $event->setArguments($arguments); diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index bf62b4c87e..888155dea9 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -228,11 +228,24 @@ class MessengerPass implements CompilerPassInterface throw new RuntimeException(sprintf('Invalid handler service "%s": argument "$%s" of method "%s::__invoke()" must have a type-hint corresponding to the message class it handles.', $serviceId, $parameters[0]->getName(), $handlerClass->getName())); } + if ($type instanceof \ReflectionUnionType) { + $types = []; + foreach ($type->getTypes() as $type) { + if (!$type->isBuiltin()) { + $types[] = (string) $type; + } + } + + if ($types) { + return $types; + } + } + if ($type->isBuiltin()) { throw new RuntimeException(sprintf('Invalid handler service "%s": type-hint of argument "$%s" in method "%s::__invoke()" must be a class , "%s" given.', $serviceId, $parameters[0]->getName(), $handlerClass->getName(), $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type)); } - return [$parameters[0]->getType()->getName()]; + return [$type->getName()]; } private function registerReceivers(ContainerBuilder $container, array $busIds) diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index e40583dd5f..3fb3c06f92 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -201,7 +201,7 @@ class OptionsResolver implements Options return $this; } - if (isset($params[0]) && null !== ($type = $params[0]->getType()) && self::class === $type->getName() && (!isset($params[1]) || (null !== ($type = $params[1]->getType()) && Options::class === $type->getName()))) { + if (isset($params[0]) && null !== ($type = $params[0]->getType()) && self::class === $type->getName() && (!isset($params[1]) || (($type = $params[1]->getType()) instanceof \ReflectionNamedType && Options::class === $type->getName()))) { // Store closure for later evaluation $this->nested[$option][] = $value; $this->defaults[$option] = []; diff --git a/src/Symfony/Contracts/Service/ServiceLocatorTrait.php b/src/Symfony/Contracts/Service/ServiceLocatorTrait.php index 0b4d60affa..1737f50e99 100644 --- a/src/Symfony/Contracts/Service/ServiceLocatorTrait.php +++ b/src/Symfony/Contracts/Service/ServiceLocatorTrait.php @@ -87,7 +87,7 @@ trait ServiceLocatorTrait } else { $type = (new \ReflectionFunction($factory))->getReturnType(); - $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').$type->getName() : '?'; + $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; } } } diff --git a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php index 5d9d456d0c..82fb5ab361 100644 --- a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php +++ b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php @@ -40,7 +40,7 @@ trait ServiceSubscriberTrait } if (self::class === $method->getDeclaringClass()->name && ($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) { - $services[self::class.'::'.$method->name] = '?'.$returnType->getName(); + $services[self::class.'::'.$method->name] = '?'.($returnType instanceof \ReflectionNamedType ? $returnType->getName() : $type); } }