Skip to content

Commit 2f558e3

Browse files
authored
Refactor routing architecture
1 parent ad393ae commit 2f558e3

19 files changed

Lines changed: 598 additions & 679 deletions

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5555
* Router cache file support (File IO was never sufficient. PHP OpCache is much faster)
5656
* Removed `BodyParsingMiddlewareTest` in favor of `JsonBodyParserMiddleware`, `XmlBodyParserMiddleware` and `FormUrlEncodedBodyParserMiddleware`.
5757
* The `$app->redirect()` method because it was not aware of the basePath. Use the `UrlGenerator` instead.
58-
* The route `setArguments` and `setArgument` methods. Use a middleware for custom route arguments now.
59-
* The `RouteContext::ROUTE` const. Use `$route = $request->getAttribute(RouteContext::ROUTING_RESULTS)->getRoute();` instead.
6058
* Old tests for PHP 7
6159
* Psalm
6260
* phpspec/prophecy

Slim/Container/Definition/SlimDefinitions.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
use Slim\Emitter\ResponseEmitter;
1919
use Slim\Interfaces\ContainerResolverInterface;
2020
use Slim\Interfaces\DefinitionsInterface;
21+
use Slim\Interfaces\DispatcherInterface;
2122
use Slim\Interfaces\EmitterInterface;
2223
use Slim\Interfaces\RequestHandlerInvocationStrategyInterface;
2324
use Slim\Interfaces\RouterInterface;
2425
use Slim\Interfaces\UrlGeneratorInterface;
26+
use Slim\Routing\FastRouteDispatcher;
2527
use Slim\Routing\Router;
2628
use Slim\Routing\UrlGenerator;
2729
use Slim\Strategy\RequestResponse;
@@ -64,6 +66,10 @@ public function getDefinitions(): array
6466
return $container->get(Router::class);
6567
},
6668

69+
DispatcherInterface::class => function (ContainerInterface $container) {
70+
return $container->get(FastRouteDispatcher::class);
71+
},
72+
6773
UrlGeneratorInterface::class => function (ContainerInterface $container) {
6874
return $container->get(UrlGenerator::class);
6975
},
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Slim\Interfaces;
6+
7+
interface DispatcherInterface
8+
{
9+
const NOT_FOUND = 0;
10+
const FOUND = 1;
11+
const METHOD_NOT_ALLOWED = 2;
12+
13+
/**
14+
* @return array<int, mixed>
15+
*/
16+
public function dispatch(string $httpMethod, string $uri): array;
17+
}

Slim/Interfaces/RouteInterface.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/**
4+
* Slim Framework (https://slimframework.com)
5+
*
6+
* @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
7+
*/
8+
9+
namespace Slim\Interfaces;
10+
11+
use Psr\Http\Server\MiddlewareInterface;
12+
use Slim\Routing\RouteGroup;
13+
14+
interface RouteInterface
15+
{
16+
/**
17+
* Get route callable.
18+
*
19+
* @return callable|string
20+
*/
21+
public function getHandler(): callable|string;
22+
23+
/**
24+
* Set route name.
25+
*
26+
* @param string $name The route name
27+
*
28+
* @return RouteInterface
29+
*/
30+
public function setName(string $name): RouteInterface;
31+
32+
/**
33+
* Get route name.
34+
*/
35+
public function getName(): ?string;
36+
37+
/**
38+
* Get route pattern.
39+
*
40+
* @return string
41+
*/
42+
public function getPattern(): string;
43+
44+
/**
45+
* Get route HTTP methods
46+
*
47+
* @return array<string>
48+
*/
49+
public function getMethods(): array;
50+
51+
/**
52+
* Get route group.
53+
*
54+
* @return RouteGroup|null
55+
*/
56+
public function getRouteGroup(): ?RouteGroup;
57+
58+
/**
59+
* Retrieve a specific route argument.
60+
*/
61+
public function getArgument(string $name, ?string $default = null): ?string;
62+
63+
/**
64+
* Get route arguments.
65+
*
66+
* @return array<string, string>
67+
*/
68+
public function getArguments(): array;
69+
70+
/**
71+
* Set route arguments.
72+
*
73+
* @param array<string,mixed> $arguments The arguments.
74+
*
75+
* @return RouteInterface
76+
*/
77+
public function setArguments(array $arguments): RouteInterface;
78+
79+
/**
80+
* @return array<MiddlewareInterface|callable|string>
81+
*/
82+
public function getMiddleware(): array;
83+
}

Slim/Middleware/BasePathMiddleware.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Psr\Http\Server\MiddlewareInterface;
1616
use Psr\Http\Server\RequestHandlerInterface;
1717
use Slim\Interfaces\RouterInterface;
18-
use Slim\Routing\RouteContext;
1918

2019
final class BasePathMiddleware implements MiddlewareInterface
2120
{
@@ -46,7 +45,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
4645
$basePath = $this->getBasePathByRequestUri($request);
4746
}
4847

49-
$request = $request->withAttribute(RouteContext::BASE_PATH, $basePath);
48+
// $request = $request->withAttribute(RouteMatch::BASE_PATH_ATTRIBUTE, $basePath);
5049

5150
$this->router->setBasePath($basePath);
5251

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<?php
22

3+
/**
4+
* Slim Framework (https://slimframework.com)
5+
*
6+
* @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
7+
*/
8+
39
namespace Slim\Middleware;
410

511
use Psr\Http\Message\ResponseInterface;
@@ -9,17 +15,17 @@
915
use RuntimeException;
1016
use Slim\Exception\HttpMethodNotAllowedException;
1117
use Slim\Exception\HttpNotFoundException;
18+
use Slim\Interfaces\RouteInterface;
1219
use Slim\Routing\PipelineRunner;
13-
use Slim\Routing\Route;
14-
use Slim\Routing\RouteContext;
1520
use Slim\Routing\RouteInvoker;
16-
use Slim\Routing\RoutingResults;
21+
use Slim\Routing\RouteMatch;
1722

1823
/**
19-
* This middleware processes the routing results to determine if a route was found,
20-
* if the HTTP method is allowed, or if the route was not found. Based on these results,
21-
* it either executes the found route's handler with its associated middleware stack or
22-
* throws appropriate exceptions for 404 Not Found or 405 Method Not Allowed.
24+
* Interprets the RouteMatch produced by RoutingMiddleware and either:
25+
* - executes the matched route together with its middleware stack, or
26+
* - throws the appropriate HTTP exception for 404 / 405 cases.
27+
*
28+
* This middleware is intended to be terminal within the routing pipeline.
2329
*/
2430
final class EndpointMiddleware implements MiddlewareInterface
2531
{
@@ -37,73 +43,88 @@ public function __construct(
3743

3844
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
3945
{
40-
/* @var RoutingResults $routingResults */
41-
$routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
46+
$routeMatch = $request->getAttribute(RouteMatch::class);
4247

43-
if (!$routingResults instanceof RoutingResults) {
48+
if (!$routeMatch instanceof RouteMatch) {
4449
throw new RuntimeException(
45-
'An unexpected error occurred while handling routing results. Routing results are not available.',
50+
'RouteMatch is missing from the request. Add RoutingMiddleware before EndpointMiddleware.',
4651
);
4752
}
4853

49-
$routeStatus = $routingResults->getRouteStatus();
50-
if ($routeStatus === RoutingResults::FOUND) {
51-
return $this->handleFound($request, $routingResults);
54+
if ($routeMatch->isFound()) {
55+
$route = $routeMatch->getRoute();
56+
57+
if (!$route instanceof RouteInterface) {
58+
throw new RuntimeException('RouteMatch is in FOUND state but does not contain a valid route.');
59+
}
60+
61+
return $this->handleFound($request, $route, $routeMatch->getArguments());
5262
}
5363

54-
if ($routeStatus === RoutingResults::NOT_FOUND) {
55-
// 404 Not Found
64+
if ($routeMatch->isNotFound()) {
5665
throw new HttpNotFoundException($request);
5766
}
5867

59-
if ($routeStatus === RoutingResults::METHOD_NOT_ALLOWED) {
60-
// 405 Method Not Allowed
68+
if ($routeMatch->isMethodNotAllowed()) {
6169
$exception = new HttpMethodNotAllowedException($request);
62-
$exception->setAllowedMethods($routingResults->getAllowedMethods());
70+
$exception->setAllowedMethods($routeMatch->getAllowedMethods());
6371

6472
throw $exception;
6573
}
6674

67-
throw new RuntimeException('An unexpected error occurred while endpoint handling.');
75+
throw new RuntimeException('An unexpected routing state was encountered.');
6876
}
6977

78+
/**
79+
* @param array<string, mixed> $arguments
80+
*/
7081
private function handleFound(
7182
ServerRequestInterface $request,
72-
RoutingResults $routingResults,
83+
RouteInterface $route,
84+
array $arguments,
7385
): ResponseInterface {
74-
$route = $routingResults->getRoute() ?? throw new RuntimeException('Route not found.');
75-
76-
// Collect route specific middleware
7786
$pipeline = $this->collectRouteMiddleware($route);
7887

79-
// Invoke the route/group specific middleware stack
8088
$pipeline[] = $this->routeInvoker->withHandler(
8189
$route->getHandler(),
82-
$routingResults->getRouteArguments(),
90+
$arguments,
8391
);
8492

85-
return $this->pipelineRunner->withPipeline($pipeline)->handle($request);
93+
return $this->pipelineRunner
94+
->withPipeline($pipeline)
95+
->handle($request);
8696
}
8797

8898
/**
89-
* @param Route $route
90-
* @return array<MiddlewareInterface|callable|string> List of middleware
99+
* Collects middleware in execution order:
100+
* - outermost parent group middleware first
101+
* - nested group middleware next
102+
* - route-specific middleware last
103+
*
104+
* @return array<MiddlewareInterface|callable|string>
91105
*/
92-
private function collectRouteMiddleware(Route $route): array
106+
private function collectRouteMiddleware(RouteInterface $route): array
93107
{
94-
$middlewares = [];
95-
96-
// Append group specific middleware from all parent route groups
108+
$groupMiddlewareStack = [];
97109
$group = $route->getRouteGroup();
98110

99-
while ($group) {
100-
// Prepend group middleware so outer groups come first
101-
$middlewares = array_merge($group->getMiddleware(), $middlewares);
111+
while ($group !== null) {
112+
array_unshift($groupMiddlewareStack, $group->getMiddleware());
102113
$group = $group->getRouteGroup();
103114
}
104115

105-
// Append endpoint-specific middleware
106-
return array_merge($middlewares, $route->getMiddleware());
107-
}
116+
$pipeline = [];
117+
118+
foreach ($groupMiddlewareStack as $middlewareList) {
119+
foreach ($middlewareList as $middleware) {
120+
$pipeline[] = $middleware;
121+
}
122+
}
108123

124+
foreach ($route->getMiddleware() as $middleware) {
125+
$pipeline[] = $middleware;
126+
}
127+
128+
return $pipeline;
129+
}
109130
}

Slim/Middleware/RoutingArgumentsMiddleware.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
use Psr\Http\Message\ServerRequestInterface;
1515
use Psr\Http\Server\MiddlewareInterface;
1616
use Psr\Http\Server\RequestHandlerInterface;
17-
use Slim\Routing\RouteContext;
18-
use Slim\Routing\RoutingResults;
17+
use Slim\Routing\RouteMatch;
1918

2019
/**
2120
* Add routing arguments to the request attributes.
@@ -24,11 +23,11 @@ final class RoutingArgumentsMiddleware implements MiddlewareInterface
2423
{
2524
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
2625
{
27-
/* @var RoutingResults|null $routingResults */
28-
$routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS);
26+
/* @var RouteMatch|null $routeMatch */
27+
$routeMatch = $request->getAttribute(RouteMatch::class);
2928

30-
if ($routingResults instanceof RoutingResults) {
31-
foreach ($routingResults->getRouteArguments() as $key => $value) {
29+
if ($routeMatch instanceof RouteMatch) {
30+
foreach ($routeMatch->getArguments() as $key => $value) {
3231
$request = $request->withAttribute($key, $value);
3332
}
3433
}

0 commit comments

Comments
 (0)