SoFloPHPIndependent reference

How PHP Routing and Middleware Work in a Request Pipeline

How PHP routing and middleware use PSR-7 messages and PSR-15 handlers to build a request pipeline, with framework-neutral concepts first.

How PHP Routing and Middleware Work in a Request Pipeline

A PHP routing middleware request pipeline is a front controller that receives the request, passes it through an ordered stack of middleware, and hands it to a router that dispatches it to the code that builds the reply. If you skip this pattern, you end up with individual PHP files that repeat authentication checks, session starts and header writes in a tangle no test suite can reach. With it you get one central place to attach logging, CORS handling, rate limiting and content negotiation in a sequence that is explicit, debuggable and replaceable.

The Front Controller and the Request-Reply Lifecycle

Every request hits a single PHP file, usually public/index.php. This is the front controller pattern PHP projects adopted once Apache mod_rewrite and Nginx try_files made clean URL rewriting reliable. The front controller bootstraps the application, builds a request object from the server globals, and hands it to a pipeline. No other PHP file is directly reachable from the web.

In a standard setup the entry point does three things: it loads Composer's autoloader, wires the container or service locator, and invokes the pipeline. That is it. The front controller owns the boundary between the web server and your code. When timeout or buffer problems surface, this is the boundary to inspect.

PHP Routing Middleware Request Pipeline

How the Pipeline Queue Works

The pipeline is a queue. Each middleware is a callable that receives the request and a delegate. The middleware can modify the request, return a reply early, or call the delegate to hand off to the next layer. The last delegate in the chain is the router, which matches the request to a handler and sends back a reply.

This model is defined by PSR-15 middleware handler interfaces. A middleware implements MiddlewareInterface with a single process method. A request handler implements RequestHandlerInterface with a handle method. The distinction matters: middleware can delegate; a request handler must produce a reply. Mixing them causes pipeline ordering bugs that are hard to trace.

Get the Order Right

Pipeline ordering is the operation that trips most teams. Logging middleware that wraps the entire call stack must sit at the top. Authentication middleware that guards routes must sit before the router. Content negotiation that reads reply headers must sit after the router. Put them in the wrong order and you log unauthenticated pings or authenticate dead ends.

PSR-7 HTTP Message Interface Defines the Message, Not the Behaviour

PSR-7 HTTP message interface is a set of PHP interfaces that describe HTTP messages. RequestInterface and ResponseInterface are value objects. They are immutable. You cannot change a header on an existing request; you create a new instance with the header set. This design prevents one middleware from corrupting state for everything downstream. It also means every header or body modification allocates a new object.

ServerRequestInterface extends RequestInterface with server-specific data: query parameters, parsed body, uploaded files, and attributes for middleware-to-handler communication. The URI interface normalises scheme, host, port, path and query string into a single object. The stream interface wraps the message body as a resource you can read and write without loading the entire payload into memory.

Confusion between PSR-7 and PSR-15 is common because both deal with HTTP. PSR-7 defines the message objects. PSR-15 defines the handler that processes them. A request is a PSR-7 ServerRequestInterface. A middleware is a PSR-15 MiddlewareInterface that acts on that request. Knowing which PSR governs which part of the stack lets you swap implementations without breaking the pipeline.

PSR-15 Middleware Handler and the Delegation Contract

PSR-15 splits responsibility between middleware and the request handler. Middleware receives the request and a RequestHandlerInterface. If the middleware can satisfy the request itself, for example by blocking a missing token, it sends back a reply immediately. Otherwise it calls $handler->handle($request) and passes along the result that comes back.

The request handler at the end of the chain is the application core. In a micro-framework this is the router. In a full-stack framework it is a kernel that boots providers and resolves a controller. The delegation chain means each middleware knows only about the next handler, not the whole stack. Add, remove or reorder middleware without touching the others.

Response factory integration is where PSR-7 and PSR-15 meet in practice. Middleware that needs to send a reply without delegating must create a ResponseInterface. A response factory is a callable or factory class that builds an empty reply with a status code and body. Slim injects a PSR-17 response factory into its middleware runner so every middleware can produce a valid reply.

How the Middleware Stack Assembles in Different PHP Frameworks

Slim: the Reference Implementation

The middleware stack concept is universal, but framework implementations differ in wiring and naming. Slim micro-framework builds the stack explicitly. Add middleware to the application object in order: error handling first, then CORS, then routing. Slim's middleware signature matches PSR-15 directly, and its router is a request handler that dispatches to route callables. This makes Slim the clearest reference implementation for learning the pipeline.

Laravel: Closure-Based Pipelines

Laravel uses a different term: the HTTP kernel. As of Laravel 11, observed October 2024, the kernel's middleware stack is configured in bootstrap/app.php through global and group middleware arrays. Laravel middleware receives the request and a $next closure, not a PSR-15 RequestHandlerInterface. The closure-based delegation predates PSR-15 and is functionally equivalent, but it is not directly interoperable with PSR-15 middleware from other packages.

Symfony: Event-Driven Dispatching

Symfony HttpKernel uses an event-driven model rather than a linear middleware stack. The kernel dispatches a kernel.request event, calls the controller, then dispatches kernel.response and kernel.exception events. Listeners on these events do the work that middleware does in PSR-15: authentication, caching, content negotiation. The event dispatcher gives more hooks but makes the execution order harder to trace from a single config file.

The PHP Request-Reply Lifecycle From Entry Point to Emitter

The lifecycle begins when the web server hands a request to the PHP SAPI. The front controller creates a ServerRequestInterface from $_SERVER, $_GET, $_POST, $_COOKIE and php://input. In a PSR-7 project use a factory like Nyholm/psr7 or Laminas/Diactoros.

The pipeline runs each middleware in order. When the router matches a route, it dispatches to a handler that produces a ResponseInterface. The reply bubbles back up through any middleware that wrapped the delegation call. The last middleware hands the final reply to the front controller. The front controller passes it to an emitter that sends status line, headers and body to the output buffer.

The emitter is the piece most developers overlook. It writes headers with header() and the body with echo or fpassthru. If a middleware or handler has already sent output, the emitter cannot set headers and the application fails with a "headers already sent" warning. Run output buffering in every PSR-7 project and make the emitter the last thing the front controller calls.

Pipeline Ordering, Delegation and the Mistakes That Break Production

Ordering Rules That Prevent Outages

Pipeline ordering is not a one-time setup decision. Every new middleware shifts the balance. Error handling middleware that catches exceptions must wrap everything else. It is always first. Session middleware that reads and writes the session cookie must run after CORS middleware so the browser does not reject the cookie on cross-origin requests. Body parsing middleware that populates $request->getParsedBody() must run before the router, or route handlers will see empty POST data.

Delegation is the mechanism that makes ordering work. A middleware that calls $handler->handle($request) is delegating. A middleware that sends a reply without calling the handler is short-circuiting. Both are valid. Problems arise when a middleware short-circuits unintentionally, for example by catching an exception and sending an error reply before the logging middleware has recorded it. Put logging first so it wraps the entire delegation chain.

Testing the Full Stack

Testing middleware in isolation is straightforward: pass a mock request and a mock handler, assert the reply. Testing the pipeline as a unit is harder because a change in ordering can produce a different outcome for the same request. Integration tests that boot the full front controller with a known route and assert status codes, headers and body content are the only reliable safety net.

When the Pipeline Model Is the Wrong Choice

A PSR-15 pipeline is overkill for a single-file script that returns a JSON health check. It adds three to five milliseconds of overhead on a cold PHP process, which matters if you are running on a shared host without opcache. For a microservice with one endpoint, a plain PHP file that reads the request and writes a reply is faster and simpler.

The pipeline model also breaks down when middleware needs to communicate with the handler beyond request attributes. Attributes are string-keyed and can carry any value, but they are not type-safe and not discoverable. If your authentication middleware sets a User object in an attribute, every downstream handler must know the attribute key and the expected type. A misnamed key fails silently at runtime.

The biggest caveat: framework-specific abstractions leak through the PSR interfaces. A middleware written for Laravel's closure-based pipeline will not run in Slim's PSR-15 runner without an adapter. A reply built with Laravel's Response class implements PSR-7 ResponseInterface only if the framework version supports it. Check your framework's PSR-7 bridge before you commit to a cross-framework middleware library.

Common Questions

Is PSR-15 required for PHP routing middleware?

No. Laravel's pipeline uses closures, not PSR-15 interfaces, and works without them. PSR-15 is useful when you want middleware portability between frameworks like Slim and Mezzio, or when you are building a library that other projects will consume.

Can I use PSR-7 middleware in a Laravel project?

Yes, with a PSR-15 adapter that wraps the PSR-15 middleware as a Laravel pipeline callable. As of Laravel 11, October 2024, you need a bridge package or a custom closure that converts the Laravel request to a PSR-7 ServerRequestInterface and the reply back.

What happens if a middleware does not call the next handler?

The pipeline stops. No further middleware runs and the router never receives the request. This is intentional for authentication middleware that blocks an invalid token, but it is a bug if a logging middleware accidentally catches an exception and sends a reply without delegating.

How do I test a PSR-15 middleware pipeline?

Instantiate the middleware stack with a known request and a final handler that produces a predictable reply. Call handle() on the outermost middleware and assert the outcome. For integration tests, boot the application's front controller and make real HTTP requests to a test server.

Can I run middleware after the reply is built?

Not in the standard PSR-15 model. Middleware that wants to modify the reply must wrap the $handler->handle() call, inspect the returned reply, and hand back a modified copy. An alternative pattern is an event listener on a kernel.response event, as Symfony HttpKernel does.