0

PSR-14 — Event Dispatcher interface

Intermediate5 min read·php-12-011
psrframework

Concept

PSR-15 defines two interfaces for HTTP server-side request handling: RequestHandlerInterface (handles a request and produces a response) and MiddlewareInterface (wraps a handler to add cross-cutting behavior).

RequestHandlerInterface: A single method handle(ServerRequestInterface $request): ResponseInterface. This is the core of any HTTP request pipeline — routes, controllers, or any object that turns a request into a response.

MiddlewareInterface: A single method process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface. Middleware can: inspect/modify the request before passing to $handler->handle($request), inspect/modify the response after $handler->handle(), short-circuit by returning a response without calling $handler, or call $handler->handle() multiple times (rare — retry logic).

Middleware pipeline: Middleware is typically stacked. Request enters the first middleware, propagates inward through the stack, the innermost handler produces a response, and the response propagates back outward through the stack in reverse order. Laravel calls this the "middleware stack" and it wraps every HTTP request.

PSR-14 (event dispatcher): EventDispatcherInterface with dispatch(object $event): object. Allows dispatching and listening to events in a PSR-standard way. Laravel's Event facade and Symfony's EventDispatcher both implement PSR-14.

Code Example

php
<?php
declare(strict_types=1);
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

// Authentication middleware
class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(private readonly TokenVerifier $verifier) {}

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler,
    ): ResponseInterface {
        $token = $request->getHeaderLine('Authorization');

        if (!$this->verifier->isValid($token)) {
            // Short-circuit — don't call $handler
            return new JsonResponse(['error' => 'Unauthorized'], 401);
        }

        // Attach user to request attributes, pass along
        $user = $this->verifier->getUser($token);
        $request = $request->withAttribute('user', $user);
        return $handler->handle($request); // call next in pipeline
    }
}

// Rate limiting middleware — uses response from inner handler
class RateLimitMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler,
    ): ResponseInterface {
        if ($this->isRateLimited($request)) {
            return new JsonResponse(['error' => 'Too Many Requests'], 429);
        }

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

        // Add headers to response on the way out
        return $response->withHeader('X-RateLimit-Remaining', (string) $this->getRemaining($request));
    }
}

// PSR-14 event dispatcher (Laravel implementation)
use Illuminate\Contracts\Events\Dispatcher;

class OrderService
{
    public function __construct(private readonly Dispatcher $events) {}

    public function place(Order $order): void
    {
        $this->saveOrder($order);
        // Dispatch PSR-14 compatible event
        $this->events->dispatch(new OrderPlaced($order));
    }
}

// Listener
class SendOrderConfirmation
{
    public function handle(OrderPlaced $event): void
    {
        Mail::to($event->order->customer_email)->send(new OrderConfirmationMail($event->order));
    }
}