Skip to content

Commit

Permalink
Unified type validation using Helper::ensureClassType()
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Sep 19, 2021
1 parent 3689d92 commit 694b044
Show file tree
Hide file tree
Showing 16 changed files with 74 additions and 157 deletions.
9 changes: 6 additions & 3 deletions src/DI/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 3 additions & 13 deletions src/DI/Definitions/AccessorDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace Nette\DI\Definitions;

use Nette;
use Nette\DI\ServiceCreationException;
use Nette\DI\Helpers;
use Nette\Utils\Type;


Expand Down Expand Up @@ -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);
Expand Down
14 changes: 3 additions & 11 deletions src/DI/Definitions/FactoryDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
44 changes: 5 additions & 39 deletions src/DI/Extensions/InjectExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ private function updateDefinition(Definitions\ServiceDefinition $def): void
unset($setups[$key]);
}
}
self::checkType($class, $property, $type, $builder);
array_unshift($setups, $inject);
}

Expand Down Expand Up @@ -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);
Expand All @@ -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
));
}
}
}
38 changes: 24 additions & 14 deletions src/DI/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Nette\DI\Definitions\Reference;
use Nette\DI\Definitions\Statement;
use Nette\Utils\Reflection;
use Nette\Utils\Type;


/**
Expand Down Expand Up @@ -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;
}


Expand Down
15 changes: 8 additions & 7 deletions src/DI/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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]));
}
Expand Down
2 changes: 1 addition & 1 deletion tests/DI/Container.dynamic.php80.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
2 changes: 1 addition & 1 deletion tests/DI/ContainerBuilder.factory.error.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions tests/DI/ContainerBuilder.factory.resolveBuiltinTypes.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/DI/Definitions.AccessorDefinition.resolve.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
2 changes: 1 addition & 1 deletion tests/DI/Definitions.FactoryDefinition.resolve.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
26 changes: 7 additions & 19 deletions tests/DI/Helpers.getReturnType.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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')));
7 changes: 4 additions & 3 deletions tests/DI/InjectExtension.errors.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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.');
Loading

0 comments on commit 694b044

Please sign in to comment.