v5 - Refactor routing to use RouteMatch and dispatcher interfaces#3438
v5 - Refactor routing to use RouteMatch and dispatcher interfaces#3438odan merged 1 commit intoslimphp:5.xfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors Slim v5 routing to replace request-attribute based RouteContext::ROUTING_RESULTS / RoutingResults with an immutable RouteMatch object and interface-driven dispatching, updating middleware and tests accordingly.
Changes:
- Introduces
DispatcherInterfacewith a defaultFastRouteDispatcher, and wires it into container definitions. - Adds
RouteInterfaceand updatesRoute+ routing/endpoint middleware to operate onRouteMatchattached to the request. - Replaces legacy routing context/results tests with
RouteMatch-focused tests and removes deprecated classes/tests.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Routing/RoutingResultsTest.php | Removes legacy RoutingResults test coverage. |
| tests/Routing/RouteMatchTest.php | Adds tests for the new RouteMatch immutable routing result. |
| tests/Routing/RouteContextTest.php | Removes legacy RouteContext test coverage. |
| tests/Middleware/RoutingMiddlewareTest.php | Updates middleware tests to assert RouteMatch request attribute behavior. |
| tests/Middleware/BasePathMiddlewareTest.php | Updates base path assertions to use app/router state rather than request attributes. |
| Slim/Routing/RoutingResults.php | Removes legacy routing result object. |
| Slim/Routing/RouteMatch.php | Adds the new immutable routing result object. |
| Slim/Routing/RouteContext.php | Removes legacy routing context helper. |
| Slim/Routing/Route.php | Implements RouteInterface and adds route argument storage accessors. |
| Slim/Routing/FastRouteDispatcher.php | Adds default dispatcher implementation backed by FastRoute. |
| Slim/Middleware/RoutingMiddleware.php | Refactors routing middleware to use injected dispatcher and attach RouteMatch. |
| Slim/Middleware/RoutingArgumentsMiddleware.php | Reads route arguments from RouteMatch and attaches them to request attributes. |
| Slim/Middleware/EndpointMiddleware.php | Executes endpoints based on RouteMatch and refactors middleware collection ordering. |
| Slim/Middleware/BasePathMiddleware.php | Shifts base path handling to router state (removing request attribute use). |
| Slim/Interfaces/RouteInterface.php | Introduces a route abstraction consumed by routing components. |
| Slim/Interfaces/DispatcherInterface.php | Introduces a dispatcher abstraction for route matching. |
| Slim/Container/Definition/SlimDefinitions.php | Registers DispatcherInterface in the container using FastRouteDispatcher. |
| CHANGELOG.md | Updates changelog entries related to removed/deprecated routing APIs. |
Comments suppressed due to low confidence (1)
Slim/Middleware/RoutingMiddleware.php:127
stripBasePath()blindly stripsstrlen($basePath)bytes from$uriwithout verifying that$uriactually starts with$basePath(or that$basePathis shorter than$uri). With a misconfigured base path this can turn unrelated paths into'/'and route incorrectly. Guard with a prefix check (and only strip when it matches), otherwise return the original$uri.
private function stripBasePath(string $uri, string $basePath): string
{
// No base path configured
if ($basePath === '' || $basePath === '/') {
return $uri;
}
return '/' . ltrim(rtrim(substr($uri, strlen($basePath)), '/'), '/');
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * | ||
| * @var array<string, string> | ||
| */ | ||
| private array $arguments; |
There was a problem hiding this comment.
$arguments is a typed property but never initialized (no default value and not set in the constructor). Calling getArgument()/getArguments() before setArguments() will throw "Typed property must not be accessed before initialization". Initialize it to an empty array (e.g., default [] on the property or set it in the constructor).
| private array $arguments; | |
| private array $arguments = []; |
| $requestPath = $request->getUri()->getPath(); | ||
| $basePath = $this->router->getBasePath(); | ||
| $dispatchPath = $this->stripBasePath($requestPath, $this->router->getBasePath()); | ||
|
|
||
| $httpMethod = $request->getMethod(); | ||
| $uri = $request->getUri()->getPath(); | ||
| $routingResult = $this->dispatcher->dispatch( | ||
| $request->getMethod(), | ||
| rawurldecode($dispatchPath) | ||
| ); | ||
|
|
||
| // Determine base path | ||
| $basePath = $request->getAttribute(RouteContext::BASE_PATH) ?? $this->router->getBasePath(); | ||
| $routeMatch = $this->createRouteMatch($routingResult, $basePath); | ||
| $request = $request->withAttribute(RouteMatch::class, $routeMatch); | ||
|
|
||
| return $handler->handle($request); | ||
| } | ||
|
|
||
| /** | ||
| * @param array<int, mixed> $routingResult | ||
| */ | ||
| private function createRouteMatch(array $routingResult, string $basePath): RouteMatch | ||
| { | ||
| $status = $routingResult[0] ?? null; | ||
|
|
||
| return match ($status) { | ||
| DispatcherInterface::FOUND => RouteMatch::found( | ||
| $this->assertRoute($routingResult[1] ?? null), | ||
| $this->extractArguments($routingResult[2] ?? null), | ||
| ), | ||
| DispatcherInterface::METHOD_NOT_ALLOWED => RouteMatch::methodNotAllowed( | ||
| $this->extractAllowedMethods($routingResult[1] ?? null), | ||
| ), | ||
| DispatcherInterface::NOT_FOUND => RouteMatch::notFound(), | ||
| default => throw new RuntimeException('Invalid routing result status returned by dispatcher.'), | ||
| }; |
There was a problem hiding this comment.
createRouteMatch() takes $basePath, but the value is never used inside the method (and the caller computes/passes it anyway). Either remove this parameter (and related $basePath local) or incorporate base path into the routing result object if that’s still required by the new design.
| // $request = $request->withAttribute(RouteMatch::BASE_PATH_ATTRIBUTE, $basePath); | ||
|
|
There was a problem hiding this comment.
There’s a commented-out line referencing RouteMatch::BASE_PATH_ATTRIBUTE, but RouteMatch does not define that constant. Leaving dead/commented code with invalid identifiers makes the intent unclear—either remove this line or implement the attribute/constant as part of the new base-path strategy.
| // $request = $request->withAttribute(RouteMatch::BASE_PATH_ATTRIBUTE, $basePath); |
| $basePath = '/api'; | ||
|
|
||
| $routeMatch = RouteMatch::notFound($basePath); | ||
|
|
There was a problem hiding this comment.
RouteMatch::notFound() is called with $basePath, but RouteMatch::notFound() is defined with no parameters. This test will fatal; either update the factory signature/implementation to accept base path (if intended) or remove the argument from the test.
| $basePath = '/api'; | |
| $routeMatch = RouteMatch::notFound($basePath); | |
| $routeMatch = RouteMatch::notFound(); |
| $basePath = '/api'; | ||
|
|
||
| $routeMatch = RouteMatch::methodNotAllowed($allowedMethods, $basePath); |
There was a problem hiding this comment.
RouteMatch::methodNotAllowed() is called with ($allowedMethods, $basePath), but RouteMatch::methodNotAllowed() only accepts the allowed-methods array. This mismatch will fatal at runtime; align the test with the factory signature, or update RouteMatch if base path is meant to be carried in the match object.
| $basePath = '/api'; | |
| $routeMatch = RouteMatch::methodNotAllowed($allowedMethods, $basePath); | |
| $routeMatch = RouteMatch::methodNotAllowed($allowedMethods); |
| use Slim\Middleware\RoutingMiddleware; | ||
| use Slim\Routing\RouteContext; | ||
| use Slim\Routing\RouteMatch; | ||
| use Slim\Routing\RoutingResults; |
There was a problem hiding this comment.
use Slim\\Routing\\RoutingResults; is now unused and refers to a class removed by this PR. While it won’t autoload, it’s confusing and can cause IDE/static-analysis noise—please remove the import.
| use Slim\Routing\RoutingResults; |
| /** | ||
| * Retrieve a specific route argument. | ||
| */ | ||
| public function getArgument(string $name, ?string $default = null): ?string; | ||
|
|
||
| /** | ||
| * Get route arguments. | ||
| * | ||
| * @return array<string, string> | ||
| */ | ||
| public function getArguments(): array; | ||
|
|
||
| /** | ||
| * Set route arguments. | ||
| * | ||
| * @param array<string,mixed> $arguments The arguments. | ||
| * | ||
| * @return RouteInterface | ||
| */ | ||
| public function setArguments(array $arguments): RouteInterface; |
There was a problem hiding this comment.
The RouteInterface argument APIs are internally inconsistent: getArgument()/getArguments() are documented/typed as returning strings, but setArguments() documents array<string, mixed>. Either restrict setArguments() to array<string, string> (and enforce/validate) or widen the getters’ types/docs so callers aren’t misled.
This is a routing architecture refactor that replaces request attributes based on
RouteContext::ROUTING_RESULTSwithRouteMatch::classChanges
DispatcherInterfaceand addFastRouteDispatcheras the default dispatcher implementationDispatcherInterfacein container definitions and resolve it viaFastRouteDispatcherRouteInterfaceand updateRouteto implement itRouteMatchas the new immutable routing result object for found, not found, and method-not-allowed statesRoutingMiddlewareto use injected dispatcher and attachRouteMatchto the requestEndpointMiddlewareto consumeRouteMatchinstead ofRoutingResultsRoutingArgumentsMiddlewareto read route arguments fromRouteMatchRoutingResultsandRouteContextclassesRouteviagetArgument(),getArguments(), andsetArguments()RouteMatchbehavior and replace legacy routing context/result assertionsRouteContextandRoutingResultssetArgumentsandsetArgument