Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Slim/Container/Definition/SlimDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
},
Expand Down
17 changes: 17 additions & 0 deletions Slim/Interfaces/DispatcherInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Slim\Interfaces;

interface DispatcherInterface
{
const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;

/**
* @return array<int, mixed>
*/
public function dispatch(string $httpMethod, string $uri): array;
}
83 changes: 83 additions & 0 deletions Slim/Interfaces/RouteInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/

namespace Slim\Interfaces;

use Psr\Http\Server\MiddlewareInterface;
use Slim\Routing\RouteGroup;

interface RouteInterface
{
/**
* Get route callable.
*
* @return callable|string
*/
public function getHandler(): callable|string;

/**
* Set route name.
*
* @param string $name The route name
*
* @return RouteInterface
*/
public function setName(string $name): RouteInterface;

/**
* Get route name.
*/
public function getName(): ?string;

/**
* Get route pattern.
*
* @return string
*/
public function getPattern(): string;

/**
* Get route HTTP methods
*
* @return array<string>
*/
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<string, string>
*/
public function getArguments(): array;

/**
* Set route arguments.
*
* @param array<string,mixed> $arguments The arguments.
*
* @return RouteInterface
*/
public function setArguments(array $arguments): RouteInterface;
Comment on lines +58 to +77
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

/**
* @return array<MiddlewareInterface|callable|string>
*/
public function getMiddleware(): array;
}
3 changes: 1 addition & 2 deletions Slim/Middleware/BasePathMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);

Comment on lines +48 to 49
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// $request = $request->withAttribute(RouteMatch::BASE_PATH_ATTRIBUTE, $basePath);

Copilot uses AI. Check for mistakes.
$this->router->setBasePath($basePath);

Expand Down
99 changes: 60 additions & 39 deletions Slim/Middleware/EndpointMiddleware.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<?php

/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim/blob/5.x/LICENSE.md (MIT License)
*/

namespace Slim\Middleware;

use Psr\Http\Message\ResponseInterface;
Expand All @@ -9,17 +15,17 @@
use RuntimeException;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
use Slim\Interfaces\RouteInterface;
use Slim\Routing\PipelineRunner;
use Slim\Routing\Route;
use Slim\Routing\RouteContext;
use Slim\Routing\RouteInvoker;
use Slim\Routing\RoutingResults;
use Slim\Routing\RouteMatch;

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

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/* @var RoutingResults $routingResults */
$routingResults = $request->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<string, mixed> $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<MiddlewareInterface|callable|string> List of middleware
* Collects middleware in execution order:
* - outermost parent group middleware first
* - nested group middleware next
* - route-specific middleware last
*
* @return array<MiddlewareInterface|callable|string>
*/
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;
}
}
11 changes: 5 additions & 6 deletions Slim/Middleware/RoutingArgumentsMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
}
}
Expand Down
Loading