Skip to content

PSR-15 Middleware

Overview

PSR-15 defines interfaces for HTTP server request handlers and middleware. XOOPS 2026 uses PSR-15 as the foundation for its request processing pipeline, enabling modular, testable, and reusable request handling components.

PSR-15 Interfaces

RequestHandlerInterface

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

interface RequestHandlerInterface
{
    /**
     * Handle the request and return a response.
     */
    public function handle(ServerRequestInterface $request): ResponseInterface;
}

MiddlewareInterface

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the
     * provided request handler to do so.
     */
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface;
}

XOOPS Middleware Pipeline

Pipeline Implementation

<?php

declare(strict_types=1);

namespace Xoops\Core\Http;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class MiddlewarePipeline implements RequestHandlerInterface
{
    /** @var MiddlewareInterface[] */
    private array $middleware = [];
    private int $index = 0;
    private RequestHandlerInterface $fallbackHandler;

    public function __construct(RequestHandlerInterface $fallbackHandler)
    {
        $this->fallbackHandler = $fallbackHandler;
    }

    /**
     * Add middleware to the pipeline
     */
    public function pipe(MiddlewareInterface $middleware): self
    {
        $this->middleware[] = $middleware;
        return $this;
    }

    /**
     * Handle the request through the middleware stack
     */
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        if (!isset($this->middleware[$this->index])) {
            return $this->fallbackHandler->handle($request);
        }

        $middleware = $this->middleware[$this->index];
        $this->index++;

        return $middleware->process($request, $this);
    }

    /**
     * Reset the pipeline for reuse
     */
    public function reset(): void
    {
        $this->index = 0;
    }
}

Kernel Implementation

<?php

declare(strict_types=1);

namespace Xoops\Core;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xoops\Core\Http\MiddlewarePipeline;
use Xoops\Core\Routing\Router;

class Kernel implements RequestHandlerInterface
{
    private MiddlewarePipeline $pipeline;

    public function __construct(
        private readonly Router $router,
        private readonly ContainerInterface $container
    ) {
        $this->pipeline = new MiddlewarePipeline($this);
        $this->configureMiddleware();
    }

    private function configureMiddleware(): void
    {
        // Core middleware stack
        $this->pipeline
            ->pipe($this->container->get(ErrorHandlerMiddleware::class))
            ->pipe($this->container->get(SecurityHeadersMiddleware::class))
            ->pipe($this->container->get(SessionMiddleware::class))
            ->pipe($this->container->get(AuthenticationMiddleware::class))
            ->pipe($this->container->get(CsrfMiddleware::class))
            ->pipe($this->container->get(RouterMiddleware::class));
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        return $this->pipeline->handle($request);
    }
}

Core Middleware Components

Error Handler Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Xoops\Core\Http\ApiResponse;

class ErrorHandlerMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly ApiResponse $response,
        private readonly bool $debug = false
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            return $handler->handle($request);
        } catch (\Throwable $e) {
            return $this->handleException($e, $request);
        }
    }

    private function handleException(
        \Throwable $e,
        ServerRequestInterface $request
    ): ResponseInterface {
        $this->logger->error($e->getMessage(), [
            'exception' => $e,
            'uri' => (string) $request->getUri(),
            'method' => $request->getMethod(),
        ]);

        $status = $this->getStatusCode($e);
        $message = $this->debug
            ? $e->getMessage()
            : $this->getPublicMessage($status);

        if ($this->wantsJson($request)) {
            return $this->response->json([
                'error' => true,
                'message' => $message,
                'code' => $status,
            ], $status);
        }

        return $this->response->html(
            $this->renderErrorPage($status, $message),
            $status
        );
    }

    private function getStatusCode(\Throwable $e): int
    {
        if ($e instanceof HttpExceptionInterface) {
            return $e->getStatusCode();
        }

        return 500;
    }

    private function getPublicMessage(int $status): string
    {
        return match ($status) {
            400 => 'Bad Request',
            401 => 'Unauthorized',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            500 => 'Internal Server Error',
            default => 'An error occurred',
        };
    }

    private function wantsJson(ServerRequestInterface $request): bool
    {
        $accept = $request->getHeaderLine('Accept');
        return str_contains($accept, 'application/json');
    }
}

CSRF Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xoops\Core\Security\CsrfTokenManager;

class CsrfMiddleware implements MiddlewareInterface
{
    private const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'];
    private const TOKEN_FIELD = '_csrf_token';
    private const TOKEN_HEADER = 'X-CSRF-Token';

    public function __construct(
        private readonly CsrfTokenManager $tokenManager
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Skip CSRF check for safe methods
        if (in_array($request->getMethod(), self::SAFE_METHODS, true)) {
            return $handler->handle($request);
        }

        // Get token from request
        $token = $this->getTokenFromRequest($request);

        // Validate token
        if (!$this->tokenManager->isValid($token)) {
            throw new CsrfValidationException('Invalid CSRF token');
        }

        return $handler->handle($request);
    }

    private function getTokenFromRequest(ServerRequestInterface $request): string
    {
        // Check header first
        $headerToken = $request->getHeaderLine(self::TOKEN_HEADER);
        if ($headerToken !== '') {
            return $headerToken;
        }

        // Check body
        $body = $request->getParsedBody();
        if (is_array($body) && isset($body[self::TOKEN_FIELD])) {
            return (string) $body[self::TOKEN_FIELD];
        }

        return '';
    }
}

Session Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xoops\Core\Session\SessionManager;

class SessionMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly SessionManager $sessionManager
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Start session
        $session = $this->sessionManager->start($request);

        // Add session to request attributes
        $request = $request->withAttribute('session', $session);

        // Handle request
        $response = $handler->handle($request);

        // Save session and add cookie to response
        return $this->sessionManager->persist($session, $response);
    }
}

Authentication Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xoops\Core\Auth\AuthenticationService;
use Xoops\Core\Auth\UserInterface;

class AuthenticationMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly AuthenticationService $auth
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Get session from previous middleware
        $session = $request->getAttribute('session');

        // Authenticate user from session
        $user = $this->auth->authenticateFromSession($session);

        // Add user to request (null if not authenticated)
        $request = $request
            ->withAttribute('user', $user)
            ->withAttribute('isAuthenticated', $user !== null)
            ->withAttribute('isAdmin', $user?->isAdmin() ?? false);

        return $handler->handle($request);
    }
}

Rate Limiting Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xoops\Core\RateLimiter\RateLimiterInterface;

class RateLimitMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly RateLimiterInterface $rateLimiter,
        private readonly int $maxRequests = 60,
        private readonly int $windowSeconds = 60
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $identifier = $this->getIdentifier($request);
        $result = $this->rateLimiter->attempt($identifier, $this->maxRequests, $this->windowSeconds);

        if (!$result->allowed) {
            throw new TooManyRequestsException(
                'Rate limit exceeded',
                $result->retryAfter
            );
        }

        $response = $handler->handle($request);

        // Add rate limit headers
        return $response
            ->withHeader('X-RateLimit-Limit', (string) $this->maxRequests)
            ->withHeader('X-RateLimit-Remaining', (string) $result->remaining)
            ->withHeader('X-RateLimit-Reset', (string) $result->resetAt);
    }

    private function getIdentifier(ServerRequestInterface $request): string
    {
        $user = $request->getAttribute('user');

        if ($user !== null) {
            return 'user:' . $user->getId();
        }

        $ip = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
        return 'ip:' . $ip;
    }
}

Router Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xoops\Core\Routing\Router;
use Xoops\Core\Routing\RouteMatchInterface;

class RouterMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly Router $router,
        private readonly ContainerInterface $container
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Match route
        $match = $this->router->match($request);

        if ($match === null) {
            throw new NotFoundException('Route not found');
        }

        // Add route parameters to request attributes
        foreach ($match->getParams() as $name => $value) {
            $request = $request->withAttribute($name, $value);
        }

        // Add route info
        $request = $request
            ->withAttribute('_route', $match->getName())
            ->withAttribute('_module', $match->getModuleSlug());

        // Resolve and execute controller
        return $this->executeHandler($match, $request);
    }

    private function executeHandler(
        RouteMatchInterface $match,
        ServerRequestInterface $request
    ): ResponseInterface {
        [$class, $method] = explode('::', $match->getHandler());

        $controller = $this->container->get($class);

        // Build method arguments from route params
        $args = [$request];
        foreach ($match->getParams() as $name => $value) {
            $args[$name] = $value;
        }

        return $controller->$method(...$args);
    }
}

Module-Specific Middleware

Permission Middleware

<?php

declare(strict_types=1);

namespace Xoops\Module\Publisher\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class PublisherPermissionMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly string $permission
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $user = $request->getAttribute('user');

        if ($user === null) {
            throw new UnauthorizedException('Authentication required');
        }

        if (!$user->hasPermission('publisher', $this->permission)) {
            throw new ForbiddenException('Permission denied');
        }

        return $handler->handle($request);
    }
}

Module Initialization Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xoops\Core\Module\ModuleManager;

class ModuleMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly ModuleManager $moduleManager
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $moduleSlug = $request->getAttribute('_module');

        if ($moduleSlug === null) {
            return $handler->handle($request);
        }

        // Load and initialize module
        $module = $this->moduleManager->load($moduleSlug);

        if ($module === null || !$module->isActive()) {
            throw new NotFoundException("Module '$moduleSlug' not found");
        }

        // Add module to request
        $request = $request->withAttribute('module', $module);

        // Execute module-specific middleware
        $middlewareStack = $module->getMiddleware();

        if (empty($middlewareStack)) {
            return $handler->handle($request);
        }

        // Build module middleware pipeline
        $pipeline = new MiddlewarePipeline($handler);
        foreach ($middlewareStack as $middleware) {
            $pipeline->pipe($middleware);
        }

        return $pipeline->handle($request);
    }
}

Route-Level Middleware

Defining Middleware in Routes

{
    "routes": {
        "article.create": {
            "path": "/articles",
            "method": ["POST"],
            "action": "Controller\\ArticleController::create",
            "middleware": ["auth", "csrf", "publisher.can_create"]
        },
        "article.delete": {
            "path": "/articles/{id:\\d+}",
            "method": ["DELETE"],
            "action": "Controller\\ArticleController::delete",
            "middleware": ["auth", "csrf", "publisher.can_delete"]
        }
    }
}

Middleware Aliases

// Container registration
$container->set('middleware.auth', fn($c) =>
    new AuthRequiredMiddleware()
);

$container->set('middleware.csrf', fn($c) =>
    $c->get(CsrfMiddleware::class)
);

$container->set('middleware.publisher.can_create', fn($c) =>
    new PublisherPermissionMiddleware('create_article')
);

$container->set('middleware.publisher.can_delete', fn($c) =>
    new PublisherPermissionMiddleware('delete_article')
);

Creating Custom Middleware

Template

<?php

declare(strict_types=1);

namespace Xoops\Module\MyModule\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class MyCustomMiddleware implements MiddlewareInterface
{
    public function __construct(
        // Inject dependencies
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // 1. Pre-processing (before handler)
        $request = $this->beforeHandler($request);

        // 2. Call the next handler
        $response = $handler->handle($request);

        // 3. Post-processing (after handler)
        $response = $this->afterHandler($response);

        return $response;
    }

    private function beforeHandler(ServerRequestInterface $request): ServerRequestInterface
    {
        // Modify request, add attributes, validate, etc.
        return $request;
    }

    private function afterHandler(ResponseInterface $response): ResponseInterface
    {
        // Modify response, add headers, etc.
        return $response;
    }
}

Conditional Middleware

<?php

declare(strict_types=1);

namespace Xoops\Core\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ConditionalMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly MiddlewareInterface $middleware,
        private readonly callable $condition
    ) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        if (($this->condition)($request)) {
            return $this->middleware->process($request, $handler);
        }

        return $handler->handle($request);
    }
}

// Usage
$middleware = new ConditionalMiddleware(
    new RateLimitMiddleware($limiter),
    fn($request) => $request->getAttribute('user') === null // Only for guests
);

Testing Middleware

<?php

use PHPUnit\Framework\TestCase;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Server\RequestHandlerInterface;

class CsrfMiddlewareTest extends TestCase
{
    private CsrfMiddleware $middleware;
    private CsrfTokenManager $tokenManager;
    private Psr17Factory $factory;

    protected function setUp(): void
    {
        $this->tokenManager = new CsrfTokenManager();
        $this->middleware = new CsrfMiddleware($this->tokenManager);
        $this->factory = new Psr17Factory();
    }

    public function testSkipsGetRequests(): void
    {
        $request = $this->factory->createServerRequest('GET', '/articles');

        $handler = $this->createMock(RequestHandlerInterface::class);
        $handler->expects($this->once())
            ->method('handle')
            ->willReturn($this->factory->createResponse(200));

        $response = $this->middleware->process($request, $handler);

        $this->assertEquals(200, $response->getStatusCode());
    }

    public function testValidatesPostRequests(): void
    {
        $token = $this->tokenManager->generate();

        $request = $this->factory->createServerRequest('POST', '/articles')
            ->withParsedBody(['_csrf_token' => $token]);

        $handler = $this->createMock(RequestHandlerInterface::class);
        $handler->expects($this->once())
            ->method('handle')
            ->willReturn($this->factory->createResponse(201));

        $response = $this->middleware->process($request, $handler);

        $this->assertEquals(201, $response->getStatusCode());
    }

    public function testRejectsInvalidToken(): void
    {
        $request = $this->factory->createServerRequest('POST', '/articles')
            ->withParsedBody(['_csrf_token' => 'invalid']);

        $handler = $this->createMock(RequestHandlerInterface::class);
        $handler->expects($this->never())->method('handle');

        $this->expectException(CsrfValidationException::class);

        $this->middleware->process($request, $handler);
    }
}

See Also

External Resources


xoops2026 #psr-15 #middleware #request-handling #pipeline