DALT.PHP
Framework Deep Dive2. Routing

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: Continue

The 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 key
  • Auth::class is 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 check

How 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 logic

Problems:

  • 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                      Stop

Each 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

  1. Middleware runs before controllers - Filters requests early
  2. Map translates keys to classes - 'auth'Auth::class
  3. Multiple middleware run in order - First to last
  4. Stopping is done by redirect/exit/throw - No return value needed
  5. 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.

On this page