diff --git a/src/DI/Container.php b/src/DI/Container.php index 30a6b73e1..d7df4c98e 100644 --- a/src/DI/Container.php +++ b/src/DI/Container.php @@ -75,9 +75,12 @@ public function addService(string $name, $service) throw new Nette\InvalidArgumentException(sprintf("Service '%s' must be a object, %s given.", $name, gettype($service))); } - $type = $service instanceof \Closure - ? (string) Nette\Utils\Reflection::getReturnType(new \ReflectionFunction($service)) - : get_class($service); + if ($service instanceof \Closure) { + $rt = Nette\Utils\Type::fromReflection(new \ReflectionFunction($service)); + $type = $rt ? Helpers::ensureClassType($rt, 'return type of factory') : ''; + } else { + $type = get_class($service); + } if (!isset($this->methods[self::getMethodName($name)])) { $this->types[$name] = $type; diff --git a/src/DI/Definitions/AccessorDefinition.php b/src/DI/Definitions/AccessorDefinition.php index 693c88a57..71c2992c0 100644 --- a/src/DI/Definitions/AccessorDefinition.php +++ b/src/DI/Definitions/AccessorDefinition.php @@ -10,7 +10,7 @@ namespace Nette\DI\Definitions; use Nette; -use Nette\DI\ServiceCreationException; +use Nette\DI\Helpers; use Nette\Utils\Type; @@ -99,18 +99,8 @@ public function complete(Nette\DI\Resolver $resolver): void if (!$this->reference) { $interface = $this->getType(); $method = new \ReflectionMethod($interface, self::METHOD_GET); - $returnType = Nette\DI\Helpers::getReturnType($method); - - if (!$returnType) { - throw new ServiceCreationException(sprintf('Method %s::get() has no return type or annotation @return.', $interface)); - } elseif (!class_exists($returnType) && !interface_exists($returnType)) { - throw new ServiceCreationException(sprintf( - "Class '%s' not found.\nCheck the return type or annotation @return of the %s::get() method.", - $returnType, - $interface - )); - } - $this->setReference($returnType); + $type = Type::fromReflection($method) ?? Helpers::getReturnTypeAnnotation($method); + $this->setReference(Helpers::ensureClassType($type, "return type of $interface::get()")); } $this->reference = $resolver->normalizeReference($this->reference); diff --git a/src/DI/Definitions/FactoryDefinition.php b/src/DI/Definitions/FactoryDefinition.php index 02fd4d4ef..cb1d277eb 100644 --- a/src/DI/Definitions/FactoryDefinition.php +++ b/src/DI/Definitions/FactoryDefinition.php @@ -10,6 +10,7 @@ namespace Nette\DI\Definitions; use Nette; +use Nette\DI\Helpers; use Nette\DI\ServiceCreationException; use Nette\Utils\Reflection; use Nette\Utils\Type; @@ -189,17 +190,8 @@ public function resolveType(Nette\DI\Resolver $resolver): void throw new ServiceCreationException('Type is missing in definition of service.'); } $method = new \ReflectionMethod($interface, self::METHOD_CREATE); - $returnType = Nette\DI\Helpers::getReturnType($method); - if (!$returnType) { - throw new ServiceCreationException(sprintf('Method %s::create() has no return type or annotation @return.', $interface)); - } elseif (!class_exists($returnType) && !interface_exists($returnType)) { - throw new ServiceCreationException(sprintf( - "Class '%s' not found.\nCheck the return type or annotation @return of the %s::create() method.", - $returnType, - $interface - )); - } - $resultDef->setType($returnType); + $type = Type::fromReflection($method) ?? Helpers::getReturnTypeAnnotation($method); + $resultDef->setType(Helpers::ensureClassType($type, "return type of $interface::create()")); } $resolver->resolveDefinition($resultDef); diff --git a/src/DI/Extensions/InjectExtension.php b/src/DI/Extensions/InjectExtension.php index e6dae2cd9..a152c7875 100644 --- a/src/DI/Extensions/InjectExtension.php +++ b/src/DI/Extensions/InjectExtension.php @@ -62,7 +62,6 @@ private function updateDefinition(Definitions\ServiceDefinition $def): void unset($setups[$key]); } } - self::checkType($class, $property, $type, $builder); array_unshift($setups, $inject); } @@ -114,17 +113,12 @@ public static function getInjectProperties(string $class): array $rp = new \ReflectionProperty($class, $name); $hasAttr = PHP_VERSION_ID >= 80000 && $rp->getAttributes(DI\Attributes\Inject::class); if ($hasAttr || DI\Helpers::parseAnnotation($rp, 'inject') !== null) { - if ($type = Reflection::getPropertyType($rp)) { - } elseif (!$hasAttr && ($type = DI\Helpers::parseAnnotation($rp, 'var'))) { - if (strpos($type, '|') !== false) { - throw new Nette\InvalidStateException(sprintf( - 'The %s is not expected to have a union type.', - Reflection::toString($rp) - )); - } - $type = Reflection::expandClassName($type, Reflection::getPropertyDeclaringClass($rp)); + $type = Nette\Utils\Type::fromReflection($rp); + if (!$type && !$hasAttr && ($annotation = DI\Helpers::parseAnnotation($rp, 'var'))) { + $annotation = Reflection::expandClassName($annotation, Reflection::getPropertyDeclaringClass($rp)); + $type = Nette\Utils\Type::fromString($annotation); } - $res[$name] = $type; + $res[$name] = DI\Helpers::ensureClassType($type, 'type of property ' . Reflection::toString($rp)); } } ksort($res); @@ -147,35 +141,7 @@ public static function callInjects(DI\Container $container, $service): void } foreach (self::getInjectProperties(get_class($service)) as $property => $type) { - self::checkType($service, $property, $type, $container); $service->$property = $container->getByType($type); } } - - - /** - * @param object|string $class - * @param DI\Container|DI\ContainerBuilder|null $container - */ - private static function checkType($class, string $name, ?string $type, $container): void - { - $propName = Reflection::toString(new \ReflectionProperty($class, $name)); - if (!$type) { - throw new Nette\InvalidStateException(sprintf('Property %s has no type.', $propName)); - - } elseif (!class_exists($type) && !interface_exists($type)) { - throw new Nette\InvalidStateException(sprintf( - "Class '%s' required by %s not found. Check the property type and 'use' statements.", - $type, - $propName - )); - - } elseif ($container && !$container->getByType($type, false)) { - throw new Nette\DI\MissingServiceException(sprintf( - 'Service of type %s required by %s not found. Did you add it to configuration file?', - $type, - $propName - )); - } - } } diff --git a/src/DI/Helpers.php b/src/DI/Helpers.php index 3efbf7832..b55c48c53 100644 --- a/src/DI/Helpers.php +++ b/src/DI/Helpers.php @@ -13,6 +13,7 @@ use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Nette\Utils\Reflection; +use Nette\Utils\Type; /** @@ -202,22 +203,31 @@ public static function parseAnnotation(\Reflector $ref, string $name): ?string } - public static function getReturnType(\ReflectionFunctionAbstract $func): ?string + public static function getReturnTypeAnnotation(\ReflectionFunctionAbstract $func): ?Type { - if ($type = Reflection::getReturnType($func)) { - return $type; - } elseif ($type = preg_replace('#[|\s].*#', '', (string) self::parseAnnotation($func, 'return'))) { - if ($type === 'object' || $type === 'mixed') { - return null; - } elseif ($func instanceof \ReflectionMethod) { - return $type === 'static' || $type === '$this' - ? $func->getDeclaringClass()->name - : Reflection::expandClassName($type, $func->getDeclaringClass()); - } else { - return $type; - } + $type = preg_replace('#[|\s].*#', '', (string) self::parseAnnotation($func, 'return')); + if (!$type || $type === 'object' || $type === 'mixed') { + return null; + } elseif ($func instanceof \ReflectionMethod) { + $type = $type === '$this' ? 'static' : $type; + $type = Reflection::expandClassName($type, $func->getDeclaringClass()); } - return null; + return Type::fromString($type); + } + + + public static function ensureClassType(?Type $type, string $hint): string + { + if (!$type) { + throw new ServiceCreationException(sprintf('%s is not declared.', ucfirst($hint))); + } elseif (!$type->isClass()) { + throw new ServiceCreationException(sprintf("%s is not expected to be union/intersection/built-in, '%s' given.", ucfirst($hint), $type)); + } + $class = $type->getSingleName(); + if (!class_exists($class) && !interface_exists($class)) { + throw new ServiceCreationException(sprintf("Class '%s' not found.\nCheck the %s.", $class, $hint)); + } + return $class; } diff --git a/src/DI/Resolver.php b/src/DI/Resolver.php index ca462ba1c..2105415ee 100644 --- a/src/DI/Resolver.php +++ b/src/DI/Resolver.php @@ -14,6 +14,7 @@ use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; use Nette\PhpGenerator\Helpers as PhpHelpers; +use Nette\Utils\Callback; use Nette\Utils\Reflection; use Nette\Utils\Strings; use Nette\Utils\Validators; @@ -110,7 +111,7 @@ public function resolveEntityType(Statement $statement): ?string try { /** @var \ReflectionMethod|\ReflectionFunction $reflection */ - $reflection = Nette\Utils\Callback::toReflection($entity[0] === '' ? $entity[1] : $entity); + $reflection = Callback::toReflection($entity[0] === '' ? $entity[1] : $entity); $refClass = $reflection instanceof \ReflectionMethod ? $reflection->getDeclaringClass() : null; @@ -121,15 +122,15 @@ public function resolveEntityType(Statement $statement): ?string if (isset($e) || ($refClass && (!$reflection->isPublic() || ($refClass->isTrait() && !$reflection->isStatic()) ))) { - throw new ServiceCreationException(sprintf('Method %s() is not callable.', Nette\Utils\Callback::toString($entity)), 0, $e ?? null); + throw new ServiceCreationException(sprintf('Method %s() is not callable.', Callback::toString($entity)), 0, $e ?? null); } $this->addDependency($reflection); - $type = Helpers::getReturnType($reflection); - if ($type && !class_exists($type) && !interface_exists($type)) { - throw new ServiceCreationException(sprintf("Class or interface '%s' not found. Check the return type of %s() method.", $type, Nette\Utils\Callback::toString($entity))); + $type = Nette\Utils\Type::fromReflection($reflection) ?? Helpers::getReturnTypeAnnotation($reflection); + if ($type) { + return Helpers::ensureClassType($type, sprintf('return type of %s()', Callback::toString($entity))); } - return $type; + return null; } elseif ($entity instanceof Reference) { // alias or factory return $this->resolveReferenceType($entity); @@ -264,7 +265,7 @@ public function completeStatement(Statement $statement, bool $currentServiceAllo case is_string($entity[0]): // static method call case $entity[0] instanceof Reference: if ($entity[1][0] === '$') { // property getter, setter or appender - Validators::assert($arguments, 'list:0..1', "setup arguments for '" . Nette\Utils\Callback::toString($entity) . "'"); + Validators::assert($arguments, 'list:0..1', "setup arguments for '" . Callback::toString($entity) . "'"); if (!$arguments && substr($entity[1], -2) === '[]') { throw new ServiceCreationException(sprintf('Missing argument for %s.', $entity[1])); } diff --git a/tests/DI/Container.dynamic.php80.phpt b/tests/DI/Container.dynamic.php80.phpt index 3135b9390..688ec991d 100644 --- a/tests/DI/Container.dynamic.php80.phpt +++ b/tests/DI/Container.dynamic.php80.phpt @@ -19,4 +19,4 @@ $container = new Container; Assert::exception(function () use ($container) { @$container->addService('six', function (): \stdClass|\Closure {}); // @ triggers service should be defined as "imported" $container->getService('six'); -}, Nette\InvalidStateException::class, 'The {closure}%a?% is not expected to have a union%a?% type.'); +}, Nette\InvalidStateException::class, "Return type of factory is not expected to be union/intersection/built-in, 'stdClass|Closure' given."); diff --git a/tests/DI/ContainerBuilder.factory.error.phpt b/tests/DI/ContainerBuilder.factory.error.phpt index 2a58e3cc3..22f9259af 100644 --- a/tests/DI/ContainerBuilder.factory.error.phpt +++ b/tests/DI/ContainerBuilder.factory.error.phpt @@ -76,7 +76,7 @@ Assert::exception(function () { $builder->addFactoryDefinition('one') ->setImplement('Bad4'); $builder->complete(); -}, Nette\InvalidStateException::class, "Service 'one' (type of Bad4): Method create() has no return type or annotation @return."); +}, Nette\InvalidStateException::class, "Service 'one' (type of Bad4): Return type of create() is not declared."); interface Bad5 diff --git a/tests/DI/ContainerBuilder.factory.resolveBuiltinTypes.phpt b/tests/DI/ContainerBuilder.factory.resolveBuiltinTypes.phpt index 9829f563e..334354c75 100644 --- a/tests/DI/ContainerBuilder.factory.resolveBuiltinTypes.phpt +++ b/tests/DI/ContainerBuilder.factory.resolveBuiltinTypes.phpt @@ -82,7 +82,7 @@ namespace $builder->addDefinition('a') ->setFactory('@factory::createArray'); $container = createContainer($builder); - }, Nette\DI\ServiceCreationException::class, "Service 'a': Class or interface 'array' not found. Check the return type of A\\Factory::createArray() method."); + }, Nette\DI\ServiceCreationException::class, "Service 'a': Return type of A\\Factory::createArray() is not expected to be union/intersection/built-in, 'array' given."); Assert::exception(function () { $builder = new DI\ContainerBuilder; @@ -91,7 +91,7 @@ namespace $builder->addDefinition('c') ->setFactory('@factory::createCallable'); $container = createContainer($builder); - }, Nette\DI\ServiceCreationException::class, "Service 'c': Class or interface 'callable' not found. Check the return type of A\\Factory::createCallable() method."); + }, Nette\DI\ServiceCreationException::class, "Service 'c': Return type of A\\Factory::createCallable() is not expected to be union/intersection/built-in, 'callable' given."); Assert::exception(function () { $builder = new DI\ContainerBuilder; @@ -100,7 +100,7 @@ namespace $builder->addDefinition('s') ->setFactory('@factory::createString'); $container = createContainer($builder); - }, Nette\DI\ServiceCreationException::class, "Service 's': Class or interface 'string' not found. Check the return type of A\\Factory::createString() method."); + }, Nette\DI\ServiceCreationException::class, "Service 's': Return type of A\\Factory::createString() is not expected to be union/intersection/built-in, 'string' given."); Assert::exception(function () { $builder = new DI\ContainerBuilder; @@ -109,7 +109,7 @@ namespace $builder->addDefinition('i') ->setFactory('@factory::createInt'); $container = createContainer($builder); - }, Nette\DI\ServiceCreationException::class, "Service 'i': Class or interface 'int' not found. Check the return type of A\\Factory::createInt() method."); + }, Nette\DI\ServiceCreationException::class, "Service 'i': Return type of A\\Factory::createInt() is not expected to be union/intersection/built-in, 'int' given."); Assert::exception(function () { $builder = new DI\ContainerBuilder; @@ -118,7 +118,7 @@ namespace $builder->addDefinition('b') ->setFactory('@factory::createBool'); $container = createContainer($builder); - }, Nette\DI\ServiceCreationException::class, "Service 'b': Class or interface 'bool' not found. Check the return type of A\\Factory::createBool() method."); + }, Nette\DI\ServiceCreationException::class, "Service 'b': Return type of A\\Factory::createBool() is not expected to be union/intersection/built-in, 'bool' given."); Assert::exception(function () { $builder = new DI\ContainerBuilder; @@ -127,7 +127,7 @@ namespace $builder->addDefinition('f') ->setFactory('@factory::createFloat'); $container = createContainer($builder); - }, Nette\DI\ServiceCreationException::class, "Service 'f': Class or interface 'float' not found. Check the return type of A\\Factory::createFloat() method."); + }, Nette\DI\ServiceCreationException::class, "Service 'f': Return type of A\\Factory::createFloat() is not expected to be union/intersection/built-in, 'float' given."); Assert::exception(function () { $builder = new DI\ContainerBuilder; diff --git a/tests/DI/Definitions.AccessorDefinition.resolve.phpt b/tests/DI/Definitions.AccessorDefinition.resolve.phpt index f65d7b303..6df67b33f 100644 --- a/tests/DI/Definitions.AccessorDefinition.resolve.phpt +++ b/tests/DI/Definitions.AccessorDefinition.resolve.phpt @@ -37,7 +37,7 @@ Assert::exception(function () { $resolver = new Nette\DI\Resolver(new Nette\DI\ContainerBuilder); $resolver->resolveDefinition($def); $resolver->completeDefinition($def); -}, Nette\DI\ServiceCreationException::class, 'Service of type Good1: Method get() has no return type or annotation @return.'); +}, Nette\DI\ServiceCreationException::class, 'Service of type Good1: Return type of get() is not declared.'); Assert::noError(function () { diff --git a/tests/DI/Definitions.FactoryDefinition.resolve.phpt b/tests/DI/Definitions.FactoryDefinition.resolve.phpt index a32930e14..1ae477b61 100644 --- a/tests/DI/Definitions.FactoryDefinition.resolve.phpt +++ b/tests/DI/Definitions.FactoryDefinition.resolve.phpt @@ -36,7 +36,7 @@ Assert::exception(function () { $def->setImplement('Good1'); $resolver = new Nette\DI\Resolver(new Nette\DI\ContainerBuilder); $resolver->resolveDefinition($def); -}, Nette\DI\ServiceCreationException::class, 'Service of type Good1: Method create() has no return type or annotation @return.'); +}, Nette\DI\ServiceCreationException::class, 'Service of type Good1: Return type of create() is not declared.'); Assert::noError(function () { diff --git a/tests/DI/Helpers.getReturnType.phpt b/tests/DI/Helpers.getReturnType.phpt index 7f405e3e5..eb2367123 100644 --- a/tests/DI/Helpers.getReturnType.phpt +++ b/tests/DI/Helpers.getReturnType.phpt @@ -14,29 +14,17 @@ require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/fixtures/Helpers.getReturnType.php'; -Assert::null(Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'noType'))); +Assert::null(Helpers::getReturnTypeAnnotation(new \ReflectionMethod(NS\A::class, 'noType'))); -Assert::same('Test\B', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'classType'))); +Assert::same('Test\B', (string) Helpers::getReturnTypeAnnotation(new \ReflectionMethod(NS\A::class, 'annotationClassType'))); -Assert::same('string', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'nativeType'))); +Assert::same('Test\B', (string) Helpers::getReturnTypeAnnotation(new \ReflectionMethod(NS\A::class, 'annotationUnionType'))); -Assert::same('NS\A', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'selfType'))); +Assert::same('string', (string) Helpers::getReturnTypeAnnotation(new \ReflectionMethod(NS\A::class, 'annotationNativeType'))); -Assert::same('Test\B', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'nullableClassType'))); +Assert::same('NS\A', (string) Helpers::getReturnTypeAnnotation(new \ReflectionMethod(NS\A::class, 'annotationSelfType'))); -Assert::same('string', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'nullableNativeType'))); - -Assert::same('NS\A', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'nullableSelfType'))); - -Assert::same('Test\B', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'annotationClassType'))); - -Assert::same('Test\B', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'annotationUnionType'))); - -Assert::same('string', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'annotationNativeType'))); - -Assert::same('NS\A', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'annotationSelfType'))); - -Assert::same('NS\A', Helpers::getReturnType(new \ReflectionMethod(NS\A::class, 'annotationStaticType'))); +Assert::same('NS\A', (string) Helpers::getReturnTypeAnnotation(new \ReflectionMethod(NS\A::class, 'annotationStaticType'))); // class name expanding is NOT supported for global functions -Assert::same('B', Helpers::getReturnType(new \ReflectionFunction('NS\annotationClassType'))); +Assert::same('B', (string) Helpers::getReturnTypeAnnotation(new \ReflectionFunction('NS\annotationClassType'))); diff --git a/tests/DI/InjectExtension.errors.phpt b/tests/DI/InjectExtension.errors.phpt index 67128bd64..c7f375928 100644 --- a/tests/DI/InjectExtension.errors.phpt +++ b/tests/DI/InjectExtension.errors.phpt @@ -46,7 +46,7 @@ services: factory: ServiceA inject: yes '); -}, InvalidStateException::class, 'Service of type DateTimeImmutable required by ServiceA::$a not found. Did you add it to configuration file?'); +}, InvalidStateException::class, "Service 'service' (type of ServiceA): Service of type DateTimeImmutable not found. Did you add it to configuration file?"); Assert::exception(function () use ($compiler) { @@ -56,7 +56,8 @@ services: factory: ServiceB inject: yes '); -}, InvalidStateException::class, "Class 'Unknown' required by ServiceB::\$a not found. Check the property type and 'use' statements."); +}, InvalidStateException::class, "Class 'Unknown' not found. +Check the type of property ServiceB::\$a."); Assert::exception(function () use ($compiler) { @@ -66,4 +67,4 @@ services: factory: ServiceC inject: yes '); -}, InvalidStateException::class, 'Property ServiceC::$a has no type.'); +}, InvalidStateException::class, 'Type of property ServiceC::$a is not declared.'); diff --git a/tests/DI/InjectExtension.getInjectProperties().php74.phpt b/tests/DI/InjectExtension.getInjectProperties().php74.phpt index bd621b9d8..fb8321651 100644 --- a/tests/DI/InjectExtension.getInjectProperties().php74.phpt +++ b/tests/DI/InjectExtension.getInjectProperties().php74.phpt @@ -15,10 +15,7 @@ namespace A public AInjected $varA; /** @inject */ - public B\BInjected $varB; - - /** @inject */ - public \A\AInjected $varC; + public AInjected $varC; public AInjected $varD; @@ -41,7 +38,6 @@ namespace Assert::same([ 'varA' => 'A\AInjected', - 'varB' => 'A\B\BInjected', 'varC' => 'A\AInjected', ], InjectExtension::getInjectProperties('A\AClass')); } diff --git a/tests/DI/InjectExtension.getInjectProperties().php80.phpt b/tests/DI/InjectExtension.getInjectProperties().php80.phpt index c9af43abe..ea55d12cc 100644 --- a/tests/DI/InjectExtension.getInjectProperties().php80.phpt +++ b/tests/DI/InjectExtension.getInjectProperties().php80.phpt @@ -22,14 +22,14 @@ class AClass class EClass { #[\Nette\DI\Attributes\Inject] - public EInjected $varA; + public stdClass $varA; } Assert::exception(function () { InjectExtension::getInjectProperties(AClass::class); -}, Nette\InvalidStateException::class, 'The AClass::$var is not expected to have a union%a?% type.'); +}, Nette\InvalidStateException::class, "Type of property AClass::\$var is not expected to be union/intersection/built-in, 'AClass|stdClass' given."); Assert::same([ - 'varA' => 'EInjected', + 'varA' => 'stdClass', ], InjectExtension::getInjectProperties(EClass::class)); diff --git a/tests/DI/fixtures/Helpers.getReturnType.php b/tests/DI/fixtures/Helpers.getReturnType.php index d33bfdf01..93bb93e5b 100644 --- a/tests/DI/fixtures/Helpers.getReturnType.php +++ b/tests/DI/fixtures/Helpers.getReturnType.php @@ -11,36 +11,6 @@ public function noType() } - public function classType(): B - { - } - - - public function nativeType(): string - { - } - - - public function selfType(): self - { - } - - - public function nullableClassType(): ?B - { - } - - - public function nullableNativeType(): ?string - { - } - - - public function nullableSelfType(): ?self - { - } - - /** @return B */ public function annotationClassType() {