Middleware
By the end of this lesson you'll understand the request/response pipeline behind every modern PHP framework — the "onion" model, the PSR-7 and PSR-15 standards, and how to build and chain your own auth, logging, and CORS middleware.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ The Onion: Code Before and After
Every web app does the same core job: take an HTTP request , run your code, return an HTTP response . Middleware is a layer that wraps that code so it can run before the request reaches your controller and after the response comes back. Picture an onion: the request travels inward through each layer to the centre (your controller), then the response travels back outward through the same layers in reverse. The key piece is $next — a callback that means "hand control to the next layer in". Code before $next runs on the way in; code after it runs on the way out.
Look at the output order: [in ] prints first, then control flows into the controller, then [out] prints. The same closure runs code on both sides of the single $next($request) call. That one idea scales to a whole stack.
2️⃣ PSR-7, PSR-15 & Chaining the Stack
Real PHP doesn't pass arrays around — it uses PSR-7 , a shared standard for HTTP message objects ( ServerRequestInterface and ResponseInterface ), so every library agrees on what a request and response look like. On top of that, PSR-15 defines the middleware contract: a MiddlewareInterface with one method, process($request, $handler) , that returns a response. Because the contract is standardised, middleware from any package can share one pipeline. Below, the request and response are kept as plain arrays so the focus stays on the pipeline — the part that chains many layers and dispatches a request through them. Notice how pipe() returns $this so calls chain, and how process() composes the stack inside-out with array_reverse so the first middleware you pipe ends up outermost.
Trace the output. With a token: Logging runs (outermost), then Auth passes you through, then the controller answers, then control unwinds back out through Auth and Logging. Without a token, Auth returns a 401 and never calls $next — the controller never runs. That's a short-circuit , and it's how you cheaply reject bad requests before they touch your database. The real MiddlewareInterface looks almost identical — swap the arrays for PSR-7 objects and rename handle to process .
3️⃣ CORS: Working on the Response
Not all middleware works on the request — plenty of it shapes the response on the way out. CORS (Cross-Origin Resource Sharing) is the classic example: when a browser on app.example.com calls your API on a different domain, it needs Access-Control-Allow-* headers on the response or it will block the result. Browsers also send a "preflight" OPTIONS request first to ask permission — and your CORS middleware should answer that immediately , short-circuiting before the real handler runs.
The GET flows through to the handler and then gets CORS headers merged onto its response. The OPTIONS preflight is answered with a 204 and the handler is never called. Same middleware, two paths — exactly the flexibility the onion gives you.
4️⃣ Your Turn: Call $next
Now you build a timing middleware. The whole trick is to do work, call $next , then do more work with the response. Fill in each ___ using the 👉 hint, run it, and check it against the Output panel.
One more, and this one is about order . An error handler can only catch what's inside it, so it must wrap the layer that throws. Fill the blank so the error handler runs first and the throwing handler is its $next .
Common Errors (and the fix)
- The request reaches the controller but the response is empty / the page hangs — a middleware forgot to call $next (or forgot to return what it returned). If a layer doesn't call and return $next($request) , the pipeline stops dead there. Only skip $next when you mean to short-circuit — and then you must return a real response.
- Exceptions escape as a 500 with a stack trace, or auth runs on unvalidated data — your middleware is in the wrong order . The error handler must be outermost (piped first) so it wraps everything; authentication must come before authorization; CORS must be able to add headers even when an inner layer short-circuits. Remember: first piped = outermost = runs first on the way in .
- "Cannot modify header information — headers already sent by ..." — you tried to add a header (or change the response) after output was already flushed . In real PHP, once any echo or whitespace before < ?php has been sent, headers are locked. Do all response/header work in middleware before the body is emitted, and never leave blank lines after a closing ? > .
- Errors silently vanish — a request "succeeds" but nothing happened — a middleware swallowed an exception with an empty catch and returned a fake success. Catch only what you can handle, log the rest, and re-throw or return a proper error status. A swallowed error is far harder to debug than one that's reported.
Pro Tips
- 💡 Implement PSR-15 for real apps. A class that implements Psr\Http\Server\MiddlewareInterface drops straight into Slim, Mezzio, or any PSR-15 dispatcher — no glue code.
- 💡 Keep middleware thin. Don't run heavy database queries in middleware that applies to static assets. Use route groups so a layer only runs where it's needed.
- 💡 Treat requests as immutable. PSR-7 objects are immutable: methods like withAttribute() return a copy . Always pass the new request into $next , not the original.
📋 Quick Reference — Middleware
Concept
In code
What it does
$next
$next($request)
Pass control to the next layer in
before
code above $next
Runs on the way in (e.g. auth check)
after
code below $next
Runs on the way out (e.g. add headers)
short-circuit
return $resp;
Return without calling $next (401, 204, 429)
PSR-7
ResponseInterface
Standard HTTP message objects
PSR-15
MiddlewareInterface
Standard middleware: process($req, $handler)
pipe order
first = outermost
First piped runs first on the way in
Frequently Asked Questions
Mini-Challenge: Rate Limiter
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. A rate limiter is real middleware you'll meet on every public API.
🎉 Lesson Complete!
- ✅ Middleware wraps your controller in an onion : requests flow in, responses flow back out through the same layers
- ✅ $next($request) hands control to the next layer; code before it runs in, code after it runs out
- ✅ PSR-7 standardises HTTP messages; PSR-15 standardises the middleware contract so layers from any package compose
- ✅ A pipeline composes the stack inside-out — the first piece you pipe is outermost and runs first
- ✅ Short-circuit by returning a response without calling $next (auth 401, CORS preflight 204, rate-limit 429)
- ✅ Order matters : error handler outermost, auth before authorization, and never modify the response after it's been sent
- ✅ Next lesson: Advanced PDO — transactions, prepared statements, and stored procedures for safe, fast database work
Practice quiz
What is middleware in a PHP request/response pipeline?
- A database table that stores requests
- A template engine for rendering HTML
- A layer of code that wraps your controller, running before the request reaches it and after the response comes back
- A tool that compiles PHP to machine code
Answer: A layer of code that wraps your controller, running before the request reaches it and after the response comes back. Middleware wraps the controller like an onion: the request travels inward through each layer, then the response travels back outward.
What does the $next callback represent inside a middleware?
- A callback that hands control to the next layer in the pipeline
- The previous middleware that already ran
- The database connection
- The HTTP status code
Answer: A callback that hands control to the next layer in the pipeline. $next means 'process the rest of the stack and give me a response' — code before it runs on the way in, code after it on the way out.
In a middleware, where does code that runs 'on the way out' go relative to $next?
- Before the $next($request) call
- Inside a separate Footer() method
- It cannot run after $next
- After the $next($request) call
Answer: After the $next($request) call. Code before $next runs on the way in (e.g. an auth check); code after $next runs on the way out (e.g. adding headers).
What does it mean for a middleware to 'short-circuit' the pipeline?
- It calls $next twice
- It returns a response WITHOUT calling $next, so the rest of the pipeline and the controller never run
- It throws an exception to skip ahead
- It restarts the request from the beginning
Answer: It returns a response WITHOUT calling $next, so the rest of the pipeline and the controller never run. Short-circuiting returns a response without $next — how auth returns 401, CORS answers a 204 preflight, or a rate limiter returns 429.
When the AuthMiddleware finds no Authorization header, what does it do?
- Returns a 401 response and never calls $next, so the controller never runs
- Calls $next($request) anyway
- Logs the user in as a guest
- Throws a fatal error
Answer: Returns a 401 response and never calls $next, so the controller never runs. It short-circuits with a 401 'Unauthorized' and never calls $next, cheaply rejecting the request before it hits the database.
In the Pipeline, why is the stack composed inside-out with array_reverse?
- To run middleware in random order
- To improve performance
- So the FIRST middleware you pipe ends up on the OUTSIDE and runs first
- Because array_reverse is required by PSR-15
Answer: So the FIRST middleware you pipe ends up on the OUTSIDE and runs first. Wrapping the controller with the last middleware first makes the first one piped the outermost, so pipe order equals run order.
Why must an error-handling middleware sit on the OUTSIDE of the layer that throws?
- Because it is alphabetically first
- Because it can only catch exceptions thrown by layers INSIDE it (in its $next)
- Because outer middleware runs faster
- Because PSR-15 forbids inner error handlers
Answer: Because it can only catch exceptions thrown by layers INSIDE it (in its $next). An error handler wraps everything inside it in try/catch, so it must be outermost to catch exceptions thrown anywhere deeper.
What does PSR-15 standardise?
- The HTTP message objects (request and response)
- The database query syntax
- The autoloader format
- The middleware contract — MiddlewareInterface with process($request, $handler) returning a response
Answer: The middleware contract — MiddlewareInterface with process($request, $handler) returning a response. PSR-15 defines the middleware contract; PSR-7 standardises the HTTP message objects it operates on.
What does a CORS middleware do for an OPTIONS preflight request?
- Forwards it to the controller like any other request
- Answers it immediately with a 204 and the CORS headers, short-circuiting before the real handler runs
- Rejects it with a 401
- Converts it to a GET request
Answer: Answers it immediately with a 204 and the CORS headers, short-circuiting before the real handler runs. Browsers send OPTIONS first to ask permission; the CORS middleware replies with a 204 and headers without hitting the app.
If a middleware forgets to call (or return) $next, what is the likely symptom?
- The request is automatically retried
- The controller runs twice
- The response is empty or the page hangs because the pipeline stops dead there
- A 500 error with a stack trace
Answer: The response is empty or the page hangs because the pipeline stops dead there. Without calling and returning $next($request), the pipeline halts at that layer — only skip $next when you mean to short-circuit.
Continue this course
- Previous: Micro MVC Framework
- Next: Advanced PDO