diff --git a/src/App.php b/src/App.php index 33468d5..ca6c900 100644 --- a/src/App.php +++ b/src/App.php @@ -53,10 +53,8 @@ public function __construct(...$middleware) $needsErrorHandlerNext = false; foreach ($middleware as $handler) { // load AccessLogHandler and ErrorHandler instance from last Container - if ($handler === AccessLogHandler::class) { - $handler = $container->getAccessLogHandler(); - } elseif ($handler === ErrorHandler::class) { - $handler = $container->getErrorHandler(); + if ($handler === AccessLogHandler::class || $handler === ErrorHandler::class) { + $handler = $container->getObject($handler); } // ensure AccessLogHandler is always followed by ErrorHandler @@ -99,12 +97,12 @@ public function __construct(...$middleware) // add default ErrorHandler as first handler unless it is already added explicitly if ($needsErrorHandler instanceof Container) { - \array_unshift($handlers, $needsErrorHandler->getErrorHandler()); + \array_unshift($handlers, $needsErrorHandler->getObject(ErrorHandler::class)); } // only log for built-in webserver and PHP development webserver by default, others have their own access log if ($needsAccessLog instanceof Container) { - $handler = $needsAccessLog->getAccessLogHandler(); + $handler = $needsAccessLog->getObject(AccessLogHandler::class); if (!$handler->isDevNull()) { \array_unshift($handlers, $handler); } diff --git a/src/Container.php b/src/Container.php index eeeaaa8..789e90d 100644 --- a/src/Container.php +++ b/src/Container.php @@ -135,45 +135,39 @@ public function getEnv(string $name): ?string } /** + * [Internal] Get an object of given class from container + * + * @template T of object + * @param class-string $class + * @return object returns an instance of given $class or throws if it can not be instantiated + * @phpstan-return T * @throws \TypeError if container config or factory returns an unexpected type + * @throws \Error if object of type $class can not be loaded * @throws \Throwable if container factory function throws unexpected exception * @internal */ - public function getAccessLogHandler(): AccessLogHandler + public function getObject(string $class) /*: object (PHP 7.2+) */ { - if ($this->container instanceof ContainerInterface) { - if ($this->container->has(AccessLogHandler::class)) { - // @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError` - return $this->container->get(AccessLogHandler::class); - } else { - return new AccessLogHandler(); + if ($this->container instanceof ContainerInterface && $this->container->has($class)) { + $value = $this->container->get($class); + if (!$value instanceof $class) { + throw new \TypeError( + 'Return value of ' . \explode("\0", \get_class($this->container))[0] . '::get() for ' . $class . ' must be of type ' . $class . ', ' . $this->gettype($value) . ' returned' + ); } + return $value; + } elseif ($this->container instanceof ContainerInterface) { + return new $class(); } - return $this->loadObject(AccessLogHandler::class); - } - /** - * @throws \TypeError if container config or factory returns an unexpected type - * @throws \Throwable if container factory function throws unexpected exception - * @internal - */ - public function getErrorHandler(): ErrorHandler - { - if ($this->container instanceof ContainerInterface) { - if ($this->container->has(ErrorHandler::class)) { - // @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError` - return $this->container->get(ErrorHandler::class); - } else { - return new ErrorHandler(); - } - } - return $this->loadObject(ErrorHandler::class); + return $this->loadObject($class); } /** * @template T of object * @param class-string $name - * @return T + * @return object returns an instance of given class $name or throws if it can not be instantiated + * @phpstan-return T * @throws \TypeError if container config or factory returns an unexpected type * @throws \Error if object of type $name can not be loaded * @throws \Throwable if container factory function throws unexpected exception diff --git a/tests/AppTest.php b/tests/AppTest.php index 0fe731d..0fc88fb 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -68,8 +68,10 @@ public function testConstructWithContainerAssignsDefaultHandlersAndContainerForR $errorHandler = new ErrorHandler(); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getAccessLogHandler')->willReturn($accessLogHandler); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($container instanceof Container); $app = new App($container); @@ -105,8 +107,15 @@ public function testConstructWithContainerAndMiddlewareClassNameAssignsCallableF { $middleware = function (ServerRequestInterface $request, callable $next) { }; + $accessLogHandler = new AccessLogHandler(); + $errorHandler = new ErrorHandler(); + $container = $this->createMock(Container::class); $container->expects($this->once())->method('callable')->with('stdClass')->willReturn($middleware); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($container instanceof Container); $app = new App($container, \stdClass::class); @@ -126,8 +135,8 @@ public function testConstructWithContainerAndMiddlewareClassNameAssignsCallableF assert(is_array($handlers)); $this->assertCount(4, $handlers); - $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); - $this->assertInstanceOf(ErrorHandler::class, $handlers[1]); + $this->assertSame($accessLogHandler, $handlers[0]); + $this->assertSame($errorHandler, $handlers[1]); $this->assertSame($middleware, $handlers[2]); $this->assertInstanceOf(RouteHandler::class, $handlers[3]); @@ -217,10 +226,14 @@ public function testConstructWithContainerAndErrorHandlerAssignsErrorHandlerAfte public function testConstructWithContainerAndErrorHandlerClassAssignsErrorHandlerFromContainerAfterDefaultAccessLogHandler(): void { + $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($container instanceof Container); $app = new App($container, ErrorHandler::class); @@ -240,20 +253,24 @@ public function testConstructWithContainerAndErrorHandlerClassAssignsErrorHandle assert(is_array($handlers)); $this->assertCount(3, $handlers); - $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); $this->assertInstanceOf(RouteHandler::class, $handlers[2]); } public function testConstructWithMultipleContainersAndErrorHandlerClassAssignsErrorHandlerFromLastContainerBeforeErrorHandlerAfterDefaultAccessLogHandler(): void { + $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); $unused = $this->createMock(Container::class); - $unused->expects($this->never())->method('getErrorHandler'); + $unused->expects($this->never())->method('getObject'); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($unused instanceof Container); assert($container instanceof Container); @@ -274,7 +291,7 @@ public function testConstructWithMultipleContainersAndErrorHandlerClassAssignsEr assert(is_array($handlers)); $this->assertCount(3, $handlers); - $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); $this->assertInstanceOf(RouteHandler::class, $handlers[2]); } @@ -282,13 +299,17 @@ public function testConstructWithMultipleContainersAndErrorHandlerClassAssignsEr public function testConstructWithMultipleContainersAndMiddlewareAssignsErrorHandlerFromLastContainerBeforeMiddlewareAfterDefaultAccessLogHandler(): void { $middleware = function (ServerRequestInterface $request, callable $next) { }; + $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); $unused = $this->createMock(Container::class); - $unused->expects($this->never())->method('getErrorHandler'); + $unused->expects($this->never())->method('getObject'); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($unused instanceof Container); assert($container instanceof Container); @@ -309,7 +330,7 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsErrorHand assert(is_array($handlers)); $this->assertCount(4, $handlers); - $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); $this->assertSame($middleware, $handlers[2]); $this->assertInstanceOf(RouteHandler::class, $handlers[3]); @@ -350,15 +371,19 @@ public function testConstructWithMultipleContainersAndMiddlewareAndErrorHandlerC $middleware = function (ServerRequestInterface $request, callable $next) { }; $unused = $this->createMock(Container::class); - $unused->expects($this->never())->method('getErrorHandler'); + $unused->expects($this->never())->method('getObject'); + $accessLogHandler = new AccessLogHandler(); $errorHandler1 = new ErrorHandler(); $container1 = $this->createMock(Container::class); - $container1->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler1); + $container1->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler1], + ]); $errorHandler2 = new ErrorHandler(); $container2 = $this->createMock(Container::class); - $container2->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler2); + $container2->expects($this->exactly(1))->method('getObject')->with(ErrorHandler::class)->willReturn($errorHandler2); assert($unused instanceof Container); assert($container1 instanceof Container); @@ -380,7 +405,7 @@ public function testConstructWithMultipleContainersAndMiddlewareAndErrorHandlerC assert(is_array($handlers)); $this->assertCount(5, $handlers); - $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler1, $handlers[1]); $this->assertSame($middleware, $handlers[2]); $this->assertSame($errorHandler2, $handlers[3]); @@ -473,8 +498,10 @@ public function testConstructWithContainerAndAccessLogHandlerClassAndErrorHandle $errorHandler = new ErrorHandler(); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getAccessLogHandler')->willReturn($accessLogHandler); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($container instanceof Container); $app = new App($container, AccessLogHandler::class, ErrorHandler::class); @@ -507,8 +534,10 @@ public function testConstructWithContainerAndAccessLogHandlerClassAndErrorHandle $errorHandler = new ErrorHandler(); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getAccessLogHandler')->willReturn($accessLogHandler); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($container instanceof Container); $app = new App($container, AccessLogHandler::class, ErrorHandler::class); @@ -569,11 +598,13 @@ public function testConstructWithMultipleContainersAndAccessLogHandlerClassAndEr $errorHandler = new ErrorHandler(); $unused = $this->createMock(Container::class); - $unused->expects($this->never())->method('getErrorHandler'); + $unused->expects($this->never())->method('getObject'); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getAccessLogHandler')->willReturn($accessLogHandler); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($unused instanceof Container); assert($container instanceof Container); @@ -608,12 +639,13 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsDefaultHa $errorHandler = new ErrorHandler(); $unused = $this->createMock(Container::class); - $unused->expects($this->never())->method('getAccessLogHandler'); - $unused->expects($this->never())->method('getErrorHandler'); + $unused->expects($this->never())->method('getObject'); $container = $this->createMock(Container::class); - $container->expects($this->once())->method('getAccessLogHandler')->willReturn($accessLogHandler); - $container->expects($this->once())->method('getErrorHandler')->willReturn($errorHandler); + $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + [AccessLogHandler::class, $accessLogHandler], + [ErrorHandler::class, $errorHandler], + ]); assert($unused instanceof Container); assert($container instanceof Container); diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index d6166ab..bc5973b 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -2510,7 +2510,7 @@ public function testGetEnvThrowsWhenFactoryFunctionExpectsIntArgumentButGivenAno $container->getEnv('X_FOO'); } - public function testGetEnvThrowsIfMapPsrContainerReturnsInvalidType(): void + public function testGetEnvThrowsIfPsrContainerReturnsInvalidType(): void { $psr = $this->createMock(ContainerInterface::class); $psr->expects($this->once())->method('has')->with('X_FOO')->willReturn(true); @@ -2524,16 +2524,25 @@ public function testGetEnvThrowsIfMapPsrContainerReturnsInvalidType(): void $container->getEnv('X_FOO'); } - public function testGetAccessLogHandlerReturnsDefaultAccessLogHandlerInstance(): void + public function testGetObjectReturnsDefaultAccessLogHandlerInstance(): void { $container = new Container([]); - $accessLogHandler = $container->getAccessLogHandler(); + $accessLogHandler = $container->getObject(AccessLogHandler::class); $this->assertInstanceOf(AccessLogHandler::class, $accessLogHandler); } - public function testGetAccessLogHandlerReturnsAccessLogHandlerInstanceFromMap(): void + public function testGetObjectReturnsDefaultErrorHandlerInstance(): void + { + $container = new Container([]); + + $errorHandler = $container->getObject(ErrorHandler::class); + + $this->assertInstanceOf(ErrorHandler::class, $errorHandler); + } + + public function testGetObjectReturnsAccessLogHandlerInstanceFromConfig(): void { $accessLogHandler = new AccessLogHandler(); @@ -2541,12 +2550,12 @@ public function testGetAccessLogHandlerReturnsAccessLogHandlerInstanceFromMap(): AccessLogHandler::class => $accessLogHandler ]); - $ret = $container->getAccessLogHandler(); + $ret = $container->getObject(AccessLogHandler::class); $this->assertSame($accessLogHandler, $ret); } - public function testGetAccessLogHandlerReturnsAccessLogHandlerInstanceFromPsrContainer(): void + public function testGetObjectReturnsAccessLogHandlerInstanceFromPsrContainer(): void { $accessLogHandler = new AccessLogHandler(); @@ -2557,12 +2566,12 @@ public function testGetAccessLogHandlerReturnsAccessLogHandlerInstanceFromPsrCon assert($psr instanceof ContainerInterface); $container = new Container($psr); - $ret = $container->getAccessLogHandler(); + $ret = $container->getObject(AccessLogHandler::class); $this->assertSame($accessLogHandler, $ret); } - public function testGetAccessLogHandlerReturnsDefaultAccessLogHandlerInstanceIfPsrContainerHasNoEntry(): void + public function testGetObjectReturnsDefaultAccessLogHandlerInstanceIfPsrContainerHasNoEntry(): void { $psr = $this->createMock(ContainerInterface::class); $psr->expects($this->once())->method('has')->with(AccessLogHandler::class)->willReturn(false); @@ -2571,12 +2580,12 @@ public function testGetAccessLogHandlerReturnsDefaultAccessLogHandlerInstanceIfP assert($psr instanceof ContainerInterface); $container = new Container($psr); - $accessLogHandler = $container->getAccessLogHandler(); + $accessLogHandler = $container->getObject(AccessLogHandler::class); $this->assertInstanceOf(AccessLogHandler::class, $accessLogHandler); } - public function testGetAccessLogHandlerThrowsIfFactoryFunctionThrows(): void + public function testGetObjectThrowsIfFactoryFunctionThrows(): void { $container = new Container([ AccessLogHandler::class => function () { @@ -2586,10 +2595,10 @@ public function testGetAccessLogHandlerThrowsIfFactoryFunctionThrows(): void $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Demo'); - $container->getAccessLogHandler(); + $container->getObject(AccessLogHandler::class); } - public function testGetAccessLogHandlerThrowsIfFactoryFunctionReturnsInvalidValue(): void + public function testGetObjectThrowsIfFactoryFunctionReturnsInvalidValue(): void { $line = __LINE__ + 2; $container = new Container([ @@ -2600,10 +2609,10 @@ public function testGetAccessLogHandlerThrowsIfFactoryFunctionReturnsInvalidValu $this->expectException(\TypeError::class); $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for FrameworkX\AccessLogHandler must be of type FrameworkX\AccessLogHandler, null returned'); - $container->getAccessLogHandler(); + $container->getObject(AccessLogHandler::class); } - public function testGetAccessLogHandlerThrowsIfConfigIsRecursive(): void + public function testGetObjectThrowsIfConfigIsRecursive(): void { $container = new Container([ AccessLogHandler::class => AccessLogHandler::class @@ -2611,10 +2620,10 @@ public function testGetAccessLogHandlerThrowsIfConfigIsRecursive(): void $this->expectException(\Error::class); $this->expectExceptionMessage('Container config for FrameworkX\AccessLogHandler is recursive'); - $container->getAccessLogHandler(); + $container->getObject(AccessLogHandler::class); } - public function testGetAccessLogHandlerThrowsIfFactoryFunctionIsRecursive(): void + public function testGetObjectThrowsIfFactoryFunctionIsRecursive(): void { $container = new Container([ AccessLogHandler::class => function () { @@ -2624,10 +2633,10 @@ public function testGetAccessLogHandlerThrowsIfFactoryFunctionIsRecursive(): voi $this->expectException(\Error::class); $this->expectExceptionMessage('Container config for FrameworkX\AccessLogHandler is recursive'); - $container->getAccessLogHandler(); + $container->getObject(AccessLogHandler::class); } - public function testGetAccessLogHandlerThrowsIfConfigReferencesInterface(): void + public function testGetObjectThrowsIfConfigReferencesInterface(): void { $container = new Container([ AccessLogHandler::class => \Iterator::class @@ -2635,86 +2644,44 @@ public function testGetAccessLogHandlerThrowsIfConfigReferencesInterface(): void $this->expectException(\Error::class); $this->expectExceptionMessage('Cannot instantiate interface Iterator'); - $container->getAccessLogHandler(); + $container->getObject(AccessLogHandler::class); } - public function testGetErrorHandlerReturnsDefaultErrorHandlerInstance(): void + public function testGetObjectThrowsWhenTryingToInstantiateInterface(): void { $container = new Container([]); - $errorHandler = $container->getErrorHandler(); - - $this->assertInstanceOf(ErrorHandler::class, $errorHandler); - } - - public function testGetErrorHandlerReturnsErrorHandlerInstanceFromMap(): void - { - $errorHandler = new ErrorHandler(); - - $container = new Container([ - ErrorHandler::class => $errorHandler - ]); - - $ret = $container->getErrorHandler(); - - $this->assertSame($errorHandler, $ret); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Cannot instantiate interface Iterator'); + $container->getObject(\Iterator::class); } - public function testGetErrorHandlerReturnsErrorHandlerInstanceFromPsrContainer(): void + public function testGetObjectThrowsWhenTryingToInstantiateInterfaceWithPsrContainer(): void { - $errorHandler = new ErrorHandler(); - $psr = $this->createMock(ContainerInterface::class); - $psr->expects($this->once())->method('has')->with(ErrorHandler::class)->willReturn(true); - $psr->expects($this->once())->method('get')->with(ErrorHandler::class)->willReturn($errorHandler); + $psr->expects($this->once())->method('has')->with(\Iterator::class)->willReturn(false); + $psr->expects($this->never())->method('get'); assert($psr instanceof ContainerInterface); $container = new Container($psr); - $ret = $container->getErrorHandler(); - - $this->assertSame($errorHandler, $ret); + $this->expectException(\Error::class); + $this->expectExceptionMessage('Cannot instantiate interface Iterator'); + $container->getObject(\Iterator::class); } - public function testGetErrorHandlerReturnsDefaultErrorHandlerInstanceIfPsrContainerHasNoEntry(): void + public function testGetObjectThrowsIfPsrContainerReturnsWrongType(): void { $psr = $this->createMock(ContainerInterface::class); - $psr->expects($this->once())->method('has')->with(ErrorHandler::class)->willReturn(false); - $psr->expects($this->never())->method('get'); + $psr->expects($this->once())->method('has')->with(AccessLogHandler::class)->willReturn(true); + $psr->expects($this->once())->method('get')->with(AccessLogHandler::class)->willReturn(42); assert($psr instanceof ContainerInterface); $container = new Container($psr); - $errorHandler = $container->getErrorHandler(); - - $this->assertInstanceOf(ErrorHandler::class, $errorHandler); - } - - public function testGetErrorHandlerThrowsIfFactoryFunctionThrows(): void - { - $container = new Container([ - ErrorHandler::class => function () { - throw new \RuntimeException('Demo'); - } - ]); - - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Demo'); - $container->getErrorHandler(); - } - - public function testGetErrorHandlerThrowsIfFactoryFunctionReturnsInvalidValue(): void - { - $line = __LINE__ + 2; - $container = new Container([ - ErrorHandler::class => function () { - return null; - } - ]); - $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Return value of {closure:' . __FILE__ . ':' . $line . '}() for FrameworkX\ErrorHandler must be of type FrameworkX\ErrorHandler, null returned'); - $container->getErrorHandler(); + $this->expectExceptionMessage('Return value of ' . get_class($psr) . '::get() for FrameworkX\AccessLogHandler must be of type FrameworkX\AccessLogHandler, int returned'); + $container->getObject(AccessLogHandler::class); } public function testInvokeContainerAsMiddlewareReturnsFromNextRequestHandler(): void