From 9af8af8ae7c9547ababbbfcf1cef7ff5a607484f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Dec 2025 14:01:10 +0100 Subject: [PATCH 1/2] Allow recursive container calls from factory functions for variables --- src/Container.php | 6 ++---- tests/ContainerTest.php | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Container.php b/src/Container.php index 4f076ae..d3c6864 100644 --- a/src/Container.php +++ b/src/Container.php @@ -395,17 +395,15 @@ private function loadVariable(string $name, int $depth = 64) /*: object|string|i \assert($factory instanceof \Closure); $closure = new \ReflectionFunction($factory); - // build list of factory parameters based on parameter types + // invoke factory with list of parameters // temporarily unset factory reference to allow loading recursive variables from environment try { unset($this->container[$name]); - $params = $this->loadFunctionParams($closure, $depth - 1, true, '$' . $name); + $value = $factory(...$this->loadFunctionParams($closure, $depth - 1, true, '$' . $name)); } finally { $this->container[$name] = $factory; } - // invoke factory with list of parameters - $value = $factory(...$params); if (!\is_object($value) && !\is_scalar($value) && $value !== null) { throw new \TypeError( 'Return value of ' . self::functionName($closure) . ' for $' . $name . ' must be of type object|string|int|float|bool|null, ' . $this->gettype($value) . ' returned' diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index dd6b97b..ad080de 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -2126,6 +2126,28 @@ public function testGetEnvReturnsStringFromRecursiveFactoryWithDefaultValueIfNot $this->assertEquals('foo', $container->getEnv('X_FOO')); } + public function testGetEnvReturnsNullIfFactoryFunctionUsesRecursiveGetEnvForVariableNotSetInGlobalEnv(): void + { + $container = new Container([ + 'X_FOO' => function (Container $container) { return $container->getEnv('X_FOO'); } + ]); + + $this->assertNull($container->getEnv('X_FOO')); + } + + public function testGetEnvReturnsStringIfFactoryFunctionUsesRecursiveGetEnvForVariableSetInGlobalEnv(): void + { + $container = new Container([ + 'X_FOO' => function (Container $container) { return $container->getEnv('X_FOO'); } + ]); + + $_ENV['X_FOO'] = 'foo'; + $ret = $container->getEnv('X_FOO'); + unset($_ENV['X_FOO']); + + $this->assertEquals('foo', $ret); + } + public function testGetEnvReturnsStringFromPsrContainer(): void { $psr = $this->createMock(ContainerInterface::class); From 61b9193163e03bfabde9166486c7283842d70996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 19 Dec 2025 14:18:41 +0100 Subject: [PATCH 2/2] Allow recursive container calls from factory functions for classes --- src/Container.php | 13 +++-- tests/ContainerTest.php | 103 +++++++++++++++++++++++++++++++--------- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/Container.php b/src/Container.php index d3c6864..b6ea718 100644 --- a/src/Container.php +++ b/src/Container.php @@ -245,12 +245,17 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) $this->container[$name] = $value; } elseif ($this->container[$name] instanceof \Closure) { - // build list of factory parameters based on parameter types - $closure = new \ReflectionFunction($this->container[$name]); - $params = $this->loadFunctionParams($closure, $depth, true, \explode("\0", $name)[0]); + $factory = $this->container[$name]; + $closure = new \ReflectionFunction($factory); // invoke factory with list of parameters - $value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params); + // temporarily unset factory reference to allow loading recursive variables from environment + try { + unset($this->container[$name]); + $value = $factory(...$this->loadFunctionParams($closure, $depth, true, \explode("\0", $name)[0])); + } finally { + $this->container[$name] = $factory; + } if (\is_string($value)) { if ($depth < 1) { diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index ad080de..f2e3f80 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -1305,7 +1305,7 @@ public function __construct(\stdClass $data) $callable = $container->callable(get_class($controller)); $this->expectException(\Error::class); - $this->expectExceptionMessage('Argument #1 ($stdClass) of {closure:' . __FILE__ . ':' . $line .'}() for $stdClass requires container config with type string, none given'); + $this->expectExceptionMessage('Argument #1 ($stdClass) of {closure:' . __FILE__ . ':' . $line .'}() for stdClass requires container config with type string, none given'); $callable($request); } @@ -1820,22 +1820,6 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresUndefine $callable($request); } - public function testCallableReturnsCallableThatThrowsWhenFactoryRequiresRecursiveClass(): void - { - $request = new ServerRequest('GET', 'http://example.com/'); - - $line = __LINE__ + 2; - $container = new Container([ - \stdClass::class => function (\stdClass $data) { return $data; } - ]); - - $callable = $container->callable(\stdClass::class); - - $this->expectException(\Error::class); - $this->expectExceptionMessage('Argument #1 ($data) of {closure:' . __FILE__ . ':' . $line .'}() for stdClass is recursive'); - $callable($request); - } - public function testCallableReturnsCallableThatThrowsWhenFactoryIsRecursive(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -2614,6 +2598,45 @@ public function testGetObjectReturnsAccessLogHandlerInstanceFromConfig(): void $this->assertSame($accessLogHandler, $ret); } + public function testGetObjectReturnsAccessLogHandlerInstanceFromFactoryFunction(): void + { + $accessLogHandler = new AccessLogHandler(); + + $container = new Container([ + AccessLogHandler::class => function () use ($accessLogHandler) { + return $accessLogHandler; + } + ]); + + $ret = $container->getObject(AccessLogHandler::class); + + $this->assertSame($accessLogHandler, $ret); + } + + public function testGetObjectReturnsDefaultStdclassInstanceWhenFactoryFunctionHasRecursiveArgument(): void + { + $container = new Container([ + \stdClass::class => function (\stdClass $object) { return $object; } + ]); + + $ret = $container->getObject(\stdClass::class); + + $this->assertInstanceOf(\stdClass::class, $ret); + } + + public function testGetObjectReturnsDefaultStdclassInstanceWhenFactoryFunctionUsesRecursiveGetObject(): void + { + $container = new Container([ + \stdClass::class => function (Container $container) { + return $container->getObject(\stdClass::class); + } + ]); + + $ret = $container->getObject(\stdClass::class); + + $this->assertInstanceOf(\stdClass::class, $ret); + } + public function testGetObjectReturnsSelfContainerByDefault(): void { $container = new Container([]); @@ -2623,6 +2646,32 @@ public function testGetObjectReturnsSelfContainerByDefault(): void $this->assertSame($container, $ret); } + public function testGetObjectReturnsSelfContainerIfFactoryFunctionHasRecursiveContainerArgument(): void + { + $container = new Container([ + Container::class => function (Container $container): Container { + return $container; + } + ]); + + $ret = $container->getObject(Container::class); + + $this->assertSame($container, $ret); + } + + public function testGetObjectReturnsSelfContainerIfFactoryFunctionUsesRecursiveGetObject(): void + { + $container = new Container([ + Container::class => function (Container $container): Container { + return $container->getObject(Container::class); + } + ]); + + $ret = $container->getObject(Container::class); + + $this->assertSame($container, $ret); + } + public function testGetObjectReturnsOtherContainerFromConfig(): void { $other = new Container(); @@ -2785,18 +2834,26 @@ public function testGetObjectThrowsIfFactoryFunctionIsRecursive(): void $container->getObject(AccessLogHandler::class); } - public function testGetObjectThrowsIfFactoryFunctionHasRecursiveContainerArgument(): void + public function testGetObjectThrowsIfConstructorRequiresContainerConfig(): void + { + $container = new Container([]); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($path) of ' . LogStreamHandler::class . '::__construct() requires container config with type string, none given'); + $container->getObject(LogStreamHandler::class); + } + + public function testGetObjectThrowsIfFactoryFunctionHasClassArgumentWithConstructorThatRequiresContainerConfig(): void { - $line = __LINE__ + 2; $container = new Container([ - Container::class => function (Container $container): Container { - return $container; + AccessLogHandler::class => function (LogStreamHandler $log) { + return new AccessLogHandler(); } ]); $this->expectException(\Error::class); - $this->expectExceptionMessage('Argument #1 ($container) of {closure:' . __FILE__ . ':' . $line .'}() for FrameworkX\Container is recursive'); - $container->getObject(Container::class); + $this->expectExceptionMessage('Argument #1 ($path) of ' . LogStreamHandler::class . '::__construct() requires container config with type string, none given'); + $container->getObject(AccessLogHandler::class); } public function testGetObjectThrowsIfConfigReferencesInterface(): void