From 633d3e87a77b05864c041aeabc8870d035e0c0bf Mon Sep 17 00:00:00 2001 From: Dominik Zogg Date: Mon, 10 May 2021 18:21:41 +0200 Subject: [PATCH] router-split --- README.md | 21 +- ...rMiddleware.md => UrlMatcherMiddleware.md} | 12 +- src/Middleware/RouterMiddleware.php | 96 +------- src/Middleware/UrlMatcherMiddleware.php | 109 +++++++++ ...issingRouteAttributeOnRequestException.php | 4 +- src/Router/RouterInterface.php | 27 +-- src/Router/Routes.php | 56 +++++ src/Router/RoutesInterface.php | 13 + src/Router/UrlGeneratorInterface.php | 31 +++ src/Router/UrlMatcherInterface.php | 12 + ...terLessTest.php => UrlMatcherLessTest.php} | 10 +- tests/Unit/ApplicationTest.php | 2 +- .../NewRelicRouteMiddlewareTest.php | 2 +- .../Unit/Middleware/RouterMiddlewareTest.php | 23 ++ .../Middleware/UrlMatcherMiddlewareTest.php | 223 ++++++++++++++++++ ...ngRouteAttributeOnRequestExceptionTest.php | 4 +- tests/Unit/Router/RoutesTest.php | 54 +++++ 17 files changed, 564 insertions(+), 135 deletions(-) rename doc/Middleware/{RouterMiddleware.md => UrlMatcherMiddleware.md} (62%) create mode 100644 src/Middleware/UrlMatcherMiddleware.php create mode 100644 src/Router/Routes.php create mode 100644 src/Router/RoutesInterface.php create mode 100644 src/Router/UrlGeneratorInterface.php create mode 100644 src/Router/UrlMatcherInterface.php rename tests/Integration/{RouterLessTest.php => UrlMatcherLessTest.php} (93%) create mode 100644 tests/Unit/Middleware/UrlMatcherMiddlewareTest.php create mode 100644 tests/Unit/Router/RoutesTest.php diff --git a/README.md b/README.md index 0ea44d2..30e2772 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,11 @@ namespace App; use Chubbyphp\Framework\Application; use Chubbyphp\Framework\Middleware\ExceptionMiddleware; -use Chubbyphp\Framework\Middleware\RouterMiddleware; +use Chubbyphp\Framework\Middleware\UrlMatcherMiddleware; use Chubbyphp\Framework\RequestHandler\CallbackRequestHandler; -use Chubbyphp\Framework\Router\FastRoute\Router; +use Chubbyphp\Framework\Router\FastRoute\UrlMatcher; use Chubbyphp\Framework\Router\Route; +use Chubbyphp\Framework\Router\Routes; use Psr\Http\Message\ServerRequestInterface; use Slim\Psr7\Factory\ResponseFactory; use Slim\Psr7\Factory\ServerRequestFactory; @@ -101,7 +102,7 @@ $responseFactory = new ResponseFactory(); $app = new Application([ new ExceptionMiddleware($responseFactory, true), - new RouterMiddleware(new Router([ + new UrlMatcherMiddleware(new UrlMatcher(new Routes([ Route::get('/hello/{name:[a-z]+}', 'hello', new CallbackRequestHandler( static function (ServerRequestInterface $request) use ($responseFactory) { $response = $responseFactory->createResponse(); @@ -110,7 +111,7 @@ $app = new Application([ return $response; } )) - ]), $responseFactory), + ])), $responseFactory), ]); $app->emit($app->handle((new ServerRequestFactory())->createFromGlobals())); @@ -127,9 +128,9 @@ $app->emit($app->handle((new ServerRequestFactory())->createFromGlobals())); * [LazyMiddleware][72] * [MiddlewareDispatcher][73] * [NewRelicRouteMiddleware][74] - * [RouterMiddleware][75] - * [SlimCallbackMiddleware][76] - * [SlimLazyMiddleware][77] + * [SlimCallbackMiddleware][75] + * [SlimLazyMiddleware][76] + * [UrlMatcherMiddleware][77] ### RequestHandler @@ -212,9 +213,9 @@ Dominik Zogg 2021 [72]: doc/Middleware/LazyMiddleware.md [73]: doc/Middleware/MiddlewareDispatcher.md [74]: doc/Middleware/NewRelicRouteMiddleware.md -[75]: doc/Middleware/RouterMiddleware.md -[76]: doc/Middleware/SlimCallbackMiddleware.md -[77]: doc/Middleware/SlimLazyMiddleware.md +[75]: doc/Middleware/SlimCallbackMiddleware.md +[76]: doc/Middleware/SlimLazyMiddleware.md +[77]: doc/Middleware/UrlMatcherMiddleware.md [80]: doc/RequestHandler/CallbackRequestHandler.md [81]: doc/RequestHandler/LazyRequestHandler.md diff --git a/doc/Middleware/RouterMiddleware.md b/doc/Middleware/UrlMatcherMiddleware.md similarity index 62% rename from doc/Middleware/RouterMiddleware.md rename to doc/Middleware/UrlMatcherMiddleware.md index 1a1362a..a475151 100644 --- a/doc/Middleware/RouterMiddleware.md +++ b/doc/Middleware/UrlMatcherMiddleware.md @@ -1,4 +1,4 @@ -# RouterMiddleware +# UrlMatcherMiddleware ## Methods @@ -7,8 +7,8 @@ ```php process($request, $handler); +$response = $urlMatcherMiddleware->process($request, $handler); ``` diff --git a/src/Middleware/RouterMiddleware.php b/src/Middleware/RouterMiddleware.php index 0eda57e..f112b01 100644 --- a/src/Middleware/RouterMiddleware.php +++ b/src/Middleware/RouterMiddleware.php @@ -4,106 +4,36 @@ namespace Chubbyphp\Framework\Middleware; -use Chubbyphp\Framework\Router\Exceptions\RouterExceptionInterface; -use Chubbyphp\Framework\Router\RouterInterface; +use Chubbyphp\Framework\Router\UrlMatcherInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; +/** + * @deprecated + */ final class RouterMiddleware implements MiddlewareInterface { - private const HTML = <<<'EOT' - - - - %s - - - - %s - - -EOT; - - private RouterInterface $router; - - private ResponseFactoryInterface $responseFactory; - - private LoggerInterface $logger; + private UrlMatcherMiddleware $urlMatcherMiddleware; public function __construct( - RouterInterface $router, + UrlMatcherInterface $urlMatcher, ResponseFactoryInterface $responseFactory, ?LoggerInterface $logger = null ) { - $this->router = $router; - $this->responseFactory = $responseFactory; - $this->logger = $logger ?? new NullLogger(); - } + @trigger_error( + sprintf('Use %s parameter instead of instead of "%s"', UrlMatcherMiddleware::class, self::class), + E_USER_DEPRECATED + ); - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - try { - $route = $this->router->match($request); - } catch (RouterExceptionInterface $routerException) { - return $this->routeExceptionResponse($routerException); - } - - $request = $request->withAttribute('route', $route); - - foreach ($route->getAttributes() as $attribute => $value) { - $request = $request->withAttribute($attribute, $value); - } - - return $handler->handle($request); + $this->urlMatcherMiddleware = new UrlMatcherMiddleware($urlMatcher, $responseFactory, $logger); } - private function routeExceptionResponse(RouterExceptionInterface $routerException): ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $this->logger->info('Route exception', [ - 'title' => $routerException->getTitle(), - 'message' => $routerException->getMessage(), - 'code' => $routerException->getCode(), - ]); - - $response = $this->responseFactory->createResponse($routerException->getCode()); - $response = $response->withHeader('Content-Type', 'text/html'); - $response->getBody()->write(sprintf( - self::HTML, - $routerException->getTitle(), - '

'.$routerException->getTitle().'

'.'

'.$routerException->getMessage().'

' - )); - - return $response; + return $this->urlMatcherMiddleware->process($request, $handler); } } diff --git a/src/Middleware/UrlMatcherMiddleware.php b/src/Middleware/UrlMatcherMiddleware.php new file mode 100644 index 0000000..274a5a7 --- /dev/null +++ b/src/Middleware/UrlMatcherMiddleware.php @@ -0,0 +1,109 @@ + + + + %s + + + + %s + + +EOT; + + private UrlMatcherInterface $urlMatcher; + + private ResponseFactoryInterface $responseFactory; + + private LoggerInterface $logger; + + public function __construct( + UrlMatcherInterface $urlMatcher, + ResponseFactoryInterface $responseFactory, + ?LoggerInterface $logger = null + ) { + $this->urlMatcher = $urlMatcher; + $this->responseFactory = $responseFactory; + $this->logger = $logger ?? new NullLogger(); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + $route = $this->urlMatcher->match($request); + } catch (RouterExceptionInterface $routerException) { + return $this->routeExceptionResponse($routerException); + } + + $request = $request->withAttribute('route', $route); + + foreach ($route->getAttributes() as $attribute => $value) { + $request = $request->withAttribute($attribute, $value); + } + + return $handler->handle($request); + } + + private function routeExceptionResponse(RouterExceptionInterface $routerException): ResponseInterface + { + $this->logger->info('Route exception', [ + 'title' => $routerException->getTitle(), + 'message' => $routerException->getMessage(), + 'code' => $routerException->getCode(), + ]); + + $response = $this->responseFactory->createResponse($routerException->getCode()); + $response = $response->withHeader('Content-Type', 'text/html'); + $response->getBody()->write(sprintf( + self::HTML, + $routerException->getTitle(), + '

'.$routerException->getTitle().'

'.'

'.$routerException->getMessage().'

' + )); + + return $response; + } +} diff --git a/src/Router/Exceptions/MissingRouteAttributeOnRequestException.php b/src/Router/Exceptions/MissingRouteAttributeOnRequestException.php index 10fdba5..5717008 100644 --- a/src/Router/Exceptions/MissingRouteAttributeOnRequestException.php +++ b/src/Router/Exceptions/MissingRouteAttributeOnRequestException.php @@ -4,7 +4,7 @@ namespace Chubbyphp\Framework\Router\Exceptions; -use Chubbyphp\Framework\Middleware\RouterMiddleware; +use Chubbyphp\Framework\Middleware\UrlMatcherMiddleware; use Chubbyphp\Framework\Router\RouterException; final class MissingRouteAttributeOnRequestException extends RouterException @@ -22,7 +22,7 @@ public static function create($route): self return new self(sprintf( 'Request attribute "route" missing or wrong type "%s", please add the "%s" middleware', is_object($route) ? get_class($route) : gettype($route), - RouterMiddleware::class + UrlMatcherMiddleware::class ), 2); } } diff --git a/src/Router/RouterInterface.php b/src/Router/RouterInterface.php index ace9829..38c2be7 100644 --- a/src/Router/RouterInterface.php +++ b/src/Router/RouterInterface.php @@ -4,30 +4,7 @@ namespace Chubbyphp\Framework\Router; -use Psr\Http\Message\ServerRequestInterface; - -interface RouterInterface +/** @deprecated */ +interface RouterInterface extends UrlGeneratorInterface, UrlMatcherInterface { - public function match(ServerRequestInterface $request): RouteInterface; - - /** - * @param array $attributes - * @param array $queryParams - * - * @throws RouterException - */ - public function generateUrl( - ServerRequestInterface $request, - string $name, - array $attributes = [], - array $queryParams = [] - ): string; - - /** - * @param array $attributes - * @param array $queryParams - * - * @throws RouterException - */ - public function generatePath(string $name, array $attributes = [], array $queryParams = []): string; } diff --git a/src/Router/Routes.php b/src/Router/Routes.php new file mode 100644 index 0000000..3547e7f --- /dev/null +++ b/src/Router/Routes.php @@ -0,0 +1,56 @@ + + */ + private array $routes; + + /** + * @param array $routes + */ + public function __construct(array $routes) + { + $this->routes = $this->aggregateRoutesByName($routes); + } + + /** + * @param array $routes + * + * @return array + */ + private function aggregateRoutesByName(array $routes): array + { + $routesByName = []; + + foreach ($routes as $i => $route) { + if (!$route instanceof RouteInterface) { + throw new \TypeError( + sprintf( + '%s::__construct() expects parameter 1 at index %d to be %s[], %s[] given', + self::class, + $i, + RouteInterface::class, + get_class($route) + ) + ); + } + $routesByName[$route->getName()] = $route; + } + + return $routesByName; + } + + /** + * @return array + */ + public function getRoutesByName(): array + { + return $this->routes; + } +} diff --git a/src/Router/RoutesInterface.php b/src/Router/RoutesInterface.php new file mode 100644 index 0000000..e9f1a96 --- /dev/null +++ b/src/Router/RoutesInterface.php @@ -0,0 +1,13 @@ + + */ + public function getRoutesByName(): array; +} diff --git a/src/Router/UrlGeneratorInterface.php b/src/Router/UrlGeneratorInterface.php new file mode 100644 index 0000000..a6ca280 --- /dev/null +++ b/src/Router/UrlGeneratorInterface.php @@ -0,0 +1,31 @@ + $attributes + * @param array $queryParams + * + * @throws RouterException + */ + public function generateUrl( + ServerRequestInterface $request, + string $name, + array $attributes = [], + array $queryParams = [] + ): string; + + /** + * @param array $attributes + * @param array $queryParams + * + * @throws RouterException + */ + public function generatePath(string $name, array $attributes = [], array $queryParams = []): string; +} diff --git a/src/Router/UrlMatcherInterface.php b/src/Router/UrlMatcherInterface.php new file mode 100644 index 0000000..b2849d2 --- /dev/null +++ b/src/Router/UrlMatcherInterface.php @@ -0,0 +1,12 @@ +expectException(RouterException::class); $this->expectExceptionMessage( 'Request attribute "route" missing or wrong type "NULL"' - .', please add the "Chubbyphp\Framework\Middleware\RouterMiddleware" middleware' + .', please add the "Chubbyphp\Framework\Middleware\UrlMatcherMiddleware" middleware' ); $app = new Application([]); diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php index d1d4707..99f8245 100644 --- a/tests/Unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -131,7 +131,7 @@ public function testHandleWithMissingRouteAttribute(): void $this->expectException(RouterException::class); $this->expectExceptionMessage( 'Request attribute "route" missing or wrong type "stdClass",' - .' please add the "Chubbyphp\Framework\Middleware\RouterMiddleware" middleware' + .' please add the "Chubbyphp\Framework\Middleware\UrlMatcherMiddleware" middleware' ); /** @var MiddlewareInterface|MockObject $routeIndependMiddleware */ diff --git a/tests/Unit/Middleware/NewRelicRouteMiddlewareTest.php b/tests/Unit/Middleware/NewRelicRouteMiddlewareTest.php index d97a26b..0219d41 100644 --- a/tests/Unit/Middleware/NewRelicRouteMiddlewareTest.php +++ b/tests/Unit/Middleware/NewRelicRouteMiddlewareTest.php @@ -146,7 +146,7 @@ public function testWithNewRelicExtensionAndMissingRoute(): void $this->expectException(RouterException::class); $this->expectExceptionMessage( 'Request attribute "route" missing or wrong type "NULL",' - .' please add the "Chubbyphp\Framework\Middleware\RouterMiddleware" middleware' + .' please add the "Chubbyphp\Framework\Middleware\UrlMatcherMiddleware" middleware' ); TestExtesionLoaded::add('newrelic'); diff --git a/tests/Unit/Middleware/RouterMiddlewareTest.php b/tests/Unit/Middleware/RouterMiddlewareTest.php index 73ee2cc..4316ec9 100644 --- a/tests/Unit/Middleware/RouterMiddlewareTest.php +++ b/tests/Unit/Middleware/RouterMiddlewareTest.php @@ -28,6 +28,29 @@ final class RouterMiddlewareTest extends TestCase { use MockByCallsTrait; + public function testDeprecationWithinConstruct(): void + { + error_clear_last(); + + /** @var RouterInterface|MockObject $router */ + $router = $this->getMockByCalls(RouterInterface::class); + + /** @var ResponseFactoryInterface|MockObject $responseFactory */ + $responseFactory = $this->getMockByCalls(ResponseFactoryInterface::class); + + new RouterMiddleware($router, $responseFactory); + + $error = error_get_last(); + + self::assertNotNull($error); + + self::assertSame(E_USER_DEPRECATED, $error['type']); + self::assertSame( + 'Use Chubbyphp\Framework\Middleware\UrlMatcherMiddleware parameter instead of instead of "Chubbyphp\Framework\Middleware\RouterMiddleware"', + $error['message'] + ); + } + public function testProcess(): void { /** @var RouteInterface|MockObject $route */ diff --git a/tests/Unit/Middleware/UrlMatcherMiddlewareTest.php b/tests/Unit/Middleware/UrlMatcherMiddlewareTest.php new file mode 100644 index 0000000..6f8264d --- /dev/null +++ b/tests/Unit/Middleware/UrlMatcherMiddlewareTest.php @@ -0,0 +1,223 @@ +getMockByCalls(RouteInterface::class, [ + Call::create('getAttributes')->with()->willReturn(['key' => 'value']), + ]); + + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockByCalls(ServerRequestInterface::class, [ + Call::create('withAttribute')->with('route', $route)->willReturnSelf(), + Call::create('withAttribute')->with('key', 'value')->willReturnSelf(), + ]); + + /** @var ResponseInterface|MockObject $response */ + $response = $this->getMockByCalls(ResponseInterface::class); + + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockByCalls(RequestHandlerInterface::class, [ + Call::create('handle')->with($request)->willReturn($response), + ]); + + /** @var RouterInterface|MockObject $router */ + $router = $this->getMockByCalls(RouterInterface::class, [ + Call::create('match')->with($request)->willReturn($route), + ]); + + /** @var ResponseFactoryInterface|MockObject $responseFactory */ + $responseFactory = $this->getMockByCalls(ResponseFactoryInterface::class); + + $middleware = new UrlMatcherMiddleware($router, $responseFactory); + + self::assertSame($response, $middleware->process($request, $handler)); + } + + public function testProcessMissingRouteWithoutLogger(): void + { + $routerException = NotFoundException::create('/'); + + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockByCalls(ServerRequestInterface::class); + + $expectedBody = <<<'EOT' + + + + Page not found + + + +

Page not found

The page "/" you are looking for could not be found. Check the address bar to ensure your URL is spelled correctly.

+ + +EOT; + + /** @var StreamInterface|MockObject $responseBody */ + $responseBody = $this->getMockByCalls(StreamInterface::class, [ + Call::create('write')->with($expectedBody), + ]); + + /** @var ResponseInterface|MockObject $response */ + $response = $this->getMockByCalls(ResponseInterface::class, [ + Call::create('withHeader')->with('Content-Type', 'text/html')->willReturnSelf(), + Call::create('getBody')->with()->willReturn($responseBody), + ]); + + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockByCalls(RequestHandlerInterface::class); + + /** @var RouterInterface|MockObject $router */ + $router = $this->getMockByCalls(RouterInterface::class, [ + Call::create('match')->with($request)->willThrowException($routerException), + ]); + + /** @var ResponseFactoryInterface|MockObject $responseFactory */ + $responseFactory = $this->getMockByCalls(ResponseFactoryInterface::class, [ + Call::create('createResponse')->with(404, '')->willReturn($response), + ]); + + $middleware = new UrlMatcherMiddleware($router, $responseFactory); + + self::assertSame($response, $middleware->process($request, $handler)); + } + + public function testProcessMissingRouteWithLogger(): void + { + $routerException = NotFoundException::create('/'); + + /** @var ServerRequestInterface|MockObject $request */ + $request = $this->getMockByCalls(ServerRequestInterface::class); + + $expectedBody = <<<'EOT' + + + + Page not found + + + +

Page not found

The page "/" you are looking for could not be found. Check the address bar to ensure your URL is spelled correctly.

+ + +EOT; + + /** @var StreamInterface|MockObject $responseBody */ + $responseBody = $this->getMockByCalls(StreamInterface::class, [ + Call::create('write')->with($expectedBody), + ]); + + /** @var ResponseInterface|MockObject $response */ + $response = $this->getMockByCalls(ResponseInterface::class, [ + Call::create('withHeader')->with('Content-Type', 'text/html')->willReturnSelf(), + Call::create('getBody')->with()->willReturn($responseBody), + ]); + + /** @var RequestHandlerInterface|MockObject $handler */ + $handler = $this->getMockByCalls(RequestHandlerInterface::class); + + /** @var RouterInterface|MockObject $router */ + $router = $this->getMockByCalls(RouterInterface::class, [ + Call::create('match')->with($request)->willThrowException($routerException), + ]); + + /** @var ResponseFactoryInterface|MockObject $responseFactory */ + $responseFactory = $this->getMockByCalls(ResponseFactoryInterface::class, [ + Call::create('createResponse')->with(404, '')->willReturn($response), + ]); + + /** @var LoggerInterface|MockObject $logger */ + $logger = $this->getMockByCalls(LoggerInterface::class, [ + Call::create('info')->with('Route exception', [ + 'title' => $routerException->getTitle(), + 'message' => $routerException->getMessage(), + 'code' => $routerException->getCode(), + ]), + ]); + + $middleware = new UrlMatcherMiddleware($router, $responseFactory, $logger); + + self::assertSame($response, $middleware->process($request, $handler)); + } +} diff --git a/tests/Unit/Router/Exceptions/MissingRouteAttributeOnRequestExceptionTest.php b/tests/Unit/Router/Exceptions/MissingRouteAttributeOnRequestExceptionTest.php index fac1d30..e49ece3 100644 --- a/tests/Unit/Router/Exceptions/MissingRouteAttributeOnRequestExceptionTest.php +++ b/tests/Unit/Router/Exceptions/MissingRouteAttributeOnRequestExceptionTest.php @@ -30,7 +30,7 @@ public function testCreateWithNull(): void self::assertSame( 'Request attribute "route" missing or wrong type "NULL", please add the' - .' "Chubbyphp\Framework\Middleware\RouterMiddleware" middleware', + .' "Chubbyphp\Framework\Middleware\UrlMatcherMiddleware" middleware', $exception->getMessage() ); self::assertSame(2, $exception->getCode()); @@ -44,7 +44,7 @@ public function testCreateWithObject(): void self::assertSame( 'Request attribute "route" missing or wrong type "stdClass", please add the' - .' "Chubbyphp\Framework\Middleware\RouterMiddleware" middleware', + .' "Chubbyphp\Framework\Middleware\UrlMatcherMiddleware" middleware', $exception->getMessage() ); self::assertSame(2, $exception->getCode()); diff --git a/tests/Unit/Router/RoutesTest.php b/tests/Unit/Router/RoutesTest.php new file mode 100644 index 0000000..9e68229 --- /dev/null +++ b/tests/Unit/Router/RoutesTest.php @@ -0,0 +1,54 @@ +expectException(\TypeError::class); + $this->expectExceptionMessage( + sprintf( + '%s::__construct() expects parameter 1 at index %d to be %s[], %s[] given', + Routes::class, + 0, + RouteInterface::class, + \stdClass::class, + ) + ); + + $route = new \stdClass(); + + new Routes([$route]); + } + + public function testGetRoutes(): void + { + $route1 = $this->getMockByCalls(RouteInterface::class, [ + Call::create('getName')->with()->willReturn('name1'), + ]); + + $route2 = $this->getMockByCalls(RouteInterface::class, [ + Call::create('getName')->with()->willReturn('name2'), + ]); + + $routes = new Routes([$route1, $route2]); + + self::assertSame(['name1' => $route1, 'name2' => $route2], $routes->getRoutesByName()); + } +}