---
title: Middleware
source: 'https://academia.sh/en/courses/web-application-framework/middleware'
course: 'The Web Application Framework'
language: en
updated: '2026-10-21T05:03:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Reads how a request passes through nested middleware layers to reach the action, and what the call to $next means'
    - 'Measures that when a layer in front gives the response, the layers behind it and the action fall under skipped'
    - 'Compares that in a request with a session, all the layers and the action fall under ran'
    - 'Measures that changing the order of the links stops the same request at a different layer and returns a different status code'
---

# Middleware

Moving cross-cutting concerns into layers placed in front of the action, the request passing through the layers to reach the action, the call to $next being made or not made, and the order of the links deciding the result.

The previous lesson showed that for an action to run, the route must match and the controller must be built. But there may also be steps in front of the action that every request must pass. If these checks are written inside the action, they are repeated in every controller. The framework puts them in one place in front of the action, and before the action is called, a response may be produced at that place.

## Cross-Cutting Concerns and Middleware

Jobs such as whether the session is open, whether the input is valid, or whether a header is added to the response concern many actions in the same way. These are called **cross-cutting concerns**: none of them belongs to one action, but all of them lie on the path of every action. **Middleware** is a function that does this work in one place. It takes the request and a `$next` function and does one of two things: either it passes the request on by calling `$next`, which continues the chain, or it returns its own response without calling `$next`.

The layers form a **chain**. The action is the innermost link of the chain; each layer is an outer link and receives the one inside it as `$next`. The request enters the outermost link, descends one step inward as each layer calls `$next`, and reaches the action. The response returns along the same path in reverse: the response of the action reaches the innermost layer, from there the layer outside it, and finally the caller. This is why a layer can have two parts: the part that runs before the call to `$next` and the part that runs after the call, on the way back.

`middleware.php` builds this structure. `chain()` wraps the layers in reverse order: the action stays innermost and the layer wrapped last stays outermost. There are three ready-made layers. `sessionLayer()` gives the response itself, a 302 redirecting to `/login`, if there is no session. `validationLayer()` gives 422 if the title is empty. `traceLayer()` calls `$next` and adds a header to the response **on the way back**.

One consequence of this structure is that middleware is not a filter concerned only with the request. The call to `$next` has two sides, and each side suits a different job. The part before the call examines or changes the request: it reads the session, validates the input, adds a field to the request. The part after the call examines or changes the response: it adds a header, wraps the body, measures the time. A single layer can do both, and both stand inside the same function, on the two sides of the same `$next` call.

The layer has a third option, and this lesson deliberately does not measure it: calling `$next` more than once, or never returning. Frameworks usually leave this to the user. The rule is simple: a layer either continues the chain or gives the response itself, and which one it does is decisive for the rest of the chain.

```php
<?php
// middleware.php — nested middleware layers and the chain
declare(strict_types=1);
require_once 'ran.php';

final class Request {
    public function __construct(
        public string $path, public array $body = [], public bool $session = false,
    ) {}
}

final class Response {
    public array $headers = [];
    public function __construct(public int $status, public string $body = '') {}
}

function sessionLayer(): array {
    return ['session', fn(Request $r, Closure $next) =>
        $r->session ? $next($r) : new Response(302, '/login')];
}

function validationLayer(): array {
    return ['validation', fn(Request $r, Closure $next) =>
        ($r->body['title'] ?? '') !== '' ? $next($r) : new Response(422, 'title required')];
}

function traceLayer(): array {                 // also runs after $next returns
    return ['trace', function (Request $r, Closure $next): Response {
        $response = $next($r);
        $response->headers['x-trace'] = 'passed';
        return $response;
    }];
}

function chain(Request $request, array $layers, string $part, Closure $action): Response {
    Ledger::newRequest();
    foreach ([...array_column($layers, 0), $part] as $p) Ledger::onPath($p);
    $link = function (Request $r) use ($part, $action): Response {
        Ledger::ran($part);
        return $action($r);
    };
    foreach (array_reverse($layers) as [$name, $layer]) {      // the outermost layer is wrapped last
        $link = function (Request $r) use ($name, $layer, $link): Response {
            Ledger::ran($name);
            return $layer($r, $link);
        };
    }
    return $link($request);
}
```

The `ran.php` core is given again in every lesson.

```php
<?php
// ran.php — did an expected part run in this request?
declare(strict_types=1);

final class Ledger {
    public static array $path = [];        // parts placed on the request's path
    public static array $ran = [];         // parts called within the request
    public static array $deferred = [];    // parts left outside the request
    public static function newRequest(): void { self::$path = self::$ran = self::$deferred = []; }
    public static function onPath(string $part): void { self::$path[] = $part; }
    public static function ran(string $part): void { self::$ran[] = $part; }
    public static function defer(string $part): void { self::$deferred[] = $part; }
}

function classify(string $part): string {
    return match (true) {
        in_array($part, Ledger::$ran, true) => 'ran',
        in_array($part, Ledger::$deferred, true) => 'deferred',
        in_array($part, Ledger::$path, true) => 'skipped',
        default => 'not wired',
    };
}
```

**FF15.** `chain()` does two jobs, and they are separate. The first `foreach` puts all the layer names and the part on the **path**: every link in the chain for this request is on the path. Then, as each link is called, it reports itself as ran with `Ledger::ran()`. The difference is deliberate: the path says what the chain is; ran says how far the chain advanced. If a link is on the path but not in the ran list, a link before it did not call `$next`, and the decision order makes that part **skipped**.

## If the Layer in Front Gives the Response

The chain is in the order `[traceLayer, sessionLayer, validationLayer]` and the action is `save`. `trace` is outermost. Two requests are processed: one without a session and one with a session. The body of both is valid, that is, it carries a title that will pass validation; the only difference is the session.

```php
<?php
// layers.php — if the layer in front gives the response, the layers behind are skipped
declare(strict_types=1);
require_once 'middleware.php';

$layers = [traceLayer(), sessionLayer(), validationLayer()];
$action = fn(Request $r) => new Response(201, 'saved');
$body = ['title' => 'Hello'];

foreach (['unauthenticated' => false, 'authenticated' => true] as $name => $session) {
    $response = chain(new Request('/posts', $body, $session), $layers, 'save', $action);
    printf("%s -> %d %s %s\n", $name, $response->status, $response->body, json_encode($response->headers));
    foreach (['trace', 'session', 'validation', 'save'] as $part) {
        printf("  %-10s %s\n", $part, classify($part));
    }
}
```

```
unauthenticated -> 302 /login {"x-trace":"passed"}
  trace      ran
  session    ran
  validation skipped
  save       skipped
authenticated -> 201 saved {"x-trace":"passed"}
  trace      ran
  session    ran
  validation ran
  save       ran
```

**FF16.** In the request without a session, the `session` layer did not call `$next` and returned 302. The `validation` layer and the `save` action behind it were on the path but were not called; both are **skipped**. In the request with a session, `session` called `$next` and every link of the chain was called: all four parts **ran** and the response was 201. The same chain, the same action; the only difference is the `session` field of the request.

There is one more detail in the first request: although the response is 302, the header carries `x-trace`. The `trace` layer is outside the session layer; it called `$next`, received the response as 302 and added the header on the way back. The part that runs on the way back runs for the outer layers even when an inner layer gives an early response. That is, the part of a layer before `$next` can be skipped, but the return part of an outer layer is not skipped.

This measurement has a limit. `Ledger` says whether a link was called; it does not say what the called link did inside. The `trace` layer appears as ran, but the fact that it added the header on the way back is not in the ledger; we read it only from the headers of the response. The ran outcome therefore means "the link was called", not "all the work of the link finished". Even if the part that runs on the way back had been cut by an error, the link would have looked as ran.

This difference matters in middleware errors. If the part of a layer before the call fails, the chain is cut there, and the links behind are on the path but were not called: in the ledger they appear as skipped. The cause of a skip may be a normal early response or an error; what tells them apart is the status code and the response body, not the outcome. For this reason the skipped outcome is not a sign of failure; it is a sign of control flow.

This lesson's outcome is **skipped**. A skipped part gives itself away by a status code: 302 says the session stopped the request, 422 says the validation did. But the status code shows only the layer that gave the response; it does not show how many parts behind were skipped, and `Ledger` lists that.

## The Bounding Measurement

Does the order of the links change the result? The order of two layers decides which one gives the response. A request without a session **and** with an empty title meets the response condition of both layers. The same request is measured in two separate orders: first `session`, then `validation`; then the reverse.

```php
<?php
// order.php — the order of the links decides which layer gives the response
declare(strict_types=1);
require_once 'middleware.php';

$action = fn(Request $r) => new Response(201, 'saved');
$request = new Request('/posts', ['title' => ''], false);     // no session and an empty title

foreach (['session first' => [sessionLayer(), validationLayer()],
          'validation first' => [validationLayer(), sessionLayer()]] as $name => $layers) {
    $response = chain($request, $layers, 'save', $action);
    printf("%s: %d %s\n", $name, $response->status, $response->body);
    foreach (['session', 'validation', 'save'] as $part) {
        printf("  %-10s %s\n", $part, classify($part));
    }
}
```

```
session first: 302 /login
  session    ran
  validation skipped
  save       skipped
validation first: 422 title required
  session    skipped
  validation ran
  save       skipped
```

**FF17.** The two orders gave two different results. When `session` came first, the request received 302, `session` ran, and `validation` and `save` were skipped. When `validation` came first, the request received 422, `validation` ran, and `session` and `save` were skipped. The request and the layers are the same; only the order changed, and the status code and the skipped part changed together. The action was skipped in both orders, because in both the layer in front gave the response itself.

Which layer is skipped matters here. In the order with `validation` first, a user without a session learns the details of the input: the 422 response says the title is empty. In the order with `session` first, the request without a session is redirected without the input being looked at at all. Which order is right is a security and usability decision; the measurement does not settle it, it only shows that the order changes the result.

In practice the order is also chosen by which layer is expensive. The session check is usually cheap and rejects the request at the door when needed; validation may require parsing the body. If a cheap layer that rejects many requests is put in front, the layers behind it that are skipped never pay this cost. In this measurement both layers are cheap, and only which of them gave the response was measured; cost is the subject of a separate measurement.

## Summary

- Middleware takes the request and the `$next` function and either continues the chain by calling `$next`, or returns its own response without calling it.
- In the chain the action is innermost; the request moves from outside to inside, the response from inside to outside, and the part of a layer after `$next` runs on the way back.
- In the request without a session, `session` returned 302, and `validation` and `save` were skipped; the outer `trace` layer still added a header to the response. In the request with a session, all four parts ran.
- When `session` came first, the same request received 302; when `validation` came first, it received 422; the order of the links decides which layer gives the response.
- This lesson's outcome is **skipped**: a skipped part is on the path, but a layer in front gave the response early.

## Next Step

In this lesson we built the layers and the action by hand inside `chain()`: `chain()` did not know which object a part depended on. In a real framework the layers, the controllers and their dependencies sit in an object container and are taken by name. The next lesson, Object Container and Facades, asks what happens if the container binds a name twice: is the first binding or the second one resolved, and does the overwritten part count as bound to the request?
