PSR-15 w praktyce: kompletny przewodnik po Middleware w PHP | masterphp.eu

09.09.2025

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&ltRoute&lt
     */
    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:

  1. kiedy request jest odrzucany,
  2. co zostanie zapisane w logach,
  3. 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.

flowchart TD A[Request] --> B[CORS Middleware] B --> C[Auth Middleware] C --> D[Logging Middleware] D --> E[Router Handler] E --> F[Response] F --> D D --> C C --> B B --> G[Client Response]

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 i Response.
Sprawdź pełny kurs