diff --git a/src/Container.php b/src/Container.php index 14e613c..4063b05 100644 --- a/src/Container.php +++ b/src/Container.php @@ -346,7 +346,7 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool // skip type checks and allow all values if expected type is undefined or mixed (PHP 8+) // allow null values if parameter is marked nullable or untyped or mixed - if ($type === null || ($value === null && $parameter->allowsNull()) || ($type instanceof \ReflectionNamedType && $type->getName() === 'mixed') || $this->validateType($value, $type)) { + if ($type === null || ($value === null && $parameter->allowsNull()) || ($type instanceof \ReflectionNamedType && $type->getName() === 'mixed') || $this->validateType($value, $type, $parameter->getDeclaringClass())) { return $value; } @@ -355,23 +355,31 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool ); } + // get class type (if any), resolve `self` and `parent` types against the parameter's declaring class + $class = $type instanceof \ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null; + if ($class === 'self' && ($declaring = $parameter->getDeclaringClass()) !== null) { + $class = $declaring->getName(); + } elseif ($class === 'parent' && ($declaring = $parameter->getDeclaringClass()) !== null && ($parent = $declaring->getParentClass()) !== false) { + $class = $parent->getName(); + } + // use default argument if not loadable as container variable or by type if ( $parameter->isDefaultValueAvailable() && - (!$type instanceof \ReflectionNamedType || $type->isBuiltin() || !\array_key_exists($type->getName(), $this->container)) + ($class === null || !\array_key_exists($class, $this->container)) ) { return $parameter->getDefaultValue(); } // abort if required container variable is not defined or for any other primitive types (array etc.) - if (!$type instanceof \ReflectionNamedType || $type->isBuiltin()) { + if ($class === null) { throw new \Error( self::parameterError($parameter, $for) . ' requires container config' . ($type !== null ? ' with type ' . self::typeName($type) : '') . ', none given' ); } - // @phpstan-ignore-next-line because `$type->getName()` is a `class-string` by definition - return $this->loadObject($type->getName(), $depth - 1); + // @phpstan-ignore-next-line because `$class` is a `class-string` by definition + return $this->loadObject($class, $depth - 1); } private function hasVariable(string $name): bool @@ -430,9 +438,10 @@ private function loadVariable(string $name, int $depth = 64) /*: mixed (PHP 8.0+ /** * @param mixed $value * @param \ReflectionType $type + * @param ?\ReflectionClass $scope * @throws void */ - private function validateType($value, \ReflectionType $type): bool + private function validateType($value, \ReflectionType $type, ?\ReflectionClass $scope): bool { // check union types (PHP 8.0+) and intersection types (PHP 8.1+) and DNF types (PHP 8.2+) if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart @@ -440,7 +449,7 @@ private function validateType($value, \ReflectionType $type): bool foreach ($type->getTypes() as $type) { // return early success if any union type matches // return early failure if any intersection type doesn't match - if ($this->validateType($value, $type) === $early) { + if ($this->validateType($value, $type, $scope) === $early) { return $early; } } @@ -454,6 +463,13 @@ private function validateType($value, \ReflectionType $type): bool // nullable types and mixed already handled before entering this check \assert($type !== 'null' && $type !== 'mixed'); + // resolve `self` and `parent` against the given class scope (e.g. as part of a union type) + if ($type === 'self' && $scope !== null) { + $type = $scope->getName(); + } elseif ($type === 'parent' && $scope !== null && ($parent = $scope->getParentClass()) !== false) { + $type = $parent->getName(); + } + return ( (\is_object($value) && ($value instanceof $type || $type === 'object')) || // instanceof or object for PHP 7.2+ (\is_string($value) && $type === 'string') || diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index a172b36..b4e51ae 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -1734,21 +1734,6 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsClassName $callable($request); } - public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresInvalidClassName(): void - { - $request = new ServerRequest('GET', 'http://example.com/'); - - $container = new Container([ - \stdClass::class => function (self $instance) { return $instance; } - ]); - - $callable = $container->callable(\stdClass::class); - - $this->expectException(\Error::class); - $this->expectExceptionMessage('Class self not found'); - $callable($request); - } - public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresUntypedArgument(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -2831,6 +2816,106 @@ public function testGetObjectReturnsDefaultStdclassInstanceWhenFactoryFunctionUs $this->assertInstanceOf(\stdClass::class, $ret); } + public function testGetObjectReturnsDefaultInstanceWhenFactoryFunctionHasSelfArgument(): void + { + $controller = new class { + public static function factory(): \Closure + { + return function (self $instance): self { return $instance; }; + } + }; + + $container = new Container([ + get_class($controller) => $controller::factory() + ]); + + $ret = $container->getObject(get_class($controller)); + + $this->assertInstanceOf(get_class($controller), $ret); + } + + public function testGetObjectReturnsDefaultStdclassInstanceWhenFactoryFunctionHasParentArgument(): void + { + $controller = new class extends \stdClass { + public static function factory(): \Closure + { + return function (parent $instance) { return $instance; }; + } + }; + + $container = new Container([ + \stdClass::class => $controller::factory() + ]); + + $ret = $container->getObject(\stdClass::class); + + $this->assertEquals(new \stdClass(), $ret); + } + + public function testGetObjectReturnsInstanceFromConfigWhenFactoryFunctionHasSelfArgument(): void + { + $controller = new class { + public static function factory(): \Closure + { + return function (self $instance): self { return $instance; }; + } + }; + + $container = new Container([ + get_class($controller) => $controller::factory(), + 'instance' => $controller + ]); + + $ret = $container->getObject(get_class($controller)); + + $this->assertSame($controller, $ret); + } + + public function testGetObjectReturnsStdclassInstanceFromConfigWhenFactoryFunctionHasParentArgument(): void + { + $controller = new class extends \stdClass { + public static function factory(): \Closure + { + return function (parent $instance) { return $instance; }; + } + }; + + $instance = new \stdClass(); + $container = new Container([ + \stdClass::class => $controller::factory(), + 'instance' => $instance + ]); + + $ret = $container->getObject(\stdClass::class); + + $this->assertSame($instance, $ret); + } + + /** + * @requires PHP 8 + */ + public function testGetObjectReturnsInstanceFromConfigWhenFactoryFunctionHasSelfUnionArgument(): void + { + $controller = new class { + public static function factory(): \Closure + { + $fn = function () { }; // PHP < 8.0 workaround ignoring assignment on next line + $fn = #[PHP8] function (self|\stdClass $instance) { return $instance; }; + $fn = $fn; + return $fn; + } + }; + + $container = new Container([ + get_class($controller) => $controller::factory(), + 'instance' => $controller + ]); + + $ret = $container->getObject(get_class($controller)); + + $this->assertSame($controller, $ret); + } + public function testGetObjectReturnsSelfContainerByDefault(): void { $container = new Container([]);