DEV Community

Cover image for How the PHP Middleware Pattern works and can easily be applied
Doeke Norg
Doeke Norg

Posted on • Originally published at doeken.org

How the PHP Middleware Pattern works and can easily be applied

In this post we'll be looking at Middleware in PHP. This pattern is most common in the handling of requests and responses. But the Middleware Pattern can also be applied in various other places. We'll look into what middleware is, how middleware works, when middleware can be useful and what an alternative to middleware might be.

Note: The middleware pattern is not part of the patterns introduced by the
Gang of Four, but I personally still view it as a pattern, as it can be applied in various situations.

What is Middleware?

Middleware in PHP is a layer of actions / callables that are wrapped around a piece of core logic in an application. These middlewares provide the ability to change either the input or the output of this logic. So they "live" between the input and the output, right in the middle.

While these short explanations are obviously very easy to follow and click instantly, let me illustrate this with a small example:

$input = [];
$output = $this->action($input);
Enter fullscreen mode Exit fullscreen mode

In this example, a Middleware layer is able to:

  1. change $input before it is injected into $this->action()
  2. change $output after it was returned by $this->action()

Let's look at how it does this.

How middleware works

The Middleware layer consists of a stack of middleware callables. These can be either a simple Closure, an invokable class or a method on a class.

Each one of the middleware callables is wrapped around the core action. Alright, I'll say it; it's like an onion, or a present with multiple layers of wrapping paper. The input gets injected into the first middleware (or most outer layer). The middleware can do something with the input, and then pass that (changed) input along to the next middleware, and the next, and the next, until it reaches the final action (the core). By this time, the input maybe changed a little or a lot.

Then the core action is executed and returns its output to the last middleware, which returns its output back to the previous middleware, all the way back to the first middleware. On its way back, every middleware can now change the output before it gets returned.

Before we look at some examples, let's first explore the different types of middleware.

Types of middleware

While the concept of middleware is pretty much the same between them, there are actually two common types of middleware: Single Pass Middleware & Double Pass Middleware.

Single Pass middleware

The most common type of middleware is Single Pass Middleware. With this type every middleware callable receives two parameters:

  1. the input (e.g. a request, message, DTO or scalar value)
  2. A callable to invoke the next middleware in the chain, that also receives a single parameter: the input.

Note: In some conventions the callable parameter is called $next to indicate the invocation of the next middleware.

This type of middleware can change the input and forward that modified version to the next middleware. To affect the output; it will first call the next middleware and retrieve its output. It can then modify that output and return this instead.

Here is a small example middleware to illustrate the full behavior.

function ($input, $next) {
  // Receive the input and change it.
  $input = $this->changeInput($input);

  // Call the next middleware or final action with the changed input, and receive the output.
  $output = $next($input); 

  // Change the output, and return it to the previous middleware or final consumer.
  return $this->changeOutput($output);
}
Enter fullscreen mode Exit fullscreen mode

Double Pass Middleware

The second type of middleware is Double Pass Middleware. With this type every middleware also receives a default output object as a parameter. So the "double" part refers to the middleware passing along both the input and an output to the next middleware / final action.

Why is this useful? In the Single Pass type a middleware must either:

  1. Create the required output object when short-circuiting (not calling $next but returning a different result).
  2. Call the $next middleware(s) to retrieve an output object, and change that.

Depending on the type of output, it can be cumbersome or even undesirable to make the middleware depend on a service or factory to create that output type. And it can be just as undesirable to call (and possibly instantiate) every other middleware and final action, only to discard everything they do; and change the output object completely.

So with the Double Pass a default output object is created beforehand and passed around. This way the middleware already has an output object of the correct type it can use when it wants to short-circuit.

Here is another small example to illustrate the full behavior.

function ($input, $output, $next) {
  // Quickly return the output object, instead of passing it along.
  if ($this->hasSomeReason($input)) {
    return $output;
  }

  // Call the next middleware and return that output instead of the default output.
    return $next($input, $output); 
}
Enter fullscreen mode Exit fullscreen mode

Now we know what a middleware callable looks like, but how can we wrap all these middlewares on top of an action?

A simple middleware implementation

Let's dive into some code, and create a very basic Single Pass middleware implementation. In this example we will prepend & append some value to a string (re-)using a single middleware class.

Note: There are many ways a middleware implementation can be built. This example isn't very optimized, and there is lots of room for improvement. But this example is really to demonstrate the inner workings of middleware as clear as possible.

As we've seen, we first need a core action (or function) to wrap our middleware around. Our action will simply receive
and return a string.

$action = fn(string $input): string => $input;
Enter fullscreen mode Exit fullscreen mode

Now we'll create an invokable middleware class that we can use multiple times with a different value to prepend and append.

class ValueMiddleware
{
    public function __construct(string $value) {}

    public function __invoke(string $input, callable $next): string
    {
        // Prepend value to the input.
        $output = $next($this->value . $input);

        // Append value to the output.
        return $output . $this->value;
    }
}
Enter fullscreen mode Exit fullscreen mode

The middleware class is instantiated with a $value. This instance will be our middleware callable, since it is an invokable class. Let's create 3 of them:

$middlewares = [
    new ValueMiddleware('1'),
    new ValueMiddleware('2'),
    new ValueMiddleware('3'),
];
Enter fullscreen mode Exit fullscreen mode

Now we'll add all of these middlewares as a layer around our $action, and then we'll examine the code.

foreach ($middlewares as $middleware) {
    $action = fn(string $input): string => $middleware($input, $action);
}
Enter fullscreen mode Exit fullscreen mode

Let's look at what is happening in this loop.

  • At first the $action closure is our simple callback which returns the $input.
  • In the first loop $action is overwritten with a new closure that also receives the $input (just like the initial action). When this closure is executed, it will call the first middleware, and provide that input, as well as our initial $action. So inside that middleware $next is now the original action.
  • In the second and third loop $action is overwritten again, but in this middleware the $next callable is not the original action, but the closure we set at the previous iteration.

Note: As we can see, $next never directly refers to a middleware. It can't, because the middleware method signature expects 2 parameters, while $next only receives one; the input. $next is always a callable which is responsible for calling the next middleware. So this can also be a method on a middleware handler that contains and keeps track of a list of (applied) middlewares.

And that's it. Our middleware implementation is complete. All we need to do now is run the $action with a value, and check out the response.

echo $action('value');
Enter fullscreen mode Exit fullscreen mode

The result of this will of course be: 123value123... wait what? Did you expect 123value321? That would make the whole onion thing make more sense, right? But it is actually correct.

The middleware that prepends and appends 1 is applied first, but is then wrapped with 2, which in turn is wrapped by 3. So middleware 3 is the first to prepend 3 on the input, but it is also the last middleware to append that value. It's a bit of a mind-bender, but when we check out the $input and return for every middleware we end up with this list:

Middleware $input return
Middleware 3 value 123value123
Middleware 2 3value 123value12
Middleware 1 23value 123value1
Core-action 123value 123value

Working our way down the $input list, and back up the return list, we can see what stages the value passes through in this entire middleware flow.

Tip: If you want the order of middlewares to be outside-in, where the top of the list is the first one to be called, you should array_reverse the array, or use a Last-In-First-Out (LIFO) iterator like a Stack.

Request and Response middleware

One of the most common places you'll find middleware being used is within a framework that transforms a Request object into a Response object. PSR-15: HTTP Server Request Handlers in the PHP Standard Recommendation (PSR), is a recommendation on how a (PSR-7) Request object should be handled and turned into a Response object. This recommendation also contains the Psr\Http\Server\MiddlewareInterface. This interface favors the use of a process method on a middleware class, but the principle is the same. It receives an input (the Request object), modifies it, and passes it along to RequestHandler which will trigger the next middleware or final action.

Tip: Laminas' Mezzio is a minimal PSR-7 middleware Framework which provides a set of PSR-15 middlewares. And while most frameworks that work with PSR-7 and PSR-15 use Single Pass Middleware (since that
is what PSR-15 recommends) it also provides a Double Pass Middleware Decorator, so those will also work with these PSRs.

Learn more about decorators and proxies

Examples of Request and Response middlewares

Middlewares can be used to perform all kinds of actions and checks during a request, so the main action (controller) can focus on the task at hand. Let's look at a few examples of middleware that can be useful.

Cross-site request forgery (CSRF) validation middleware

To prevent a CSRF attack a framework can choose to add a certain validation to a request. This validation need to happen before the main action is triggered. In this case a middleware can examine the request object. If it deems the request valid, it can pass it along to the next handler. But if the validation fails, the middleware can immediately short-circuit the request, and return a 403 Forbidden response.

Multi-tenant middleware

Some applications allow for multi-tenancy, which basically means the same source code is used for different customers; e.g. by hooking up a separate database / source per customer. A middleware can inspect the request and figure out (for instance by checking the request URL) what database should be selected, or which customer should be loaded. It can even attach that Customer entity (if one exists) to the Request object, so the core action can read the proper information off the request object, instead of having to figure out the correct customer itself.

Exception handling middleware

Another great type of middleware is the handling of exceptions. If this middleware is applied as the outermost middleware it can wrap the entire request to response flow in a try-catch block. If an exception is thrown, this middleware can log the exception and then return a properly configured response object which contains information about the exception.

Other real world examples of middleware

While middleware is most common in the handling of requests and responses, there are other use cases. Here are a some examples:

Symfony Messenger middleware

The Queue handler of Symfony symfony/messenger uses middleware to manipulate the Message when it is sent to the queue, and when it is read from the queue and handled by the MessageHandler.

One example of these is the router_context middleware. Because messages are (mostly) handled asynchronous, the original Request is not available. This middleware stores the original Request state (things like the host and HTTP port) when it is sent to the queue and restores this when the Message is handled, so the handler can use this context to build up things like absolute URLs.

Guzzle middleware

While also somewhat in the realm of Request and Response; the Guzzle HTTP Client also supports the use of middleware. These again allow you to change the request and response to and from the HTTP request. This implementation works a bit different, but still revolves around a callback that triggers the next middleware / action.

Alternative to middleware

Like we've seen, middleware essentially allows us to change input and output respectively before and after a core action, within a single function / callable. So a great alternative to middleware would be to provide event hooks before and after the core action. With these events you can change the input before it hits the core action, and maybe even return early. And afterwards you can change the output.

Both of these options work quite well. For instance Laravel uses middleware around their Request and Response flow, while Symfony uses the Events approach.

Note: If you want to learn more about events, you can check out my (quite lengthy) blog post on Event Dispatching.

Thanks for reading

When using middleware you can easily create a separation of concerns within your code. Every middleware has a tiny job to do. It can easily be tested; and the core action can focus on what it does best, without worrying about any possible edge cases or side effects. And while it currently mostly revolves around (PSR-7) Request and Response objects, the pattern can be used widely. Do you have other ideas where this pattern can be useful? Let me know!

Top comments (0)