From 6491e4dbac20a4819fb5f8e2998f6ec0290a4793 Mon Sep 17 00:00:00 2001 From: Daniel Bettles Date: Sun, 26 Mar 2023 07:50:42 +0000 Subject: [PATCH] Tweaked the router to allow 'default' route parameters to be correctly passed through to the action. --- .markdownlint.json | 4 ++ CHANGELOG.md | 11 ++++- phpstan.neon | 1 + src/Router.php | 91 ++++++++++++++++++++++++++-------------- tests/src/RouterTest.php | 51 ++++++++++++++++++++-- 5 files changed, 122 insertions(+), 36 deletions(-) create mode 100644 .markdownlint.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..e5f04d7 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD024": false, + "MD013": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5cb16..7010857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +No unreleased changes. + +## [4.0.0] - 2023-03-26 + +### Added + +- 'Default' route parameters will be processed, and passed through to, the action. + ### Removed - `AbstractAction::createNotFoundException()` because it was a bit pointless 🤦‍♂️ @@ -108,7 +116,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), First stable release. -[unreleased]: https://github.com/danbettles/marigold/compare/v3.0.0...HEAD +[unreleased]: https://github.com/danbettles/marigold/compare/v4.0.0...HEAD +[4.0.0]: https://github.com/danbettles/marigold/compare/v3.0.0...v4.0.0 [3.0.0]: https://github.com/danbettles/marigold/compare/v2.5.0...v3.0.0 [2.5.0]: https://github.com/danbettles/marigold/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/danbettles/marigold/compare/v2.3.3...v2.4.0 diff --git a/phpstan.neon b/phpstan.neon index dbf0324..948f0f6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,6 +8,7 @@ parameters: typeAliases: HeadersArray: 'array' + RouteArray: 'array{id:string,path:string,action:mixed,parameters?:array}' ignoreErrors: - diff --git a/src/Router.php b/src/Router.php index c2bdd85..6130021 100644 --- a/src/Router.php +++ b/src/Router.php @@ -10,19 +10,21 @@ use function array_combine; use function array_filter; use function array_intersect_key; -use function array_keys; use function array_key_exists; +use function array_keys; +use function array_replace; use function count; use function explode; use function implode; use function preg_match; use function preg_match_all; -use function strpos; use function str_replace; +use function strpos; use const ARRAY_FILTER_USE_KEY; use const false; use const null; +use const true; /** * Maps paths to actions. @@ -35,37 +37,58 @@ * 'path' => '/posts/{postId}', * 'action' => ['FooBar', 'baz'], * ], + * [ + * 'id' => 'showArticle', + * 'path' => '/articles/{articleId}', + * 'action' => ShowArticleAction::class, + * ], + * [ + * 'id' => 'showAboutPage', + * 'path' => '/about', + * 'action' => ShowArticleAction::class, + * 'parameters' => [ + * 'articleId' => 123, + * ], + * ], * // ... * ] * * `action` can be anything: the name of a method; a callable; whatever's appropriate for the app. * - * A matched route, the return value of `match()`, will have an additional element, `parameters`, containing the values - * of any parameters found in the path. + * A matched route, the return value of `match()`, will always have an additional array element, `parameters`, + * containing the values of any parameters found in the path. + * + * Default values for parameters can be supplied in the `parameters` element. + * + * @phpstan-type MatchedRouteArray array{id:string,path:string,action:mixed,parameters:array} + * @phpstan-type IndexedRouteArrayArray array + * @phpstan-type PlaceholdersArray array */ class Router { /** - * @var array{id:null,path:null,action:null} + * @var array */ - private const EMPTY_ROUTE = [ - 'id' => null, - 'path' => null, - 'action' => null, + private const ROUTE_ELEMENTS = [ + // Name => mandatory? + 'id' => true, + 'path' => true, + 'action' => true, + 'parameters' => false, ]; /** - * @var array + * @phpstan-var IndexedRouteArrayArray */ private array $routes; /** - * @var array> + * @phpstan-var array */ private array $placeholdersByRouteId = []; /** - * @param array $routes + * @phpstan-param RouteArray[] $routes */ public function __construct(array $routes) { @@ -78,8 +101,8 @@ private function countPathParts(string $path): int } /** - * @param array $routes - * @return array + * @phpstan-param IndexedRouteArrayArray $routes + * @phpstan-return IndexedRouteArrayArray */ private function eliminateUnmatchableRoutes(string $path, array $routes): array { @@ -93,21 +116,24 @@ private function eliminateUnmatchableRoutes(string $path, array $routes): array } /** - * @param array{id:string,path:string,action:mixed} $baseRoute + * @phpstan-param RouteArray $baseRoute * @param array $parameters - * @return array{id:string,path:string,action:mixed,parameters:array} + * @phpstan-return MatchedRouteArray */ private function createMatchedRoute( array $baseRoute, array $parameters = [] ): array { - $baseRoute['parameters'] = $parameters; + $baseRoute['parameters'] = array_replace( + ($baseRoute['parameters'] ?? []), + $parameters + ); return $baseRoute; } /** - * @return array{id:string,path:string,action:mixed,parameters:array}|null + * @phpstan-return MatchedRouteArray|null * @throws OutOfBoundsException If there is no request URI in the server vars * @throws InvalidArgumentException If the request URI is invalid */ @@ -216,7 +242,7 @@ public function generatePath(string $routeId, array $parameters = []): string } /** - * @param array $routes + * @phpstan-param RouteArray[] $routes * @throws InvalidArgumentException If there are no routes * @throws InvalidArgumentException If a route is missing elements */ @@ -226,23 +252,26 @@ private function setRoutes(array $routes): self throw new InvalidArgumentException('There are no routes'); } - $numExpectedRouteEls = count(self::EMPTY_ROUTE); + $mandatoryRouteElements = array_filter(self::ROUTE_ELEMENTS); + $numMandatoryRouteElements = count($mandatoryRouteElements); foreach ($routes as $i => $route) { - $filteredRoute = array_intersect_key($route, self::EMPTY_ROUTE); + // In reality, some routes won't be valid, hence why we need to adjust PHPStan's expectations. + /** @phpstan-var mixed[] */ + $filteredRoute = array_intersect_key($route, self::ROUTE_ELEMENTS); + + $numMandatoryInFiltered = count(array_intersect_key($filteredRoute, $mandatoryRouteElements)); - // In reality, some routes may not be what we were hoping for, hence why we need to adjust PHPStan's - // expectations. - /** @phpstan-var mixed[] $filteredRoute */ - if ($numExpectedRouteEls !== count($filteredRoute)) { + if ($numMandatoryRouteElements !== $numMandatoryInFiltered) { throw new InvalidArgumentException( "The route at index `{$i}` is missing elements. " . - 'Required: ' . implode(', ', array_keys(self::EMPTY_ROUTE)) . '.' + 'Required: ' . implode(', ', array_keys($mandatoryRouteElements)) . '.' ); } + /** @phpstan-var RouteArray $filteredRoute */ + $routeId = $route['id']; - /** @var array{id:string,path:string,action:mixed} $filteredRoute */ $this->routes[$routeId] = $filteredRoute; } @@ -250,7 +279,7 @@ private function setRoutes(array $routes): self } /** - * @return array + * @phpstan-return IndexedRouteArrayArray */ public function getRoutes(): array { @@ -258,7 +287,7 @@ public function getRoutes(): array } /** - * @return array + * @phpstan-return PlaceholdersArray */ private function getRoutePlaceholders(string $routeId): array { @@ -269,9 +298,9 @@ private function getRoutePlaceholders(string $routeId): array $matches = null; $matched = (bool) preg_match_all("~\{($placeholderNamePattern)\}~", $route['path'], $matches); - /** @var array */ + /** @phpstan-var PlaceholdersArray */ $placeholders = $matched - // name => placeholder + // Name => placeholder. E.g. "articleId" => "{articleId}". ? array_combine($matches[1], $matches[0]) : [] ; diff --git a/tests/src/RouterTest.php b/tests/src/RouterTest.php index dd114dc..54f8c64 100755 --- a/tests/src/RouterTest.php +++ b/tests/src/RouterTest.php @@ -104,7 +104,7 @@ public function providesRoutesContainingInvalid(): array /** * @dataProvider providesRoutesContainingInvalid - * @param array $routesContainingInvalid (Using the valid type to silence PHPStan.) + * @phpstan-param RouteArray[] $routesContainingInvalid (Using the valid type to silence PHPStan.) */ public function testThrowsAnExceptionIfARouteIsInvalid( int $routeIndex, @@ -273,13 +273,56 @@ public function providesMatchableRoutes(): array ], $this->createHttpRequest(['REQUEST_URI' => '/articles/the-quick-brown-fox']), ], + [ + [ + 'id' => 'showAboutPage', + 'path' => '/about', + 'action' => 'ShowAboutPageAction', + 'parameters' => [ + 'articleId' => 'about-page', + ], + ], + [ + [ + 'id' => 'showAboutPage', + 'path' => '/about', + 'action' => 'ShowAboutPageAction', + 'parameters' => [ + 'articleId' => 'about-page', + ], + ], + ], + $this->createHttpRequest(['REQUEST_URI' => '/about']), + ], + // 'Default' parameters will be overridden. + [ + [ + 'id' => 'showArticle', + 'path' => '/articles/{articleId}', + 'action' => 'anything', + 'parameters' => [ + 'articleId' => '456', + ], + ], + [ + [ + 'id' => 'showArticle', + 'path' => '/articles/{articleId}', + 'action' => 'anything', + 'parameters' => [ + 'articleId' => '123', + ], + ], + ], + $this->createHttpRequest(['REQUEST_URI' => '/articles/456']), + ], ]; } /** * @dataProvider providesMatchableRoutes * @param array{path:string,action:mixed,parameters:string[]} $expectedRoute - * @param array $routes + * @phpstan-param RouteArray[] $routes */ public function testMatchAttemptsToFindAMatchingRoute( array $expectedRoute, @@ -332,7 +375,7 @@ public function providesUnmatchableRoutes(): array /** * @dataProvider providesUnmatchableRoutes - * @param array $unmatchableRoutes + * @phpstan-param RouteArray[] $unmatchableRoutes */ public function testMatchReturnsNullIfThereIsNoMatchingRoute( array $unmatchableRoutes, @@ -478,7 +521,7 @@ public function providesIncompleteArgsForGeneratepath(): array /** * @dataProvider providesIncompleteArgsForGeneratepath - * @param array $routes + * @phpstan-param RouteArray[] $routes * @param array $parameters */ public function testGeneratepathThrowsAnExceptionIfInsufficientParametersWerePassed(