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.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
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 .
📋 Quick Reference — Middleware
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.
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.