Middleware & the pipeline
The onion model, before and after work, parameters and groups, terminable middleware β and the pipeline class that powers it all.
Middleware is a layer wrapped around the request. Each layer decides whether to pass the request inward, and gets a chance to change the response on the way back out.
request βββΊ [ A βββΊ [ B βββΊ [ route ] βββΊ B ] βββΊ A ] βββΊ response
The shape
Every middleware is a class with one method: handle($request, Closure $next).
<?php
use Illuminate\Support\Facades\Route;
class AddHeader
{
public function handle($request, \Closure $next)
{
// before: the request has not reached the route yet
$response = $next($request);
// after: the route has run and produced a response
return $response->header('X-Powered-By-Guide', 'yes');
}
}
Route::get('/wrapped', fn () => 'the route ran')->middleware(AddHeader::class);
$r = visit('/wrapped');
echo $r->getContent(), ' | header: ', $r->headers->get('X-Powered-By-Guide'), PHP_EOL;Order is visible
Register several and the nesting becomes obvious.
<?php
use Illuminate\Support\Facades\Route;
class Outer
{
public function handle($request, \Closure $next)
{
echo " outer: before", PHP_EOL;
$response = $next($request);
echo " outer: after", PHP_EOL;
return $response;
}
}
class Inner
{
public function handle($request, \Closure $next)
{
echo " inner: before", PHP_EOL;
$response = $next($request);
echo " inner: after", PHP_EOL;
return $response;
}
}
Route::get('/onion', function () {
echo " route body", PHP_EOL;
return 'done';
})->middleware([Outer::class, Inner::class]);
visit('/onion');
echo 'finished', PHP_EOL;Outer runs first on the way in and last on the way out. That symmetry is the whole model.
Short-circuiting
A middleware that does not call $next() stops the request. The route never runs.
<?php
use Illuminate\Support\Facades\Route;
class RequireToken
{
public function handle($request, \Closure $next)
{
if ($request->query('token') !== 'secret') {
return response('Forbidden', 403);
}
return $next($request);
}
}
Route::get('/private', fn () => 'the good stuff')->middleware(RequireToken::class);
echo visit('/private')->getStatusCode(), PHP_EOL; // 403
echo visit('/private?token=secret')->getContent(), PHP_EOL; // allowed throughπ§ Scenario: An authorisation check written inside every controller action is five lines duplicated fifteen times, and the sixteenth action is the one someone forgets. The same check as middleware applies to a whole route group and cannot be forgotten β that is the security argument for middleware, and it is stronger than the tidiness argument.
Parameters
Anything after a colon is passed to handle() as an extra argument.
<?php
use Illuminate\Support\Facades\Route;
class RequireRole
{
public function handle($request, \Closure $next, string ...$roles)
{
$role = $request->query('role', 'guest');
if (! in_array($role, $roles, true)) {
return response("need one of: " . implode(', ', $roles), 403);
}
return $next($request);
}
}
Route::get('/admin', fn () => 'admin area')
->middleware(RequireRole::class . ':admin,owner');
echo visit('/admin?role=guest')->getContent(), PHP_EOL;
echo visit('/admin?role=admin')->getContent(), PHP_EOL;This is the mechanism behind auth:sanctum, throttle:60,1 and can:update,post.
Aliases and groups
In bootstrap/app.php you give middleware short names and bundle them into groups.
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => RequireRole::class,
]);
$middleware->appendToGroup('web', [
AddHeader::class,
]);
$middleware->prepend(TrustProxies::class); // global, before everything
})web and api are the two built-in groups. web adds sessions, cookies and CSRF; api is deliberately stateless.
<?php
use Illuminate\Pipeline\Pipeline;
$result = app(Pipeline::class)
->send(' hello world ')
->through([
fn ($text, $next) => $next(trim($text)),
fn ($text, $next) => $next(ucwords($text)),
fn ($text, $next) => $next(str_replace(' ', '-', $text)),
])
->thenReturn();
echo $result, PHP_EOL;Terminable middleware
A terminate() method runs after the response has been sent to the browser β the right place for slow, non-essential work.
<?php
use Illuminate\Support\Facades\Route;
class LogRequest
{
public function handle($request, \Closure $next)
{
return $next($request);
}
public function terminate($request, $response): void
{
echo " terminate: logged {$request->path()} β {$response->getStatusCode()}", PHP_EOL;
}
}
Route::get('/logged', fn () => 'response body')->middleware(LogRequest::class);
echo visit('/logged')->getContent(), PHP_EOL;β οΈ terminate() only runs after a sent response, and only under a SAPI that supports it (FastCGI). It is not a queue: work that must happen should be a queued job, not a terminate hook.
Excluding a middleware
<?php
use Illuminate\Support\Facades\Route;
class Blocker
{
public function handle($request, \Closure $next)
{
return response('blocked by group middleware', 403);
}
}
Route::middleware([Blocker::class])->group(function () {
Route::get('/blocked', fn () => 'never reached');
Route::get('/allowed', fn () => 'made it through')
->withoutMiddleware([Blocker::class]);
});
echo visit('/blocked')->getContent(), PHP_EOL;
echo visit('/allowed')->getContent(), PHP_EOL;withoutMiddleware() only removes route middleware β global middleware cannot be skipped this way.
Reference: Middleware in the Laravel documentation.