Middleware System
How middleware filters requests before they reach controllers
Security at the Door
Middleware is like a security checkpoint at a building entrance:
- Check ID before entering
- Verify you have permission
- Stop you if something's wrong
- Let you through if everything's okay
In web apps, middleware runs between the router and controller:
Request → Router → Middleware → Controller → Response
↓
If fails: Stop here
If passes: ContinueThe Middleware Map
Middleware classes are registered in a map:
// framework/Core/Middleware/Middleware.php
public const MAP = [
'guest' => Guest::class,
'auth' => Auth::class,
'csrf' => Csrf::class,
];What this means:
'auth'is a shorthand keyAuth::classis the full class name:Core\Middleware\Auth
Why use a map?
- Routes use short names:
->only('auth') - The map translates to actual classes
- Easy to add new middleware without changing routes
How Middleware is Resolved
When a route has middleware, the router calls:
Middleware::resolve($route['middleware']);The Resolution Process
public static function resolve($keys)
{
if (!$keys) {
return;
}
$middlewares = is_array($keys) ? $keys : [$keys];
foreach ($middlewares as $key) {
$middleware = static::MAP[$key] ?? false;
if (!$middleware) {
throw new \Exception("No Matching Middleware found for key '{$key}'");
}
(new $middleware)->handle();
}
}Step by step:
1. Check if middleware exists
if (!$keys) {
return;
}If the route has no middleware, skip this entirely.
2. Normalize to array
$middlewares = is_array($keys) ? $keys : [$keys];This handles both:
- Single:
->only('auth') - Multiple:
->only(['auth', 'csrf'])
3. Loop through each middleware
foreach ($middlewares as $key) {If you have multiple middleware, they run in order.
4. Find the class
$middleware = static::MAP[$key] ?? false;
if (!$middleware) {
throw new \Exception("No Matching Middleware found for key '{$key}'");
}Look up the key in the MAP. If not found, throw an error.
5. Create and run
(new $middleware)->handle();This:
- Creates a new instance:
new Auth() - Calls the
handle()method immediately
The Middleware Contract
Every middleware must have a handle() method:
class Auth
{
public function handle()
{
// Check something
// If fails: redirect/exit
// If passes: return (continue)
}
}The pattern:
- If the check fails → stop the request (redirect, exit, throw)
- If the check passes → return (let the request continue)
Middleware Execution Order
$router->post('/posts', 'posts/store.php')->only(['auth', 'csrf']);What happens:
1. Router matches /posts
2. Middleware::resolve(['auth', 'csrf'])
3. Run Auth middleware
↓ If fails: redirect to /
↓ If passes: continue
4. Run Csrf middleware
↓ If fails: show 419 error
↓ If passes: continue
5. Run controller (posts/store.php)Order matters!
// Good: Check auth first, then CSRF
->only(['auth', 'csrf'])
// Bad: Check CSRF before auth
->only(['csrf', 'auth'])
// Why bad? Unauthenticated users waste time on CSRF checkHow Middleware Stops Requests
Middleware can stop a request in several ways:
1. Redirect and Exit
public function handle()
{
if (!$_SESSION['user']) {
header('location: /');
exit();
}
}What happens:
header('location: /')tells browser to go to/exit()stops PHP execution immediately- Controller never runs
2. Throw an Exception
public function handle()
{
if (!$token) {
throw new \Exception('CSRF token missing');
}
}What happens:
- Exception bubbles up to
public/index.php - Caught by the global error handler
- User sees error page
3. Output and Exit
public function handle()
{
if (!$valid) {
http_response_code(419);
echo 'CSRF token mismatch';
exit;
}
}What happens:
- Sets HTTP status code
- Outputs message
- Stops execution
Why Middleware is Powerful
Without Middleware
Every controller needs to check auth:
// posts/index.php
if (!$_SESSION['user']) {
header('location: /');
exit();
}
// ... actual logic
// posts/show.php
if (!$_SESSION['user']) {
header('location: /');
exit();
}
// ... actual logic
// posts/store.php
if (!$_SESSION['user']) {
header('location: /');
exit();
}
// ... actual logicProblems:
- Repetitive code
- Easy to forget
- Hard to change (update 50 files)
With Middleware
$router->get('/posts', 'posts/index.php')->only('auth');
$router->get('/posts/{id}', 'posts/show.php')->only('auth');
$router->post('/posts', 'posts/store.php')->only('auth');Benefits:
- ✅ DRY (Don't Repeat Yourself)
- ✅ Centralized logic
- ✅ Easy to change
- ✅ Declarative (route says what it needs)
The Pipeline Pattern
Middleware implements the "pipeline" pattern:
Request
↓
[Middleware 1] → Pass? → [Middleware 2] → Pass? → [Controller]
↓ Fail ↓ Fail
Stop StopEach middleware is a "gate" that can:
- Let the request through
- Stop the request
This is similar to:
- Airport security checkpoints
- Building access control
- Quality control in manufacturing
Adding Custom Middleware
Want to add your own? Easy:
Step 1: Create the class
// framework/Core/Middleware/Admin.php
namespace Core\Middleware;
class Admin
{
public function handle()
{
if (!($_SESSION['user']['is_admin'] ?? false)) {
header('location: /');
exit();
}
}
}Step 2: Register in the map
public const MAP = [
'guest' => Guest::class,
'auth' => Auth::class,
'csrf' => Csrf::class,
'admin' => Admin::class, // ← Add this
];Step 3: Use it
$router->get('/admin', 'admin/index.php')->only(['auth', 'admin']);Now /admin requires both login AND admin status.
Key Takeaways
- Middleware runs before controllers - Filters requests early
- Map translates keys to classes -
'auth'→Auth::class - Multiple middleware run in order - First to last
- Stopping is done by redirect/exit/throw - No return value needed
- Pipeline pattern - Each middleware is a gate
What's Good Here
✅ Simple implementation (just a map + loop)
✅ Easy to add new middleware
✅ Declarative route protection
✅ Centralized logic (DRY)
✅ Clear and understandable for learning
Design Note
Advanced features like middleware parameters, "after" middleware, and middleware groups are intentionally omitted. They're useful in production but add complexity. DALT keeps it simple so you can understand the core pattern.
Next, explore how routes are registered and stored.