diff --git a/src/App.php b/src/App.php index b1a7d44..173d2fb 100644 --- a/src/App.php +++ b/src/App.php @@ -49,11 +49,19 @@ public function __construct(...$middleware) // only log for built-in webserver and PHP development webserver by default, others have their own access log $needsAccessLog = (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') ? $container : null; + // remember if RouteHandler is added explicitly + $router = null; + if ($middleware) { $needsErrorHandlerNext = false; foreach ($middleware as $handler) { + // if explicit RouteHandler is given, it must be last in the chain + if ($router !== null) { + throw new \TypeError('RouteHandler must not be followed by other handlers'); + } + // load required internal classes from last Container - if (\in_array($handler, [AccessLogHandler::class, ErrorHandler::class, Container::class], true)) { + if (\in_array($handler, [AccessLogHandler::class, ErrorHandler::class, Container::class, RouteHandler::class], true)) { $handler = $container->getObject($handler); } @@ -88,6 +96,9 @@ public function __construct(...$middleware) $needsAccessLog = null; $needsErrorHandlerNext = true; } + if ($handler instanceof RouteHandler) { + $router = $handler; + } } } if ($needsErrorHandlerNext) { @@ -108,8 +119,12 @@ public function __construct(...$middleware) } } - $this->router = new RouteHandler($container); - $handlers[] = $this->router; + // add default RouteHandler as last handler in middleware chain + if ($router === null) { + $handlers[] = $router = $container->getObject(RouteHandler::class); + } + + $this->router = $router; $this->handler = new MiddlewareHandler($handlers); $this->sapi = \PHP_SAPI === 'cli' ? new ReactiveHandler($container->getEnv('X_LISTEN')) : new SapiHandler(); } diff --git a/src/Container.php b/src/Container.php index 16a820e..a144dc9 100644 --- a/src/Container.php +++ b/src/Container.php @@ -2,6 +2,7 @@ namespace FrameworkX; +use FrameworkX\Io\RouteHandler; use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; @@ -163,6 +164,8 @@ public function getObject(string $class) /*: object (PHP 7.2+) */ // fallback for missing required internal classes from PSR-11 adapter if ($class === Container::class) { return $this; // @phpstan-ignore-line returns instanceof `T` + } elseif ($class === RouteHandler::class) { + return new RouteHandler($this); // @phpstan-ignore-line returns instanceof `T` } return new $class(); } diff --git a/src/Io/RouteHandler.php b/src/Io/RouteHandler.php index 6343451..2325571 100644 --- a/src/Io/RouteHandler.php +++ b/src/Io/RouteHandler.php @@ -30,11 +30,11 @@ class RouteHandler /** @var Container */ private $container; - public function __construct(?Container $container = null) + public function __construct(Container $container) { $this->routeCollector = new RouteCollector(new RouteParser(), new RouteGenerator()); $this->errorHandler = new ErrorHandler(); - $this->container = $container ?? new Container(); + $this->container = $container; } /** diff --git a/tests/AppMiddlewareTest.php b/tests/AppMiddlewareTest.php index 7380eec..0312554 100644 --- a/tests/AppMiddlewareTest.php +++ b/tests/AppMiddlewareTest.php @@ -18,171 +18,126 @@ class AppMiddlewareTest extends TestCase { public function testGetMethodWithMiddlewareAddsGetRouteOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->get('/', $middleware, $controller); } public function testHeadMethodWithMiddlewareAddsHeadRouteOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['HEAD'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->head('/', $middleware, $controller); } public function testPostMethodWithMiddlewareAddsPostRouteOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['POST'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->post('/', $middleware, $controller); } public function testPutMethodWithMiddlewareAddsPutRouteOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['PUT'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->put('/', $middleware, $controller); } public function testPatchMethodWithMiddlewareAddsPatchRouteOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['PATCH'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->patch('/', $middleware, $controller); } public function testDeleteMethodWithMiddlewareAddsDeleteRouteOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['DELETE'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->delete('/', $middleware, $controller); } public function testOptionsMethodWithMiddlewareAddsOptionsRouteOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['OPTIONS'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->options('/', $middleware, $controller); } public function testAnyMethodWithMiddlewareAddsAllHttpMethodsOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->any('/', $middleware, $controller); } public function testMapMethodWithMiddlewareAddsGivenMethodsOnRouter(): void { - $app = $this->createAppWithoutLogger(); - $middleware = function () {}; $controller = function () { }; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET', 'POST'], '/', $middleware, $controller); + assert($router instanceof RouteHandler); - $ref = new \ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->map(['GET', 'POST'], '/', $middleware, $controller); } diff --git a/tests/AppTest.php b/tests/AppTest.php index 5bbcfbe..70b2190 100644 --- a/tests/AppTest.php +++ b/tests/AppTest.php @@ -36,6 +36,30 @@ class AppTest extends TestCase { + public function testConstructAssignsDefaultMiddleware(): void + { + $app = new App(); + + $ref = new \ReflectionProperty($app, 'handler'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handler = $ref->getValue($app); + assert($handler instanceof MiddlewareHandler); + + $ref = new \ReflectionProperty($handler, 'handlers'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handlers = $ref->getValue($handler); + assert(is_array($handlers)); + + $this->assertCount(3, $handlers); + $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertInstanceOf(ErrorHandler::class, $handlers[1]); + $this->assertInstanceOf(RouteHandler::class, $handlers[2]); + } + public function testConstructWithMiddlewareAssignsGivenMiddleware(): void { $middleware = function () { }; @@ -62,15 +86,17 @@ public function testConstructWithMiddlewareAssignsGivenMiddleware(): void $this->assertInstanceOf(RouteHandler::class, $handlers[3]); } - public function testConstructWithContainerAssignsDefaultHandlersAndContainerForRouteHandlerOnly(): void + public function testConstructWithContainerMockAssignsDefaultHandlersFromContainer(): void { $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $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], + [RouteHandler::class, $routeHandler], ]); assert($container instanceof Container); @@ -93,6 +119,31 @@ public function testConstructWithContainerAssignsDefaultHandlersAndContainerForR $this->assertCount(3, $handlers); $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); + $this->assertSame($routeHandler, $handlers[2]); + } + + public function testConstructWithContainerInstanceAssignsDefaultHandlersAndContainerForRouteHandlerOnly(): void + { + $container = new Container([]); + $app = new App($container); + + $ref = new ReflectionProperty($app, 'handler'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handler = $ref->getValue($app); + assert($handler instanceof MiddlewareHandler); + + $ref = new ReflectionProperty($handler, 'handlers'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handlers = $ref->getValue($handler); + assert(is_array($handlers)); + + $this->assertCount(3, $handlers); + $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertInstanceOf(ErrorHandler::class, $handlers[1]); $this->assertInstanceOf(RouteHandler::class, $handlers[2]); $routeHandler = $handlers[2]; @@ -109,12 +160,14 @@ public function testConstructWithContainerAndMiddlewareClassNameAssignsCallableF $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $container = $this->createMock(Container::class); $container->expects($this->once())->method('callable')->with('stdClass')->willReturn($middleware); - $container->expects($this->exactly(2))->method('getObject')->willReturnMap([ + $container->expects($this->exactly(3))->method('getObject')->willReturnMap([ [AccessLogHandler::class, $accessLogHandler], [ErrorHandler::class, $errorHandler], + [RouteHandler::class, $routeHandler], ]); assert($container instanceof Container); @@ -138,14 +191,7 @@ public function testConstructWithContainerAndMiddlewareClassNameAssignsCallableF $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); $this->assertSame($middleware, $handlers[2]); - $this->assertInstanceOf(RouteHandler::class, $handlers[3]); - - $routeHandler = $handlers[3]; - $ref = new ReflectionProperty($routeHandler, 'container'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $this->assertSame($container, $ref->getValue($routeHandler)); + $this->assertSame($routeHandler, $handlers[3]); } public function testConstructWithErrorHandlerOnlyAssignsErrorHandlerAfterDefaultAccessLogHandler(): void @@ -228,11 +274,13 @@ public function testConstructWithContainerAndErrorHandlerClassAssignsErrorHandle { $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $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], + [RouteHandler::class, $routeHandler], ]); assert($container instanceof Container); @@ -255,26 +303,28 @@ public function testConstructWithContainerAndErrorHandlerClassAssignsErrorHandle $this->assertCount(3, $handlers); $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); - $this->assertInstanceOf(RouteHandler::class, $handlers[2]); + $this->assertSame($routeHandler, $handlers[2]); } public function testConstructWithMultipleContainersAndErrorHandlerClassAssignsErrorHandlerFromLastContainerBeforeErrorHandlerAfterDefaultAccessLogHandler(): void { $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $unused = $this->createMock(Container::class); $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], + [RouteHandler::class, $routeHandler], ]); assert($unused instanceof Container); assert($container instanceof Container); - $app = new App($unused, $container, ErrorHandler::class, $unused); + $app = new App($unused, $unused, $container, ErrorHandler::class); $ref = new ReflectionProperty($app, 'handler'); if (PHP_VERSION_ID < 80100) { @@ -293,7 +343,7 @@ public function testConstructWithMultipleContainersAndErrorHandlerClassAssignsEr $this->assertCount(3, $handlers); $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); - $this->assertInstanceOf(RouteHandler::class, $handlers[2]); + $this->assertSame($routeHandler, $handlers[2]); } public function testConstructWithMultipleContainersAndMiddlewareAssignsErrorHandlerFromLastContainerBeforeMiddlewareAfterDefaultAccessLogHandler(): void @@ -301,19 +351,21 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsErrorHand $middleware = function (ServerRequestInterface $request, callable $next) { }; $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $unused = $this->createMock(Container::class); $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], + [RouteHandler::class, $routeHandler], ]); assert($unused instanceof Container); assert($container instanceof Container); - $app = new App($unused, $container, $middleware, $unused); + $app = new App($unused, $unused, $container, $middleware); $ref = new ReflectionProperty($app, 'handler'); if (PHP_VERSION_ID < 80100) { @@ -333,7 +385,7 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsErrorHand $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); $this->assertSame($middleware, $handlers[2]); - $this->assertInstanceOf(RouteHandler::class, $handlers[3]); + $this->assertSame($routeHandler, $handlers[3]); } public function testConstructWithMiddlewareAndErrorHandlerAssignsGivenErrorHandlerAfterMiddlewareAndDefaultAccessLogHandlerAndErrorHandlerFirst(): void @@ -372,6 +424,7 @@ public function testConstructWithMultipleContainersAndMiddlewareAndErrorHandlerC $unused = $this->createMock(Container::class); $unused->expects($this->never())->method('getObject'); + assert($unused instanceof Container); $accessLogHandler = new AccessLogHandler(); $errorHandler1 = new ErrorHandler(); @@ -380,15 +433,19 @@ public function testConstructWithMultipleContainersAndMiddlewareAndErrorHandlerC [AccessLogHandler::class, $accessLogHandler], [ErrorHandler::class, $errorHandler1], ]); + assert($container1 instanceof Container); $errorHandler2 = new ErrorHandler(); $container2 = $this->createMock(Container::class); - $container2->expects($this->exactly(1))->method('getObject')->with(ErrorHandler::class)->willReturn($errorHandler2); - - assert($unused instanceof Container); - assert($container1 instanceof Container); + $container2->expects($this->once())->method('getObject')->with(ErrorHandler::class)->willReturn($errorHandler2); assert($container2 instanceof Container); - $app = new App($unused, $container1, $middleware, $container2, ErrorHandler::class, $unused); + + $routeHandler = $this->createMock(RouteHandler::class); + $container3 = $this->createMock(Container::class); + $container3->expects($this->once())->method('getObject')->with(RouteHandler::class)->willReturn($routeHandler); + assert($container3 instanceof Container); + + $app = new App($unused, $container1, $middleware, $container2, ErrorHandler::class, $unused, $container3); $ref = new ReflectionProperty($app, 'handler'); if (PHP_VERSION_ID < 80100) { @@ -496,11 +553,13 @@ public function testConstructWithContainerAndAccessLogHandlerClassAndErrorHandle { $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $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], + [RouteHandler::class, $routeHandler], ]); assert($container instanceof Container); @@ -523,7 +582,7 @@ public function testConstructWithContainerAndAccessLogHandlerClassAndErrorHandle $this->assertCount(3, $handlers); $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); - $this->assertInstanceOf(RouteHandler::class, $handlers[2]); + $this->assertSame($routeHandler, $handlers[2]); } public function testConstructWithContainerAndAccessLogHandlerClassAndErrorHandlerClassWillUseContainerToGetAccessLogHandlerAndWillSkipAccessLogHandlerToDevNull(): void @@ -532,11 +591,13 @@ public function testConstructWithContainerAndAccessLogHandlerClassAndErrorHandle $accessLogHandler->expects($this->once())->method('isDevNull')->willReturn(true); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $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], + [RouteHandler::class, $routeHandler], ]); assert($container instanceof Container); @@ -558,7 +619,7 @@ public function testConstructWithContainerAndAccessLogHandlerClassAndErrorHandle $this->assertCount(2, $handlers); $this->assertSame($errorHandler, $handlers[0]); - $this->assertInstanceOf(RouteHandler::class, $handlers[1]); + $this->assertSame($routeHandler, $handlers[1]); } public function testConstructWithMiddlewareBeforeAccessLogHandlerAndErrorHandlerAssignsDefaultErrorHandlerAsFirstHandlerFollowedByGivenHandlers(): void @@ -596,19 +657,21 @@ public function testConstructWithMultipleContainersAndAccessLogHandlerClassAndEr { $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $unused = $this->createMock(Container::class); $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], + [RouteHandler::class, $routeHandler], ]); assert($unused instanceof Container); assert($container instanceof Container); - $app = new App($unused, $container, AccessLogHandler::class, ErrorHandler::class, $unused); + $app = new App($unused, $unused, $container, AccessLogHandler::class, ErrorHandler::class); $ref = new ReflectionProperty($app, 'handler'); if (PHP_VERSION_ID < 80100) { @@ -627,7 +690,7 @@ public function testConstructWithMultipleContainersAndAccessLogHandlerClassAndEr $this->assertCount(3, $handlers); $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); - $this->assertInstanceOf(RouteHandler::class, $handlers[2]); + $this->assertSame($routeHandler, $handlers[2]); } @@ -637,20 +700,22 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsDefaultHa $accessLogHandler = new AccessLogHandler(); $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); $unused = $this->createMock(Container::class); $unused->expects($this->never())->method('getObject'); $container = $this->createMock(Container::class); - $container->expects($this->exactly(3))->method('getObject')->willReturnMap([ + $container->expects($this->exactly(4))->method('getObject')->willReturnMap([ [AccessLogHandler::class, $accessLogHandler], [ErrorHandler::class, $errorHandler], [Container::class, $container], + [RouteHandler::class, $routeHandler], ]); assert($unused instanceof Container); assert($container instanceof Container); - $app = new App($unused, $container, Container::class, $middleware, $unused); + $app = new App($unused, $unused, $container, Container::class, $middleware); $ref = new ReflectionProperty($app, 'handler'); if (PHP_VERSION_ID < 80100) { @@ -670,7 +735,107 @@ public function testConstructWithMultipleContainersAndMiddlewareAssignsDefaultHa $this->assertSame($accessLogHandler, $handlers[0]); $this->assertSame($errorHandler, $handlers[1]); $this->assertSame($middleware, $handlers[2]); - $this->assertInstanceOf(RouteHandler::class, $handlers[3]); + $this->assertSame($routeHandler, $handlers[3]); + } + + public function testConstructWithRouteHandlerOnlyAssignsRouteHandlerAfterDefaultErrorHandler(): void + { + $routeHandler = $this->createMock(RouteHandler::class); + assert($routeHandler instanceof RouteHandler); + + $app = new App($routeHandler); + + $ref = new ReflectionProperty($app, 'router'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handler = $ref->getValue($app); + + $this->assertSame($routeHandler, $handler); + + $ref = new ReflectionProperty($app, 'handler'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handler = $ref->getValue($app); + assert($handler instanceof MiddlewareHandler); + + $ref = new ReflectionProperty($handler, 'handlers'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handlers = $ref->getValue($handler); + assert(is_array($handlers)); + + $this->assertCount(3, $handlers); + $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertInstanceOf(ErrorHandler::class, $handlers[1]); + $this->assertSame($routeHandler, $handlers[2]); + } + + public function testConstructWithRouteHandlerClassOnlyAssignsRouteHandlerAfterDefaultErrorHandler(): void + { + $app = new App(RouteHandler::class); + + $ref = new ReflectionProperty($app, 'router'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handler = $ref->getValue($app); + + $this->assertInstanceOf(RouteHandler::class, $handler); + + $ref = new ReflectionProperty($app, 'handler'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handler = $ref->getValue($app); + assert($handler instanceof MiddlewareHandler); + + $ref = new ReflectionProperty($handler, 'handlers'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handlers = $ref->getValue($handler); + assert(is_array($handlers)); + + $this->assertCount(3, $handlers); + $this->assertInstanceOf(AccessLogHandler::class, $handlers[0]); + $this->assertInstanceOf(ErrorHandler::class, $handlers[1]); + $this->assertInstanceOf(RouteHandler::class, $handlers[2]); + } + + public function testConstructWithContainerAndAccessLogHandlerAndErrorHandlerAndRouteHandlerAssignsGivenHandlersWithoutUsingContainer(): void + { + $accessLog = new AccessLogHandler(); + $errorHandler = new ErrorHandler(); + $routeHandler = $this->createMock(RouteHandler::class); + assert($routeHandler instanceof RouteHandler); + + $container = $this->createMock(Container::class); + $container->expects($this->never())->method('getObject'); + assert($container instanceof Container); + + $app = new App($container, $accessLog, $errorHandler, $routeHandler); + + $ref = new ReflectionProperty($app, 'handler'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handler = $ref->getValue($app); + assert($handler instanceof MiddlewareHandler); + + $ref = new ReflectionProperty($handler, 'handlers'); + if (PHP_VERSION_ID < 80100) { + $ref->setAccessible(true); + } + $handlers = $ref->getValue($handler); + assert(is_array($handlers)); + + $this->assertCount(3, $handlers); + $this->assertSame($accessLog, $handlers[0]); + $this->assertSame($errorHandler, $handlers[1]); + $this->assertSame($routeHandler, $handlers[2]); } public function testConstructWithAccessLogHandlerOnlyThrows(): void @@ -690,6 +855,25 @@ public function testConstructWithAccessLogHandlerFollowedByMiddlewareThrows(): v new App($accessLogHandler, $middleware); } + public function testConstructWithRouteHandlerInstanceFollowedByMiddlewareThrows(): void + { + $routeHandler = $this->createMock(RouteHandler::class); + assert($routeHandler instanceof RouteHandler); + + $middleware = function (ServerRequestInterface $request, callable $next) { }; + + $this->expectException(\TypeError::class); + new App($routeHandler, $middleware); + } + + public function testConstructWithRouteHandlerClassNameFollowedByMiddlewareThrows(): void + { + $middleware = function (ServerRequestInterface $request, callable $next) { }; + + $this->expectException(\TypeError::class); + new App(RouteHandler::class, $middleware); + } + public function testConstructWithContainerWithListenAddressWillPassListenAddressToReactiveHandler(): void { $container = new Container([ @@ -735,144 +919,99 @@ public function testRunWillExecuteRunOnSapiHandler(): void public function testGetMethodAddsGetRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->get('/', function () { }); } public function testHeadMethodAddsHeadRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['HEAD'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->head('/', function () { }); } public function testPostMethodAddsPostRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['POST'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->post('/', function () { }); } public function testPutMethodAddsPutRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['PUT'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->put('/', function () { }); } public function testPatchMethodAddsPatchRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['PATCH'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->patch('/', function () { }); } public function testDeleteMethodAddsDeleteRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['DELETE'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->delete('/', function () { }); } public function testOptionsMethodAddsOptionsRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['OPTIONS'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->options('/', function () { }); } public function testAnyMethodAddsRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->any('/', function () { }); } public function testMapMethodAddsRouteOnRouter(): void { - $app = new App(); - $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET', 'POST'], '/', $this->anything()); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->map(['GET', 'POST'], '/', function () { }); } @@ -895,20 +1034,15 @@ public function testGetWithAccessLogHandlerClassAsMiddlewareThrows(): void public function testRedirectMethodAddsAnyRouteOnRouterWhichWhenInvokedReturnsRedirectResponseWithTargetLocation(): void { - $app = new App(); - $handler = null; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], '/', $this->callback(function ($fn) use (&$handler) { $handler = $fn; return true; })); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->redirect('/', '/users'); @@ -929,20 +1063,15 @@ public function testRedirectMethodAddsAnyRouteOnRouterWhichWhenInvokedReturnsRed public function testRedirectMethodWithCustomRedirectCodeAddsAnyRouteOnRouterWhichWhenInvokedReturnsRedirectResponseWithCustomRedirectCode(): void { - $app = new App(); - $handler = null; $router = $this->createMock(RouteHandler::class); $router->expects($this->once())->method('map')->with(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], '/', $this->callback(function ($fn) use (&$handler) { $handler = $fn; return true; })); + assert($router instanceof RouteHandler); - $ref = new ReflectionProperty($app, 'router'); - if (PHP_VERSION_ID < 80100) { - $ref->setAccessible(true); - } - $ref->setValue($app, $router); + $app = new App($router); $app->redirect('/', '/users', 307); diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index 014c308..19c4644 100644 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -5,6 +5,7 @@ use FrameworkX\AccessLogHandler; use FrameworkX\Container; use FrameworkX\ErrorHandler; +use FrameworkX\Io\RouteHandler; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -2626,6 +2627,23 @@ public function testGetObjectReturnsOtherContainerFromFactoryFunction(): void $this->assertSame($other, $ret); } + public function testGetObjectReturnsDefaultRouteHandlerInstance(): void + { + $container = new Container([]); + + $router = $container->getObject(RouteHandler::class); + + $this->assertInstanceOf(RouteHandler::class, $router); + + $ref = new \ReflectionProperty($router, 'container'); + if (\PHP_VERSION_ID < 801000) { + $ref->setAccessible(true); + } + $ret = $ref->getValue($router); + + $this->assertSame($container, $ret); + } + public function testGetObjectReturnsAccessLogHandlerInstanceFromPsrContainer(): void { $accessLogHandler = new AccessLogHandler(); @@ -2670,6 +2688,28 @@ public function testGetObjectReturnsSelfContainerIfPsrContainerHasNoEntry(): voi $this->assertSame($container, $ret); } + public function testGetObjectReturnsDefaultRouteHandlerInstanceIfPsrContainerHasNoEntry(): void + { + $psr = $this->createMock(ContainerInterface::class); + $psr->expects($this->once())->method('has')->with(RouteHandler::class)->willReturn(false); + $psr->expects($this->never())->method('get'); + + assert($psr instanceof ContainerInterface); + $container = new Container($psr); + + $router = $container->getObject(RouteHandler::class); + + $this->assertInstanceOf(RouteHandler::class, $router); + + $ref = new \ReflectionProperty($router, 'container'); + if (\PHP_VERSION_ID < 801000) { + $ref->setAccessible(true); + } + $ret = $ref->getValue($router); + + $this->assertSame($container, $ret); + } + public function testGetObjectThrowsIfFactoryFunctionThrows(): void { $container = new Container([ diff --git a/tests/Io/RouteHandlerTest.php b/tests/Io/RouteHandlerTest.php index f01327a..c80a228 100644 --- a/tests/Io/RouteHandlerTest.php +++ b/tests/Io/RouteHandlerTest.php @@ -18,7 +18,10 @@ public function testMapRouteWithControllerAddsRouteOnRouter(): void { $controller = function () { }; - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); $router = $this->createMock(RouteCollector::class); $router->expects($this->once())->method('addRoute')->with(['GET'], '/', $controller); @@ -37,7 +40,10 @@ public function testMapRouteWithMiddlewareAndControllerAddsRouteWithMiddlewareHa $middleware = function () { }; $controller = function () { }; - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); $router = $this->createMock(RouteCollector::class); $router->expects($this->once())->method('addRoute')->with(['GET'], '/', new MiddlewareHandler([$middleware, $controller])); @@ -57,8 +63,8 @@ public function testMapRouteWithClassNameAddsRouteOnRouterWithControllerCallable $container = $this->createMock(Container::class); $container->expects($this->once())->method('callable')->with('stdClass')->willReturn($controller); - assert($container instanceof Container); + $handler = new RouteHandler($container); $router = $this->createMock(RouteCollector::class); @@ -77,7 +83,10 @@ public function testMapRouteWithContainerAndControllerAddsRouteOnRouterWithContr { $controller = function () { }; - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); $router = $this->createMock(RouteCollector::class); $router->expects($this->once())->method('addRoute')->with(['GET'], '/', $controller); @@ -96,9 +105,13 @@ public function testMapRouteWithContainerAndControllerClassNameAddsRouteOnRouter $controller = function () { }; $container = $this->createMock(Container::class); - $container->expects($this->once())->method('callable')->with('stdClass')->willReturn($controller); + $container->expects($this->never())->method('callable'); + assert($container instanceof Container); - $handler = new RouteHandler(); + $handler = new RouteHandler($container); + + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('callable')->with('stdClass')->willReturn($controller); $router = $this->createMock(RouteCollector::class); $router->expects($this->once())->method('addRoute')->with(['GET'], '/', $controller); @@ -143,7 +156,11 @@ public function testHandleRequestWithProxyRequestReturnsResponseWithMessageThatP $request = new ServerRequest('GET', 'http://example.com/'); $request = $request->withRequestTarget('http://example.com/'); - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + $response = $handler($request); /** @var ResponseInterface $response */ @@ -161,7 +178,11 @@ public function testHandleRequestWithConnectProxyRequestReturnsResponseWithMessa $request = new ServerRequest('CONNECT', 'example.com:80'); $request = $request->withRequestTarget('example.com:80'); - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + $response = $handler($request); /** @var ResponseInterface $response */ @@ -179,7 +200,11 @@ public function testHandleRequestWithGetRequestReturnsResponseFromMatchingHandle $request = new ServerRequest('GET', 'http://example.com/'); $response = new Response(200, [], ''); - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + $handler->map(['GET'], '/', function () use ($response) { return $response; }); $ret = $handler($request); @@ -201,7 +226,11 @@ public function __invoke(): ?Response { }; $controller::$response = $response; - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + $handler->map(['GET'], '/', $controller); $ret = $handler($request); @@ -224,7 +253,8 @@ public function __invoke(): ?Response }; $controller::$response = $response; - $handler = new RouteHandler(); + $handler = new RouteHandler(new Container([])); + $handler->map(['GET'], '/', get_class($controller)); $ret = $handler($request); @@ -251,7 +281,8 @@ public function __invoke(): ?Response }; $controller::$response = $response; - $handler = new RouteHandler(); + $handler = new RouteHandler(new Container([])); + $handler->map(['GET'], '/', get_class($controller)); $ret = $handler($request); @@ -276,7 +307,8 @@ public function __invoke(): ?Response } }; - $handler = new RouteHandler(); + $handler = new RouteHandler(new Container([])); + $handler->map(['GET'], '/', get_class($controller)); $ret = $handler($request); @@ -296,7 +328,8 @@ public function __invoke(ServerRequestInterface $request, callable $next): Respo } }; - $handler = new RouteHandler(); + $handler = new RouteHandler(new Container([])); + $handler->map(['GET'], '/', get_class($middleware), function () use ($response) { return $response; }); $ret = $handler($request); @@ -317,7 +350,8 @@ public function __invoke(): int } }; - $handler = new RouteHandler(); + $handler = new RouteHandler(new Container([])); + $handler->map(['GET'], '/', get_class($controller)); $ret = $handler($request); @@ -332,7 +366,11 @@ public function testHandleRequestWithGetRequestWithHttpUrlInPathReturnsResponseF $request = new ServerRequest('GET', 'http://example.com/http://localhost/'); $response = new Response(200, [], ''); - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + $handler->map(['GET'], '/http://localhost/', function () use ($response) { return $response; }); $ret = $handler($request); @@ -346,7 +384,11 @@ public function testHandleRequestWithOptionsAsteriskRequestReturnsResponseFromMa $request = $request->withRequestTarget('*'); $response = new Response(200, [], ''); - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + $handler->map(['OPTIONS'], '*', function () use ($response) { return $response; }); $ret = $handler($request); @@ -358,7 +400,11 @@ public function testHandleRequestWithContainerInstanceOnlyThrows(): void { $request = new ServerRequest('GET', 'http://example.com/'); - $handler = new RouteHandler(); + $container = $this->createMock(Container::class); + assert($container instanceof Container); + + $handler = new RouteHandler($container); + $handler->map(['GET'], '/', new Container()); $this->expectException(\BadMethodCallException::class); @@ -370,7 +416,8 @@ public function testHandleRequestWithContainerClassOnlyThrows(): void { $request = new ServerRequest('GET', 'http://example.com/'); - $handler = new RouteHandler(); + $handler = new RouteHandler(new Container([])); + $handler->map(['GET'], '/', Container::class); $this->expectException(\BadMethodCallException::class);