diff --git a/CHANGELOG.md b/CHANGELOG.md index d07d251b6..18caddd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,8 +55,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Router cache file support (File IO was never sufficient. PHP OpCache is much faster) * Removed `BodyParsingMiddlewareTest` in favor of `JsonBodyParserMiddleware`, `XmlBodyParserMiddleware` and `FormUrlEncodedBodyParserMiddleware`. * The `$app->redirect()` method because it was not aware of the basePath. Use the `UrlGenerator` instead. -* The route `setArguments` and `setArgument` methods. Use a middleware for custom route arguments now. -* The `RouteContext::ROUTE` const. Use `$route = $request->getAttribute(RouteContext::ROUTING_RESULTS)->getRoute();` instead. * Old tests for PHP 7 * Psalm * phpspec/prophecy diff --git a/Slim/Container/Definition/SlimDefinitions.php b/Slim/Container/Definition/SlimDefinitions.php index 00a3f2344..d3e38fd18 100644 --- a/Slim/Container/Definition/SlimDefinitions.php +++ b/Slim/Container/Definition/SlimDefinitions.php @@ -18,10 +18,12 @@ use Slim\Emitter\ResponseEmitter; use Slim\Interfaces\ContainerResolverInterface; use Slim\Interfaces\DefinitionsInterface; +use Slim\Interfaces\DispatcherInterface; use Slim\Interfaces\EmitterInterface; use Slim\Interfaces\RequestHandlerInvocationStrategyInterface; use Slim\Interfaces\RouterInterface; use Slim\Interfaces\UrlGeneratorInterface; +use Slim\Routing\FastRouteDispatcher; use Slim\Routing\Router; use Slim\Routing\UrlGenerator; use Slim\Strategy\RequestResponse; @@ -64,6 +66,10 @@ public function getDefinitions(): array return $container->get(Router::class); }, + DispatcherInterface::class => function (ContainerInterface $container) { + return $container->get(FastRouteDispatcher::class); + }, + UrlGeneratorInterface::class => function (ContainerInterface $container) { return $container->get(UrlGenerator::class); }, diff --git a/Slim/Interfaces/DispatcherInterface.php b/Slim/Interfaces/DispatcherInterface.php new file mode 100644 index 000000000..5bde8d2e3 --- /dev/null +++ b/Slim/Interfaces/DispatcherInterface.php @@ -0,0 +1,17 @@ + + */ + public function dispatch(string $httpMethod, string $uri): array; +} diff --git a/Slim/Interfaces/RouteInterface.php b/Slim/Interfaces/RouteInterface.php new file mode 100644 index 000000000..5f8955c03 --- /dev/null +++ b/Slim/Interfaces/RouteInterface.php @@ -0,0 +1,83 @@ + + */ + public function getMethods(): array; + + /** + * Get route group. + * + * @return RouteGroup|null + */ + public function getRouteGroup(): ?RouteGroup; + + /** + * Retrieve a specific route argument. + */ + public function getArgument(string $name, ?string $default = null): ?string; + + /** + * Get route arguments. + * + * @return array + */ + public function getArguments(): array; + + /** + * Set route arguments. + * + * @param array $arguments The arguments. + * + * @return RouteInterface + */ + public function setArguments(array $arguments): RouteInterface; + + /** + * @return array + */ + public function getMiddleware(): array; +} diff --git a/Slim/Middleware/BasePathMiddleware.php b/Slim/Middleware/BasePathMiddleware.php index 6f9e1ec27..1a26ad1d0 100644 --- a/Slim/Middleware/BasePathMiddleware.php +++ b/Slim/Middleware/BasePathMiddleware.php @@ -15,7 +15,6 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Interfaces\RouterInterface; -use Slim\Routing\RouteContext; final class BasePathMiddleware implements MiddlewareInterface { @@ -46,7 +45,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $basePath = $this->getBasePathByRequestUri($request); } - $request = $request->withAttribute(RouteContext::BASE_PATH, $basePath); + // $request = $request->withAttribute(RouteMatch::BASE_PATH_ATTRIBUTE, $basePath); $this->router->setBasePath($basePath); diff --git a/Slim/Middleware/EndpointMiddleware.php b/Slim/Middleware/EndpointMiddleware.php index 9868d96ea..7aa32e4d9 100644 --- a/Slim/Middleware/EndpointMiddleware.php +++ b/Slim/Middleware/EndpointMiddleware.php @@ -1,5 +1,11 @@ getAttribute(RouteContext::ROUTING_RESULTS); + $routeMatch = $request->getAttribute(RouteMatch::class); - if (!$routingResults instanceof RoutingResults) { + if (!$routeMatch instanceof RouteMatch) { throw new RuntimeException( - 'An unexpected error occurred while handling routing results. Routing results are not available.', + 'RouteMatch is missing from the request. Add RoutingMiddleware before EndpointMiddleware.', ); } - $routeStatus = $routingResults->getRouteStatus(); - if ($routeStatus === RoutingResults::FOUND) { - return $this->handleFound($request, $routingResults); + if ($routeMatch->isFound()) { + $route = $routeMatch->getRoute(); + + if (!$route instanceof RouteInterface) { + throw new RuntimeException('RouteMatch is in FOUND state but does not contain a valid route.'); + } + + return $this->handleFound($request, $route, $routeMatch->getArguments()); } - if ($routeStatus === RoutingResults::NOT_FOUND) { - // 404 Not Found + if ($routeMatch->isNotFound()) { throw new HttpNotFoundException($request); } - if ($routeStatus === RoutingResults::METHOD_NOT_ALLOWED) { - // 405 Method Not Allowed + if ($routeMatch->isMethodNotAllowed()) { $exception = new HttpMethodNotAllowedException($request); - $exception->setAllowedMethods($routingResults->getAllowedMethods()); + $exception->setAllowedMethods($routeMatch->getAllowedMethods()); throw $exception; } - throw new RuntimeException('An unexpected error occurred while endpoint handling.'); + throw new RuntimeException('An unexpected routing state was encountered.'); } + /** + * @param array $arguments + */ private function handleFound( ServerRequestInterface $request, - RoutingResults $routingResults, + RouteInterface $route, + array $arguments, ): ResponseInterface { - $route = $routingResults->getRoute() ?? throw new RuntimeException('Route not found.'); - - // Collect route specific middleware $pipeline = $this->collectRouteMiddleware($route); - // Invoke the route/group specific middleware stack $pipeline[] = $this->routeInvoker->withHandler( $route->getHandler(), - $routingResults->getRouteArguments(), + $arguments, ); - return $this->pipelineRunner->withPipeline($pipeline)->handle($request); + return $this->pipelineRunner + ->withPipeline($pipeline) + ->handle($request); } /** - * @param Route $route - * @return array List of middleware + * Collects middleware in execution order: + * - outermost parent group middleware first + * - nested group middleware next + * - route-specific middleware last + * + * @return array */ - private function collectRouteMiddleware(Route $route): array + private function collectRouteMiddleware(RouteInterface $route): array { - $middlewares = []; - - // Append group specific middleware from all parent route groups + $groupMiddlewareStack = []; $group = $route->getRouteGroup(); - while ($group) { - // Prepend group middleware so outer groups come first - $middlewares = array_merge($group->getMiddleware(), $middlewares); + while ($group !== null) { + array_unshift($groupMiddlewareStack, $group->getMiddleware()); $group = $group->getRouteGroup(); } - // Append endpoint-specific middleware - return array_merge($middlewares, $route->getMiddleware()); - } + $pipeline = []; + + foreach ($groupMiddlewareStack as $middlewareList) { + foreach ($middlewareList as $middleware) { + $pipeline[] = $middleware; + } + } + foreach ($route->getMiddleware() as $middleware) { + $pipeline[] = $middleware; + } + + return $pipeline; + } } diff --git a/Slim/Middleware/RoutingArgumentsMiddleware.php b/Slim/Middleware/RoutingArgumentsMiddleware.php index b632e9e32..7179496c2 100644 --- a/Slim/Middleware/RoutingArgumentsMiddleware.php +++ b/Slim/Middleware/RoutingArgumentsMiddleware.php @@ -14,8 +14,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Slim\Routing\RouteContext; -use Slim\Routing\RoutingResults; +use Slim\Routing\RouteMatch; /** * Add routing arguments to the request attributes. @@ -24,11 +23,11 @@ final class RoutingArgumentsMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - /* @var RoutingResults|null $routingResults */ - $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS); + /* @var RouteMatch|null $routeMatch */ + $routeMatch = $request->getAttribute(RouteMatch::class); - if ($routingResults instanceof RoutingResults) { - foreach ($routingResults->getRouteArguments() as $key => $value) { + if ($routeMatch instanceof RouteMatch) { + foreach ($routeMatch->getArguments() as $key => $value) { $request = $request->withAttribute($key, $value); } } diff --git a/Slim/Middleware/RoutingMiddleware.php b/Slim/Middleware/RoutingMiddleware.php index 127945612..6c69ab5ab 100644 --- a/Slim/Middleware/RoutingMiddleware.php +++ b/Slim/Middleware/RoutingMiddleware.php @@ -6,90 +6,120 @@ * @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License) */ -declare(strict_types=1); - namespace Slim\Middleware; -use FastRoute\Dispatcher\GroupCountBased; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use RuntimeException; +use Slim\Interfaces\DispatcherInterface; +use Slim\Interfaces\RouteInterface; use Slim\Interfaces\RouterInterface; -use Slim\Routing\RouteContext; -use Slim\Routing\RoutingResults; +use Slim\Routing\RouteMatch; /** - * Middleware for resolving routes. + * Resolves the current request against the registered routes and stores + * the immutable RouteMatch on the request attributes. * - * This middleware handles the routing process by dispatching the request to the appropriate route - * based on the HTTP method and URI. It then stores the routing results in the request attributes. + * This middleware should run before the endpoint runner middleware. */ final class RoutingMiddleware implements MiddlewareInterface { + private DispatcherInterface $dispatcher; + private RouterInterface $router; - public function __construct(RouterInterface $router) - { + public function __construct( + DispatcherInterface $dispatcher, + RouterInterface $router + ) { + $this->dispatcher = $dispatcher; $this->router = $router; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - // Dispatch - $dispatcher = new GroupCountBased($this->router->getRouteCollector()->getData()); + $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 $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.'), + }; + } - if (is_string($basePath)) { - // Remove base path for the dispatcher - $uri = $this->removeBasePath($uri, $basePath); + private function assertRoute(mixed $route): RouteInterface + { + if (!$route instanceof RouteInterface) { + throw new RuntimeException('Dispatcher returned an invalid route for FOUND status.'); } - $routeInfo = $dispatcher->dispatch($httpMethod, rawurldecode($uri)); - $routeStatus = (int)$routeInfo[0]; - $routingResults = null; - - if ($routeStatus === RoutingResults::FOUND) { - $routingResults = new RoutingResults( - $routeStatus, - $routeInfo[1], - $request->getMethod(), - $uri, - $routeInfo[2], - ); + return $route; + } + + /** + * @return array + */ + private function extractArguments(mixed $arguments): array + { + if ($arguments === null) { + return []; } - if ($routeStatus === RoutingResults::METHOD_NOT_ALLOWED) { - $routingResults = new RoutingResults( - $routeStatus, - null, - $request->getMethod(), - $uri, - $routeInfo[1], - ); + if (!is_array($arguments)) { + throw new RuntimeException('Dispatcher returned invalid route arguments.'); } - if ($routeStatus === RoutingResults::NOT_FOUND) { - $routingResults = new RoutingResults($routeStatus, null, $request->getMethod(), $uri); + return $arguments; + } + + /** + * @return list + */ + private function extractAllowedMethods(mixed $allowedMethods): array + { + if ($allowedMethods === null) { + return []; } - if ($routingResults) { - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); + if (!is_array($allowedMethods)) { + throw new RuntimeException('Dispatcher returned invalid allowed methods.'); } - return $handler->handle($request); + return array_values($allowedMethods); } - private function removeBasePath(string $uri, string $basePath): string + private function stripBasePath(string $uri, string $basePath): string { // No base path configured - if (!$basePath || $basePath === '/') { + if ($basePath === '' || $basePath === '/') { return $uri; } diff --git a/Slim/Routing/FastRouteDispatcher.php b/Slim/Routing/FastRouteDispatcher.php new file mode 100644 index 000000000..57972ac97 --- /dev/null +++ b/Slim/Routing/FastRouteDispatcher.php @@ -0,0 +1,43 @@ +router = $router; + } + + public function dispatch(string $httpMethod, string $uri): array + { + return $this->getDispatcher()->dispatch($httpMethod, $uri); + } + + private function getDispatcher(): GroupCountBased + { + if ($this->dispatcher === null) { + $this->dispatcher = new GroupCountBased( + $this->router->getRouteCollector()->getData() + ); + } + + return $this->dispatcher; + } +} diff --git a/Slim/Routing/Route.php b/Slim/Routing/Route.php index 37dc8dd26..b5cf5dc7a 100644 --- a/Slim/Routing/Route.php +++ b/Slim/Routing/Route.php @@ -9,8 +9,9 @@ namespace Slim\Routing; use Slim\Interfaces\MiddlewareCollectionInterface; +use Slim\Interfaces\RouteInterface; -final class Route implements MiddlewareCollectionInterface +final class Route implements RouteInterface, MiddlewareCollectionInterface { use MiddlewareCollectionTrait; @@ -19,6 +20,9 @@ final class Route implements MiddlewareCollectionInterface */ private array $methods; + /** + * The route matching pattern + */ private string $pattern; /** @@ -26,10 +30,23 @@ final class Route implements MiddlewareCollectionInterface */ private $handler; + /** + * Route name + */ private ?string $name = null; + /** + * Parent route group + */ private ?RouteGroup $group; + /** + * Route parameters + * + * @var array + */ + private array $arguments; + /** * @param array $methods * @param string $pattern @@ -44,11 +61,17 @@ public function __construct(array $methods, string $pattern, callable|string $ha $this->group = $group; } + /** + * {@inheritdoc} + */ public function getHandler(): callable|string { return $this->handler; } + /** + * {@inheritdoc} + */ public function setName(string $name): self { $this->name = $name; @@ -56,26 +79,64 @@ public function setName(string $name): self return $this; } + /** + * {@inheritdoc} + */ public function getName(): ?string { return $this->name; } + /** + * {@inheritdoc} + */ public function getPattern(): string { return $this->pattern; } /** - * @return array + * {@inheritdoc} */ public function getMethods(): array { return $this->methods; } + /** + * {@inheritdoc} + */ public function getRouteGroup(): ?RouteGroup { return $this->group; } + + /** + * {@inheritdoc} + */ + public function getArgument(string $name, ?string $default = null): ?string + { + if (array_key_exists($name, $this->arguments)) { + return $this->arguments[$name]; + } + return $default; + } + + /** + * {@inheritdoc} + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments): RouteInterface + { + $this->arguments = $arguments; + + return $this; + } } diff --git a/Slim/Routing/RouteContext.php b/Slim/Routing/RouteContext.php deleted file mode 100644 index 6ee7de685..000000000 --- a/Slim/Routing/RouteContext.php +++ /dev/null @@ -1,84 +0,0 @@ -routingResults = $routingResults; - $this->basePath = $basePath; - } - - public static function fromRequest(ServerRequestInterface $request): self - { - /* @var RoutingResults|null $routingResults */ - $routingResults = $request->getAttribute(self::ROUTING_RESULTS); - - /* @var string|null $basePath */ - $basePath = $request->getAttribute(self::BASE_PATH); - - if (!$routingResults instanceof RoutingResults) { - throw new RuntimeException( - 'Cannot create RouteContext before routing has been completed. Add RoutingMiddleware to fix this.', - ); - } - - if ($basePath !== null && !is_string($basePath)) { - throw new RuntimeException( - sprintf('Invalid basePath attribute type: %s', gettype($basePath)), - ); - } - - return new self($routingResults, $basePath); - } - - public function getRoutingResults(): RoutingResults - { - return $this->routingResults; - } - - public function getBasePath(): ?string - { - return $this->basePath; - } - - public function getRoute(): ?Route - { - return $this->routingResults->getRoute(); - } - - /** - * @return array - */ - public function getArguments(): array - { - return $this->routingResults->getRouteArguments(); - } - - public function getArgument(string $key): mixed - { - return $this->routingResults->getRouteArgument($key); - } -} diff --git a/Slim/Routing/RouteInvoker.php b/Slim/Routing/RouteInvoker.php index 53bfcf422..45600e0e6 100644 --- a/Slim/Routing/RouteInvoker.php +++ b/Slim/Routing/RouteInvoker.php @@ -1,5 +1,11 @@ */ - private array $args = []; + private array $arguments = []; public function __construct( ResponseFactoryInterface $responseFactory, RequestHandlerInvocationStrategyInterface $invocationStrategy, - ContainerResolverInterface $containerResolver, + ContainerResolverInterface $resolver, ) { $this->responseFactory = $responseFactory; $this->invocationStrategy = $invocationStrategy; - $this->resolver = $containerResolver; + $this->resolver = $resolver; } /** - * Add handler. - * * @param callable|string $handler - * @param array $args - * - * @return self + * @param array $arguments */ - public function withHandler(callable|string $handler, array $args = []): self + public function withHandler(callable|string $handler, array $arguments = []): self { $clone = clone $this; $clone->handler = $this->resolver->resolveCallable($handler); - $clone->args = $args; + $clone->arguments = $arguments; return $clone; } @@ -57,8 +59,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface { if ($this->handler === null) { throw new RuntimeException( - 'RouteInvokerMiddleware: no handler has been assigned. ' . - 'Use withHandler() before using this middleware.', + 'RouteInvoker has no handler assigned. Call withHandler() before execution.', ); } @@ -66,7 +67,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $this->handler, $request, $this->responseFactory->createResponse(), - $this->args, + $this->arguments, ); } } diff --git a/Slim/Routing/RouteMatch.php b/Slim/Routing/RouteMatch.php new file mode 100644 index 000000000..d7fe0fc57 --- /dev/null +++ b/Slim/Routing/RouteMatch.php @@ -0,0 +1,111 @@ + */ + private array $arguments; + + /** @var list */ + private array $allowedMethods; + + /** + * @param array $arguments + * @param list $allowedMethods + */ + private function __construct( + int $status, + ?RouteInterface $route = null, + array $arguments = [], + array $allowedMethods = [], + ) { + $this->status = $status; + $this->route = $route; + $this->arguments = $arguments; + $this->allowedMethods = $allowedMethods; + } + + /** + * @param array $arguments + */ + public static function found( + RouteInterface $route, + array $arguments = [], + ): self { + return new self(DispatcherInterface::FOUND, $route, $arguments, []); + } + + public static function notFound(): self + { + return new self(DispatcherInterface::NOT_FOUND, null, [], []); + } + + /** + * @param list $allowedMethods + */ + public static function methodNotAllowed(array $allowedMethods): self + { + return new self(DispatcherInterface::METHOD_NOT_ALLOWED, null, [], $allowedMethods); + } + + public function isFound(): bool + { + return $this->status === DispatcherInterface::FOUND; + } + + public function isNotFound(): bool + { + return $this->status === DispatcherInterface::NOT_FOUND; + } + + public function isMethodNotAllowed(): bool + { + return $this->status === DispatcherInterface::METHOD_NOT_ALLOWED; + } + + public function getStatus(): int + { + return $this->status; + } + + public function getRoute(): ?RouteInterface + { + return $this->route; + } + + /** + * @return array + */ + public function getArguments(): array + { + return $this->arguments; + } + + public function getArgument(string $name, mixed $default = null): mixed + { + return $this->arguments[$name] ?? $default; + } + + /** + * @return list + */ + public function getAllowedMethods(): array + { + return $this->allowedMethods; + } + +} diff --git a/Slim/Routing/RoutingResults.php b/Slim/Routing/RoutingResults.php deleted file mode 100644 index 77162292f..000000000 --- a/Slim/Routing/RoutingResults.php +++ /dev/null @@ -1,103 +0,0 @@ - - */ - private array $routeArguments; - - /** - * @var array - */ - private array $allowedMethods; - - /** - * @param int $routeStatus - * @param ?Route $route - * @param string $method - * @param string $uri - * @param array $routeArguments - * @param array $allowedMethods - */ - public function __construct( - int $routeStatus, - ?Route $route, - string $method, - string $uri, - array $routeArguments = [], - array $allowedMethods = [], - ) { - $this->route = $route; - $this->method = $method; - $this->uri = $uri; - $this->routeStatus = $routeStatus; - $this->routeArguments = $routeArguments; - $this->allowedMethods = $allowedMethods; - } - - public function getRoute(): ?Route - { - return $this->route; - } - - public function getMethod(): string - { - return $this->method; - } - - public function getUri(): string - { - return $this->uri; - } - - public function getRouteStatus(): int - { - return $this->routeStatus; - } - - /** - * @return array - */ - public function getRouteArguments(): array - { - return $this->routeArguments; - } - - public function getRouteArgument(string $key): mixed - { - return $this->routeArguments[$key] ?? null; - } - - /** - * @return string[] - */ - public function getAllowedMethods(): array - { - return $this->allowedMethods; - } -} diff --git a/tests/Middleware/BasePathMiddlewareTest.php b/tests/Middleware/BasePathMiddlewareTest.php index 1865d341c..07e423a4a 100644 --- a/tests/Middleware/BasePathMiddlewareTest.php +++ b/tests/Middleware/BasePathMiddlewareTest.php @@ -229,9 +229,8 @@ public function testSubDirectoryWithFooPath(): void $app->add(BasePathMiddleware::class); $app->addRoutingMiddleware(); - $app->get('/foo', function ($request, ResponseInterface $response) { - $basePath = $this->get(RouterInterface::class)->getBasePath(); - $response->getBody()->write('basePath: ' . $basePath); + $app->get('/foo', function ($request, ResponseInterface $response) use ($app) { + $response->getBody()->write('basePath: ' . $app->getBasePath()); return $response; }); diff --git a/tests/Middleware/RoutingMiddlewareTest.php b/tests/Middleware/RoutingMiddlewareTest.php index fbfe32924..55180ad17 100644 --- a/tests/Middleware/RoutingMiddlewareTest.php +++ b/tests/Middleware/RoutingMiddlewareTest.php @@ -10,7 +10,6 @@ namespace Slim\Tests\Middleware; -use FastRoute\Dispatcher; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -18,11 +17,12 @@ use Slim\Exception\HttpMethodNotAllowedException; use Slim\Exception\HttpNotFoundException; use Slim\Factory\AppFactory; +use Slim\Interfaces\DispatcherInterface; use Slim\Interfaces\UrlGeneratorInterface; use Slim\Middleware\EndpointMiddleware; use Slim\Middleware\JsonBodyParserMiddleware; use Slim\Middleware\RoutingMiddleware; -use Slim\Routing\RouteContext; +use Slim\Routing\RouteMatch; use Slim\Routing\RoutingResults; use Slim\Tests\Traits\AppTestTrait; @@ -36,16 +36,11 @@ public function testRouteIsStoredOnSuccessfulMatch() $test = $this; $middleware = function (ServerRequestInterface $request, RequestHandlerInterface $handler) use ($test) { - // routingResults is available - /** @var RoutingResults $routingResults */ - $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS); - $test->assertInstanceOf(RoutingResults::class, $routingResults); - // route is available - $route = $routingResults->getRoute(); - $test->assertNotNull($route); + /** @var RouteMatch $routeMatch */ + $routeMatch = $request->getAttribute(RouteMatch::class); + $test->assertInstanceOf(RouteMatch::class, $routeMatch); - // routeParser is available return $handler->handle($request); }; @@ -103,14 +98,12 @@ public function testRouteIsNotStoredOnMethodNotAllowed() $request = $exception->getRequest(); // routingResults is available - /** @var RoutingResults $routingResults */ - $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS); - $test->assertInstanceOf(RoutingResults::class, $routingResults); - $test->assertSame(Dispatcher::METHOD_NOT_ALLOWED, $routingResults->getRouteStatus()); + /** @var RouteMatch $routeMatch */ + $routeMatch = $request->getAttribute(RouteMatch::class); + $test->assertSame(DispatcherInterface::METHOD_NOT_ALLOWED, $routeMatch->getStatus()); // route is not available - $route = $routingResults->getRoute(); - $test->assertNull($route); + $test->assertNull($routeMatch->getRoute()); // Re-throw to keep the behavior consistent throw $exception; @@ -148,14 +141,11 @@ public function testRouteIsNotStoredOnNotFound() $request = $exception->getRequest(); // routingResults is available - /** @var RoutingResults $routingResults */ - $routingResults = $request->getAttribute(RouteContext::ROUTING_RESULTS); - $test->assertInstanceOf(RoutingResults::class, $routingResults); - $test->assertSame(Dispatcher::NOT_FOUND, $routingResults->getRouteStatus()); + $routeMatch = $request->getAttribute(RouteMatch::class); + $test->assertSame(DispatcherInterface::NOT_FOUND, $routeMatch->getStatus()); // route is not available - $route = $routingResults->getRoute(); - $test->assertNull($route); + $test->assertNull($routeMatch->getRoute()); // Re-throw to keep the behavior consistent throw $exception; diff --git a/tests/Routing/RouteContextTest.php b/tests/Routing/RouteContextTest.php deleted file mode 100644 index dec989701..000000000 --- a/tests/Routing/RouteContextTest.php +++ /dev/null @@ -1,251 +0,0 @@ -getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $routingResults = new RoutingResults(200, null, 'GET', '/test', []); - $basePath = '/base-path'; - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults) - ->withAttribute(RouteContext::BASE_PATH, $basePath); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertSame($routingResults, $routeContext->getRoutingResults()); - $this->assertSame($basePath, $routeContext->getBasePath()); - } - - /** - * Tests that an exception is thrown when attempting to create a RouteContext - * without routing results attribute set in the request. - */ - public function testFromRequestThrowsExceptionIfRoutingResultsAreMissing(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $request = $request - ->withAttribute(RouteContext::BASE_PATH, '/base-path'); - - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage( - 'Cannot create RouteContext before routing has been completed. Add RoutingMiddleware to fix this.' - ); - - RouteContext::fromRequest($request); - } - - /** - * Tests that the RoutingResults instance returned by getRoutingResults matches - * the one originally provided in the request attributes. - */ - public function testGetRoutingResultsReturnsCorrectInstance(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $routingResults = new RoutingResults(200, null, 'GET', '/test', []); - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertSame($routingResults, $routeContext->getRoutingResults()); - } - - /** - * Tests that the base path value returned by getBasePath matches - * the one originally provided in the request attributes. - */ - public function testGetBasePathReturnsCorrectValue(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $routingResults = new RoutingResults(200, null, 'GET', '/test', []); - $basePath = '/base-path'; - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults) - ->withAttribute(RouteContext::BASE_PATH, $basePath); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertSame($basePath, $routeContext->getBasePath()); - } - - /** - * Tests that getBasePath returns null when no base path attribute - * was set in the request. - */ - public function testGetBasePathReturnsNullIfNotSet(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $routingResults = new RoutingResults(200, null, 'GET', '/test', []); - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertNull($routeContext->getBasePath()); - } - - /** - * Tests that getRoute() returns the correct Route instance when a route is matched - */ - public function testGetRouteReturnsCorrectInstance(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - // Create a route for testing - $route = $app->get('/test', function () {})->setName('test-route'); - $routingResults = new RoutingResults(200, $route, 'GET', '/test', []); - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertInstanceOf(Route::class, $routeContext->getRoute()); - $this->assertSame($route, $routeContext->getRoute()); - $this->assertSame('test-route', $routeContext->getRoute()->getName()); - } - - /** - * Tests that getRoute() returns null when no route is matched - */ - public function testGetRouteReturnsNullWhenNoRouteMatched(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $routingResults = new RoutingResults(404, null, 'GET', '/not-found', []); - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertNull($routeContext->getRoute()); - } - - /** - * Tests that getArguments() returns all route arguments correctly - */ - public function testGetArgumentsReturnsCorrectValues(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $arguments = ['id' => '123', 'name' => 'test']; - $routingResults = new RoutingResults(200, null, 'GET', '/test', $arguments); - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertSame($arguments, $routeContext->getArguments()); - } - - /** - * Tests that getArgument() returns the correct value for a specific argument key - */ - public function testGetArgumentReturnsCorrectValue(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $arguments = ['id' => '123', 'name' => 'test']; - $routingResults = new RoutingResults(200, null, 'GET', '/test', $arguments); - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertSame('123', $routeContext->getArgument('id')); - $this->assertSame('test', $routeContext->getArgument('name')); - } - - /** - * Tests that getArgument() returns null when the requested key doesn't exist - */ - public function testGetArgumentReturnsNullForNonExistentKey(): void - { - $app = AppFactory::create(); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/'); - - $arguments = ['id' => '123']; - $routingResults = new RoutingResults(200, null, 'GET', '/test', $arguments); - - $request = $request - ->withAttribute(RouteContext::ROUTING_RESULTS, $routingResults); - - $routeContext = RouteContext::fromRequest($request); - - $this->assertNull($routeContext->getArgument('non-existent')); - } -} diff --git a/tests/Routing/RouteMatchTest.php b/tests/Routing/RouteMatchTest.php new file mode 100644 index 000000000..ca9bdf0f6 --- /dev/null +++ b/tests/Routing/RouteMatchTest.php @@ -0,0 +1,104 @@ +createMock(RouteInterface::class); + + $arguments = ['id' => 42, 'slug' => 'test']; + $basePath = '/api'; + + $routeMatch = RouteMatch::found($route, $arguments); + + $this->assertTrue($routeMatch->isFound()); + $this->assertFalse($routeMatch->isNotFound()); + $this->assertFalse($routeMatch->isMethodNotAllowed()); + + $this->assertSame($route, $routeMatch->getRoute()); + $this->assertSame($arguments, $routeMatch->getArguments()); + $this->assertSame(42, $routeMatch->getArgument('id')); + $this->assertSame('test', $routeMatch->getArgument('slug')); + $this->assertNull($routeMatch->getArgument('missing')); + + $this->assertSame([], $routeMatch->getAllowedMethods()); + } + + public function testNotFoundRouteMatch(): void + { + $basePath = '/api'; + + $routeMatch = RouteMatch::notFound($basePath); + + $this->assertFalse($routeMatch->isFound()); + $this->assertTrue($routeMatch->isNotFound()); + $this->assertFalse($routeMatch->isMethodNotAllowed()); + + $this->assertNull($routeMatch->getRoute()); + $this->assertSame([], $routeMatch->getArguments()); + $this->assertSame([], $routeMatch->getAllowedMethods()); + } + + public function testMethodNotAllowedRouteMatch(): void + { + $allowedMethods = ['GET', 'POST']; + $basePath = '/api'; + + $routeMatch = RouteMatch::methodNotAllowed($allowedMethods, $basePath); + + $this->assertFalse($routeMatch->isFound()); + $this->assertFalse($routeMatch->isNotFound()); + $this->assertTrue($routeMatch->isMethodNotAllowed()); + + $this->assertNull($routeMatch->getRoute()); + $this->assertSame([], $routeMatch->getArguments()); + $this->assertSame($allowedMethods, $routeMatch->getAllowedMethods()); + } + + public function testGetArgumentWithDefaultValue(): void + { + $route = $this->createMock(RouteInterface::class); + + $routeMatch = RouteMatch::found($route, ['id' => 123]); + + $this->assertSame(123, $routeMatch->getArgument('id')); + $this->assertSame('default', $routeMatch->getArgument('missing', 'default')); + } + + public function testEmptyArguments(): void + { + $route = $this->createMock(RouteInterface::class); + + $routeMatch = RouteMatch::found($route); + + $this->assertSame([], $routeMatch->getArguments()); + } + + public function testStatusConsistency(): void + { + $route = $this->createMock(RouteInterface::class); + + $found = RouteMatch::found($route); + $notFound = RouteMatch::notFound(); + $methodNotAllowed = RouteMatch::methodNotAllowed(['GET']); + + $this->assertSame(DispatcherInterface::FOUND, $found->getStatus()); + $this->assertSame(DispatcherInterface::NOT_FOUND, $notFound->getStatus()); + $this->assertSame(DispatcherInterface::METHOD_NOT_ALLOWED, $methodNotAllowed->getStatus()); + } +} diff --git a/tests/Routing/RoutingResultsTest.php b/tests/Routing/RoutingResultsTest.php deleted file mode 100644 index b0147d9c0..000000000 --- a/tests/Routing/RoutingResultsTest.php +++ /dev/null @@ -1,105 +0,0 @@ - 'value1']; - $allowedMethods = ['GET', 'POST']; - - // Create RoutingResults instance - $routingResults = new RoutingResults( - $status, - $route, - $method, - $uri, - $routeArguments, - $allowedMethods, - ); - - $this->assertSame($status, $routingResults->getRouteStatus()); - $this->assertSame($route, $routingResults->getRoute()); - $this->assertSame($method, $routingResults->getMethod()); - $this->assertSame($uri, $routingResults->getUri()); - $this->assertSame('value1', $routingResults->getRouteArgument('arg1')); - $this->assertSame(null, $routingResults->getRouteArgument('nada')); - $this->assertSame($routeArguments, $routingResults->getRouteArguments()); - $this->assertSame($allowedMethods, $routingResults->getAllowedMethods()); - } - - public function testGettersWithNullRoute(): void - { - // Define test parameters with null route - $status = RoutingResults::NOT_FOUND; - $method = 'POST'; - $uri = '/not-found'; - $routeArguments = []; - $allowedMethods = ['GET']; - - // Create RoutingResults instance with null route - $routingResults = new RoutingResults( - $status, - null, - $method, - $uri, - $routeArguments, - $allowedMethods, - ); - - $this->assertSame($status, $routingResults->getRouteStatus()); - $this->assertNull($routingResults->getRoute()); - $this->assertSame($method, $routingResults->getMethod()); - $this->assertSame($uri, $routingResults->getUri()); - $this->assertSame($routeArguments, $routingResults->getRouteArguments()); - $this->assertSame($allowedMethods, $routingResults->getAllowedMethods()); - } - - public function testRoutingArgumentsFromRouteContext(): void - { - $app = AppFactory::create(); - - $app->addRoutingMiddleware(); - - // Define a route with arguments - $app->get('/test/{id}', function (ServerRequestInterface $request, ResponseInterface $response) { - $args = RouteContext::fromRequest($request)->getRoutingResults()->getRouteArguments(); - $response->getBody()->write('ID: ' . $args['id']); - - return $response; - }); - - $request = $app->getContainer() - ->get(ServerRequestFactoryInterface::class) - ->createServerRequest('GET', '/test/123'); - - $response = $app->handle($request); - - $this->assertSame(200, $response->getStatusCode()); - $this->assertSame('ID: 123', (string)$response->getBody()); - } -}