PSR-15 w praktyce: kompletny przewodnik po Middleware w PHP
W tym artykule przyjrzymy się szczegółowo:
- Komponentom niezbędnym do uruchomienia middleware, czyli krótkie słowo wstępu
- czym jest middleware i jaką pełni rolę,
- interfejsom PSR-15 i ich praktycznym implementacjom,
- tworzeniu własnych komponentów krok po kroku,
- zarządzaniu kolejnością i zaawansowanym scenariuszom,
- najlepszym praktykom i błędom, których warto unikać.
Słowo wstępu, czyli czego potrzebujemy do uruchomienia middleware
Midleware jako jeden ze standardów PSR do uruchomienia potrzebuje pozostałych komponentów:
- Jakiegoś handlera który przetworzy nam request z PSR-7 na response - u nas będzie to bardzo prosta klasa RouteHandler
- Kernela będącego Handlerem middleware zgodnym z PSR-15
- Aplikacji będącej kontenerem DI zgodnym z PSR-3
- Klasy Route odpowiadającej za pojedynczą trasę
- Klasy Route collection odpowiadającej za zestaw tras
- Klasy Router wykonującej zmianę requestu na response
- Klasy MiddlewareStack wywołującej middleware w określonej kolejności
Uproszczone definicje tych klas pokazano poniżej
<?php
class Response implements ResponseInterface
{
//szczegółowa implementacja została opisana w rozdziałach 2 i 12 kursu
}
readonly class RouteHandler
{
final public function __construct(private Closure $callback)
{
}
public function dispatch(ServerRequestInterface $request): ResponseInterface
{
return ($this->callback)($request);
}
}
class Route
{
public private(set) array $middlewareBefore = [];
//analogiczne tablice dla middleware after i globalnych
private readonly string $method;
public function __construct(
public string $path,
string $method,
public readonly RouteHandler $handler,
public readonly ?string $name = null
)
{
$this->method = strtoupper($method);
}
//na potrzeby middleware reszta klasy nie ma większgo znaczenia
}
class RouteCollection implements \IteratorAggregate, \Countable
{
/**
* @var list<Route<
*/
private array $routes = [];
public array $middlewareBefore {
get {
return array_values(
array_unique(
array_merge(
...array_map(
static fn(Route $route): array => $route->middlewareBefore,
$this->routes
)
)
)
);
}
}
public function findRoute(RequestInterface $request): Route
{
$matcher = new AdvancedRouteMatcher();
$matchingRoutes = array_filter(
$this->routes,
static fn (Route $route) => $matcher->matches($request, $route)
);
return array_values($matchingRoutes)[0] ?? throw new RouteNotFoundError(
'No route found for ' . $request->getMethod(
) . ' ' . $request->getUri()->getPath()
);
}
//analogiczne tablice dla pozostałych typów middleware
public function addRoute(Route $route): void
{
$this->routes[] = $route;
if ($route->name) {
$this->namedRoutes[$route->name] = $route;
}
}
}
class Router
{
public function __construct(
public private(set) RouteCollection $routes = new RouteCollection(),
) {
}
public function addRoute(
Route $route
): self {
$this->routes->addRoute($route);
return $this;
}
public function dispatch(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
{
$route = $this->routes->findRoute($request);
$handler = $route->handler;
$response = $handler->dispatch($request);
return $next->handle($request->withAttribute('route_response', $response));
}
}
Czym właściwie jest middleware?
Middleware działa jak filtr: przechwytuje żądanie zanim trafi do głównej logiki aplikacji i może je przetworzyć, odrzucić albo wzbogacić. Wyobraź sobie je jako warstwy cebuli — każde middleware otacza kolejne, a żądanie przechodzi przez nie w ustalonej kolejności.
Warto pamiętać, że middleware jest w gruncie rzeczy dowolną klasą,
która przekształca obiekt żądania w odpowiedź. Standard PSR-15 definiuje ujednolicony
interfejs i cykl życia middleware, ale jego użycie nie jest obowiązkowe. Dobrym przykładem
jest middleware w Laravelu, które przyjmuje po prostu dowolny callable
jako handler. Dzieje się tak dlatego, że request w Laravelu nie musi być zgodny z PSR-7.
Dodatkowo od wersji 11 framework wprowadził Middleware helper
, który obsługuje
trzy tablice: prepend
, append
i replace
— co jest
bezpośrednim odpowiednikiem systemu pokazanego w tym artykule poniżej.
Przykład middleware w Laravelu
<?php
class CheckAuth
{
final public function handle(\Illuminate\Http\Request $request, Closure $next): \Illuminate\Http\RedirectResponse
{
/** @var ?\App\Models\User $user */
$user = $request->user();
if (!$user?->can('posts.write') ) {
return to_route(route: 'login');
}
return $next($request);
}
}
Równie ciekawym przykładem jest Symfony. Ten framework natywnie
wspiera standard PSR-15 – możesz więc pisać middleware w pełni zgodne z interfejsem
MiddlewareInterface
. Jednocześnie sam Symfony od dawna stosuje własne
podejście oparte na event listeners i event subscribers, które
reagują na zdarzenia takie jak kernel.request
czy kernel.response
.
Dzięki temu możesz wybrać pomiędzy stylem „eventowym” a czystym PSR-15, w zależności
od potrzeb projektu.
<?php
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;
final class CheckAuthListener
{
public function __construct(private RouterInterface $router) {}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$user = $request->getUser(); // w praktyce np. z Security component
if (! $user || ! $user->can('posts.write')) {
$loginUrl = $this->router->generate('login');
$event->setResponse(new RedirectResponse($loginUrl));
}
}
}
Przykłady zastosowań:
- uwierzytelnianie i autoryzacja,
- logowanie i monitoring,
- obsługa CORS,
- cache,
- modyfikacja nagłówków odpowiedzi.
Podstawowe interfejsy PSR-15
PSR-15 definiuje dwa główne interfejsy:
MiddlewareInterface
– opisuje pojedynczy komponent middleware,RequestHandlerInterface
– pozwala przekazać żądanie dalej w łańcuchu.
Budowa własnego middleware — krok po kroku
Zbudujmy proste middleware, obsługuje request poprzez router i przechwytuje wyjątek
<?php
final class RouterMiddleware implements MiddlewareInterface
{
public function __construct(private Router $router) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
try {
return $this->router->dispatch($request, $handler);
} catch (\Throwable $exception) {
$request = $request->withAttribute('exception', $exception);
return $handler->handle($request);
}
}
}
Takie podejście pozwala łatwo „opakować” dowolną logikę diagnostyczną bez ingerencji w główny kod aplikacji.
Globalne i warunkowe middleware
Middleware można stosować globalnie (dla całej aplikacji) lub przypisywać tylko do wybranych tras. Dzięki temu możesz np. globalnie logować czas odpowiedzi, a tylko na określonych trasach sprawdzać autoryzację.
Kolejność ma znaczenie
Pokazany poniżej idealnie pokazuje, czemu kolejność middleware ma znaczenie — bo każdy z nich działa jak warstwa cebuli .
- Jeśli CORS pójdzie na sam dół, to klient dostanie odmowę jeszcze zanim zdąży przejść przez autoryzację czy logowanie.
- Jeśli Auth Middleware pójdzie przed Logging Middleware, to wszystkie odrzucone requesty zostaną zapisane — co może być przydatne. A jak odwrócisz kolejność, to część prób w ogóle nie trafi do logów.
- Jeśli Router Handler byłby na początku, to całe bezpieczeństwo i kontrola dostępu zostałyby ominięte.
Kolejność więc decyduje:
- kiedy request jest odrzucany,
- co zostanie zapisane w logach,
- czy odpowiedź dojdzie do klienta zgodnie z wymaganiami (np. z nagłówkami CORS).
Można to podsumować tak: middleware to nie tylko zestaw filtrów, ale ścieżka, w której każdy element ma swoje miejsce w hierarchii.
Poniżej implementacja klasy MiddlewareStack odpowiedzialnej za kolejność middleware
<?php
class MiddlewareStack implements RequestHandlerInterface
{
private array $middleware = [];
private int $currentIndex = 0;
public function __construct(
private readonly Router $router,
private Response $originalResponse = new Response()
)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
//sprawdzamy czy middleware routera złapał wyjątek - PRZERYWAMY!!
$exception = $request->getAttribute('exception');
if($exception !== null) {
throw $exception;
}
//pobieramy aktualne middlewar
$middleware = $this->middleware[$this->currentIndex] ?? null;
//jeśli router wykonał główny kod aplikacji i nie ma więcej middleware
if ($this->currentIndex === count($this->middleware) && $this->routerExecuted) {
return $request->withAttribute('route_response', $this->originalResponse);
}
//oznaczamy że router sie wykonał
if ($middleware instanceof RouterMiddleware)
{
$this->routerExecuted = true;
}
$this->currentIndex++;
//przetwarzamy kolejne middleware, ponieważ wywołujemy process na middleware, rekurencyjnie wywołamy ten handler
return $middleware?->process($request, $this) ?? $this->originalResponse;
}
}
Dobre praktyki i najczęstsze błędy
- Nie mieszaj logiki biznesowej z middleware – trzymaj się zasady Single Responsibility.
- Zwracaj Response w każdej gałęzi – brak odpowiedzi = błąd.
- Testuj middleware niezależnie – każdy komponent powinien być łatwy do przetestowania jednostkowo.
- Dokumentuj kolejność – w większych aplikacjach łatwo o chaos.
Jak testować middleware?
Kluczowym elementem działania middleware jest kolejność ich wywołań. Możemy napisać prosty test, który sprawdzi czy middleware faktycznie działa w oczekiwanym porządku.
<?php
class MiddlewareTest extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
//mockujemy middleware
$this->middlewareClass = new class implements MiddlewareInterface {
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
$response = $handler->handle($request);
return $response->withHeader('X-Middleware-Test', 'true');
}
};
//mockujemy konfiguracje aplikacji
$config = $this->createMock(ConfigContract::class);
$config->expects($this->any())
->method('get')
->with('middleware')
->willReturn([
'before_global' => [],
'global' => [],
'after_global' => [],
]);
$app->set(ConfigContract::class, $config);
}
public function testMiddlewareBefore(): void
{
$app = Application::getInstance();
$response = new Response();
$handler = new RouteHandler(callback: fn() => $response);
$app->withRoutes(function (Router $router) use ($handler) {
$router->addRoute(
new Route(
'/',
'GET',
$handler
)->withMiddlewareBefore($this->middlewareClass::class)
);
});
$response = $app->handle();
$before = Config::get('middleware.before_global');
//weryfikacja że middleware sie dodała do tablicy
$this->assertEquals($before, $this->middlewareClass::class);
//weryfikacja że middleware sie wykonało
$this->assertEquals('true', $response->getHeaderLine('X-Middleware-Test'));
}
}
1. Testy integracyjne
Middleware, są kompomentem który bardzo mocno ingeruje w całość aplikacji należy je testować w testach integracyjnych. W przykładowym teście utworzym bardzo prosty handler który po prostu zwraca odpowiedź, a następnie sprawdzimy czy po wykonaniu middleware odpowiedź jest zgodna z PSR-7
<?php
class RouterMiddlewareTest extends BaseTestCase
{
public function setUp(): void
{
$app = Application::getInstance();
$config = $this->createMock(ConfigContract::class);
$config->expects($this->any())
->method('get')
->with('middleware')
->willReturn([
'before_global' => [],
'global' => [
RouterMiddleware::class
],
'after_global' => [],
]);
$app->set(ConfigContract::class, $config);
}
public function testMiddleware()
{
$app = Application::getInstance();
$response = new Response()->withContent('hello from tests');
$handler = new RouteHandler(callback: fn() => $response);
$app->withRoutes(function (Router $router) use ($handler) {
$router->addRoute(
new Route(
'/',
'GET',
$handler
)
);
});
$response = $app->handle();
$this->assertInstanceOf(Response::class, $response);
$this->assertEquals('hello from tests', $response->getBody()->getContents());
}
}
Podsumowanie
PSR-15 pozwala budować aplikacje modularne, łatwe w utrzymaniu i kompatybilne z całym ekosystemem PHP-FIG. To standard, który warto dobrze poznać i stosować w każdym projekcie webowym.
Pamiętaj: testuj kolejność, loguj wyjątki, oddziel logikę biznesową, dokumentuj middleware
👉 To tylko fragment pełnego kursu MasterPHP!
W kursie znajdziesz kompletną implementację PSR-15 wraz z przykładami:
- tworzenie globalnego middleware,
- dynamiczne włączanie i wyłączanie komponentów,
- zaawansowane zarządzanie kolejnością,
- modyfikacje obiektów
Request
iResponse
.