diff --git a/src/App.php b/src/App.php index ca6c900..b1a7d44 100644 --- a/src/App.php +++ b/src/App.php @@ -52,8 +52,8 @@ public function __construct(...$middleware) if ($middleware) { $needsErrorHandlerNext = false; foreach ($middleware as $handler) { - // load AccessLogHandler and ErrorHandler instance from last Container - if ($handler === AccessLogHandler::class || $handler === ErrorHandler::class) { + // load required internal classes from last Container + if (\in_array($handler, [AccessLogHandler::class, ErrorHandler::class, Container::class], true)) { $handler = $container->getObject($handler); } diff --git a/src/Container.php b/src/Container.php index 789e90d..16a820e 100644 --- a/src/Container.php +++ b/src/Container.php @@ -76,6 +76,9 @@ public function __invoke(ServerRequestInterface $request, ?callable $next = null */ public function callable(string $class): callable { + // may be any class name except AccessLogHandler or Container itself + \assert(!\in_array($class, [AccessLogHandler::class, self::class], true)); + return function (ServerRequestInterface $request, ?callable $next = null) use ($class) { try { if ($this->container instanceof ContainerInterface) { @@ -157,6 +160,10 @@ public function getObject(string $class) /*: object (PHP 7.2+) */ } return $value; } elseif ($this->container instanceof ContainerInterface) { + // fallback for missing required internal classes from PSR-11 adapter + if ($class === Container::class) { + return $this; // @phpstan-ignore-line returns instanceof `T` + } return new $class(); } @@ -223,6 +230,9 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) \assert($this->container[$name] instanceof $name); return $this->container[$name]; + } elseif ($name === self::class) { + // return container itself for self-references unless explicitly configured (see above) + return $this; // @phpstan-ignore-line returns instanceof `T` } // Check `$name` references a valid class name that can be autoloaded diff --git a/src/Io/RouteHandler.php b/src/Io/RouteHandler.php index 0e893e5..6343451 100644 --- a/src/Io/RouteHandler.php +++ b/src/Io/RouteHandler.php @@ -55,6 +55,11 @@ public function map(array $methods, string $route, $handler, ...$handlers): void $last = \key($handlers); $container = $this->container; foreach ($handlers as $i => $handler) { + // unlikely: load container self-reference from container + if ($handler === Container::class) { + $handlers[$i] = $handler = $container->getObject($handler); + } + if ($handler instanceof Container && $i !== $last) { $container = $handler; unset($handlers[$i]); diff --git a/tests/AppTest.php b/tests/AppTest.php index 0fc88fb..5bbcfbe 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -642,14 +642,15 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsDefaultHa $unused->expects($this->never())->method('getObject'); $container = $this->createMock(Container::class); - $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + $container->expects($this->exactly(3))->method('getObject')->willReturnMap([ [AccessLogHandler::class, $accessLogHandler], [ErrorHandler::class, $errorHandler], + [Container::class, $container], ]); assert($unused instanceof Container); assert($container instanceof Container); - $app = new App($unused, $container, $middleware, $unused); + $app = new App($unused, $container, Container::class, $middleware, $unused); $ref = new ReflectionProperty($app, 'handler'); if (PHP_VERSION_ID < 80100) { diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index bc5973b..014c308 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -289,6 +289,40 @@ public function __invoke(ServerRequestInterface $request): Response $this->assertEquals('{"name":"Alice"}', (string) $response->getBody()); } + public function testCallableReturnsCallableForClassNameViaAutowiringWithFactoryFunctionWithContainerDependency(): void + { + $request = new ServerRequest('GET', 'http://example.com/'); + + $controller = new class(new \stdClass()) { + /** @var \stdClass */ + private $data; + + public function __construct(\stdClass $data) + { + $this->data = $data; + } + + public function __invoke(ServerRequestInterface $request): Response + { + return new Response(200, [], (string) json_encode($this->data)); + } + }; + + $container = new Container([ + \stdClass::class => function (Container $container) { + return (object)['container' => spl_object_hash($container)]; + } + ]); + + $callable = $container->callable(get_class($controller)); + $this->assertInstanceOf(\Closure::class, $callable); + + $response = $callable($request); + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('{"container":"' . spl_object_hash($container) . '"}', (string) $response->getBody()); + } + public function testCallableTwiceReturnsCallableForClassNameViaAutowiringWithFactoryFunctionForDependencyWillCallFactoryOnlyOnce(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -2555,6 +2589,43 @@ public function testGetObjectReturnsAccessLogHandlerInstanceFromConfig(): void $this->assertSame($accessLogHandler, $ret); } + public function testGetObjectReturnsSelfContainerByDefault(): void + { + $container = new Container([]); + + $ret = $container->getObject(Container::class); + + $this->assertSame($container, $ret); + } + + public function testGetObjectReturnsOtherContainerFromConfig(): void + { + $other = new Container(); + + $container = new Container([ + Container::class => $other + ]); + + $ret = $container->getObject(Container::class); + + $this->assertSame($other, $ret); + } + + public function testGetObjectReturnsOtherContainerFromFactoryFunction(): void + { + $other = new Container(); + + $container = new Container([ + Container::class => function () use ($other) { + return $other; + } + ]); + + $ret = $container->getObject(Container::class); + + $this->assertSame($other, $ret); + } + public function testGetObjectReturnsAccessLogHandlerInstanceFromPsrContainer(): void { $accessLogHandler = new AccessLogHandler(); @@ -2585,6 +2656,20 @@ public function testGetObjectReturnsDefaultAccessLogHandlerInstanceIfPsrContaine $this->assertInstanceOf(AccessLogHandler::class, $accessLogHandler); } + public function testGetObjectReturnsSelfContainerIfPsrContainerHasNoEntry(): void + { + $psr = $this->createMock(ContainerInterface::class); + $psr->expects($this->once())->method('has')->with(Container::class)->willReturn(false); + $psr->expects($this->never())->method('get'); + + assert($psr instanceof ContainerInterface); + $container = new Container($psr); + + $ret = $container->getObject(Container::class); + + $this->assertSame($container, $ret); + } + public function testGetObjectThrowsIfFactoryFunctionThrows(): void { $container = new Container([ @@ -2636,6 +2721,20 @@ public function testGetObjectThrowsIfFactoryFunctionIsRecursive(): void $container->getObject(AccessLogHandler::class); } + public function testGetObjectThrowsIfFactoryFunctionHasRecursiveContainerArgument(): void + { + $line = __LINE__ + 2; + $container = new Container([ + Container::class => function (Container $container): Container { + return $container; + } + ]); + + $this->expectException(\Error::class); + $this->expectExceptionMessage('Argument #1 ($container) of {closure:' . __FILE__ . ':' . $line .'}() for FrameworkX\Container is recursive'); + $container->getObject(Container::class); + } + public function testGetObjectThrowsIfConfigReferencesInterface(): void { $container = new Container([ diff --git a/tests/Io/RouteHandlerTest.php b/tests/Io/RouteHandlerTest.php index 69a3893..f01327a 100644 --- a/tests/Io/RouteHandlerTest.php +++ b/tests/Io/RouteHandlerTest.php @@ -113,6 +113,31 @@ public function testMapRouteWithContainerAndControllerClassNameAddsRouteOnRouter $handler->map(['GET'], '/', $container, \stdClass::class); } + public function testMapRouteWithContainerClassNameAndControllerClassNameAddsRouteOnRouterWithControllerCallableFromOtherContainer(): void + { + $controller = function () { }; + + $other = $this->createMock(Container::class); + $other->expects($this->once())->method('callable')->with('stdClass')->willReturn($controller); + + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('getObject')->with(Container::class)->willReturn($other); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + + $router = $this->createMock(RouteCollector::class); + $router->expects($this->once())->method('addRoute')->with(['GET'], '/', $controller); + + $ref = new \ReflectionProperty($handler, 'routeCollector'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $ref->setValue($handler, $router); + + $handler->map(['GET'], '/', Container::class, \stdClass::class); + } + public function testHandleRequestWithProxyRequestReturnsResponseWithMessageThatProxyRequestsAreNotAllowed(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -329,7 +354,7 @@ public function testHandleRequestWithOptionsAsteriskRequestReturnsResponseFromMa $this->assertSame($response, $ret); } - public function testHandleRequestWithContainerOnlyThrows(): void + public function testHandleRequestWithContainerInstanceOnlyThrows(): void { $request = new ServerRequest('GET', 'http://example.com/'); @@ -340,4 +365,16 @@ public function testHandleRequestWithContainerOnlyThrows(): void $this->expectExceptionMessage('Container should not be used as final request handler'); $handler($request); } + + public function testHandleRequestWithContainerClassOnlyThrows(): void + { + $request = new ServerRequest('GET', 'http://example.com/'); + + $handler = new RouteHandler(); + $handler->map(['GET'], '/', Container::class); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Container should not be used as final request handler'); + $handler($request); + } }