The Middleware Pattern

0

The middleware pattern is a powerful design pattern available to programmers in various programming languages which simplifies layered processes required for various features like logging, authenticating, processing, data transformation, monitoring, caching, load balancing, error handling and much more.

It works by putting the initial user’s request into a callback function and passing it through the middleware pipeline, with each middleware in the pipeline passing that callback function to the next middleware and the last middleware finally invoking that callback function fulfilling the user’s request.

There are times when the middleware comes between a process and the result that the process initiates to fulfill a user’s request or the initiated feature like queuing a message to be sent or handling an event like validating user’s request or filtering the results.

Starting Simple

Working with this example, I’m going to have everything in one file with the tag <pre> at the top to style the lines of text we will be printing to the page. Let’s start with a simple example that works with queuing messages.

class MessageQueue
{
    private array $queue;

    public function enqueue(String $message): void
	{
        $this->queue[] = $message;
    }

    public function dequeue(): string
	{
        return array_shift($this->queue);
    }

    public function inQueue(): bool
    {
        return count($this->queue);
    }
}

That is our super simple message queuing class were we add, retrieve and handle the while loop exit condition logic for the middleware system we will be implementing here.

There are many possible reasons we might want to implement the middleware pattern here for our message queuing system. We might want to filter messages, log some things (maybe we’re keeping track of things and logging for statistical use) or something else entirely. These actions needs to be performed the same for every message, but the number of messages we’re sending through would be a dynamic amount.

There are many ways we could go about doing this, but I feel the most scalable and maintainable method is to implement the middleware pattern here.

The Middleware

Before we go ahead and start working on our middleware, we need to set some standards by which each middleware will be build around to make sure every current middleware and any possible future middleware works as expected. To do that, we would set up an interface which each middleware would be implementing.

interface Middleware
{
    public function handle(string $message, callable $next);
}

That ensures that each middleware has the handle method which the request would be grabbing and passing the required arguments for processing.

Now that we have the standard set up, we could start creating the middlewares we would be sending the request through. The first middleware we’re going to make is to process the logging logic for our messages. To keep things simple, we would simply be printing a line of text instead of actually performing any sort of action. Also, for this implementation of a message queuing system, we won’t need to return any feedback to the user.

class LoggingMiddleware implements Middleware
{
    public function handle(string $message, callable $next): void
    {
        echo "[Middleware] Logging: $message\n";
        
        $response = $next($message);
        
        echo "[Middleware] Logging finished: $response\n\n\n";
    }
}

The $message is the message from the queue that the middleware is processing in an iteration, and the $next is the callback function (the next middleware) that the current middleware will be calling. Since the $next is being called between the two echo statements, the next middleware will finish before the second echo statement in this middleware is called giving us an option to run some code after the message was sent.

class ProcessingMiddleware implements Middleware
{
    public function handle(string $message, callable $next)
    {
        echo "[Middleware] Processing: $message\n";
        
        if(mt_rand(0, 1)) {
            $response = $next($message);
            
            echo "[Middleware] Processing finished: $response\n";
            return "[Middleware] All good!";
        } else {
            echo "[Middleware] Processing failed for message: $message\n";
            return "[Middleware] Everything bad!";
        }
    }
}

This is our custom middleware for processing. We are using a random number generator to generate a 0 (for false) or 1 (for true) to simulate a success/failure state of message sending process. Each of these middlewares gets put into a callback function and gets sent to the next middleware in the signature.

Running the Middleware

Well, we’ve got our message queue class and all of the middlewares that we are going to be using to send our messages. We are now ready to start creating a system which will be sending the messages utilizing the middlewares we have created for it.

The usage I have in mind for this system is putting the middlewares in a list which we would be later iterating and using.

$middlewares = [
    new LoggingMiddleware(),
    new ProcessingMiddleware(),
];

So, we need the first middleware called to be the LoggingMiddleware() and then the ProcessingMiddleware() afterwards. The interesting thing about the middleware pattern is that the last item in the middleware list gets run first while everything before it is put into a callback function. So in order to have the middleware run properly we need to reverse the array. We could do that manually by entering the middlewares in reverse order, or simply use the array_reverse() function available to us in PHP. I would be using the array_reverse() function to reverse the array, this way we could keep the logical order in which the middlewares would be running represented in the middlewares array.

Lets create the handling logic for our messaging queue system which we would be calling to send the messages using our middlewares.

class MessageQueueHandler
{
    protected $middlewares = [];

    public function __construct(array $middlewares)
    {
        $this->middlewares = $middlewares;
    }

    public function processQueue(MessageQueue $queue): void
    {
        $next = function () {
            return "[End-Point] Message was sent successfully!";
        };

        while($queue->inQueue()) {
            $message = $queue->dequeue();
            $this->executeMiddlewares($message, $next);
        }
    }

    protected function executeMiddlewares(string $message, callable $next): void
    {
        $middlewares = array_reverse($this->middlewares);
        foreach($middlewares as $middleware) {
            $next = function ($message) use ($middleware, $next) {
                return $middleware->handle($message, $next);
            };
        }
        $next($message);
    }
}

Here, when we instantiate the message queue handler class, we have to pass the list of middlewares we would be using for each message we would be sending, and since we are using the array_reverse() function in our execution method, we can keep the logical order of the middlewares in the array.

Using the System

So, having all that, we are ready to use our system. We need to add some messages into the queue that we will be sending.

$messageQueue = new MessageQueue();

$messageQueue->enqueue("Message 1");
$messageQueue->enqueue("Message 2");
$messageQueue->enqueue("Message 3");
$messageQueue->enqueue("Message 4");
$messageQueue->enqueue("Message 5");
$messageQueue->enqueue("Message 6");

$middlewares = [
    new LoggingMiddleware(),
    new ProcessingMiddleware(),
];

$messageQueueHandler = new MessageQueueHandler($middlewares);

$messageQueueHandler->processQueue($messageQueue);

There we have it. A ‘working’ message queuing system with middlewares. Of course middlewares are more in use with processing routes and their implementation/integration would be a bit different than in the mailing system.

Routing Requests Through Middleware

Mainly, middlewares are used in conjunction with routes for authentication or other features. Without implementing an entire routing system, we would look from where the routing system hands over the reigns to the middleware system. The route is already determined, the parameters that needs to be passed to the controller is already in place; all that is left to do is pass these things on to the middleware.

In order to properly showcase the middleware system in terms of routing the requests, we would need a simple sample request class. Once again, I’ll just put everything in one page for this example to simplify things. Instead of breaking everything down, I’ll just post the entire working page

<?php

class Request
{
    private array $request = [];

    public function __construct()
    {
        $this->request['_GET'] = $_GET;
        $this->request['_POST'] = $_POST;
    }

    public function request()
    {
        return $this->request;
    }
}

class MiddlewarePipeline
{
    protected $middlewares = [];

    public function addMiddleware($middleware)
    {
        $this->middlewares[] = $middleware;
    }

    public function handle(Request $request, callable $controllerClosure)
    {
        $pipeline = array_reduce(
            array_reverse($this->middlewares),
            function ($next, $middleware) {
                return function ($request) use ($middleware, $next) {
                    return $middleware->handle($request, $next);
                };
            },
            $controllerClosure
        );

        return $pipeline($request);
    }
}

interface Middleware
{
    public function handle(Request $request, callable $next);
}

class SampleMiddleware implements Middleware
{
    public function handle(Request $request, callable $next)
    {
        echo "Sample middleware before\n";
        $response = $next($request);
        echo "Sample middleware after\n";
        return $response;
    }
}

class AnotherMiddleware implements Middleware
{
    public function handle(Request $request, callable $next)
    {
        echo "Another middleware before\n";
        $response = $next($request);
        echo "Another middleware after\n";
        return $response;
    }
}

class YetAnotherMiddleware implements Middleware
{
    public function handle(Request $request, callable $next)
    {
        echo "Yet another middleware before\n";
        $response = $next($request);
        echo "Yet another middleware after\n";
        return $response;
    }
}

class route
{
    function handle(array $middleware, string $class, string $method, array $params = [])
    {
        $pipeline = new MiddlewarePipeline();
        foreach ($middleware as $middlewareClass) {
            $pipeline->addMiddleware(new $middlewareClass());
        }

        $request = new Request();

        return $pipeline->handle($request, function ($request) use ($class, $method, $params) {

            if(!empty($request->request())) {
                $params['request'] = $request;
            }

            $classInstance = new $class();
            return call_user_func_array([$classInstance, $method], $params);
        });
    }
}

class controller {
    public function index(Request $request) {
        $request = $request->request();
        echo "Inside the controller's `index` method... {$request['email']}\n";
        return "\n\nThe return value of the controller's `index` method\n\n";
    }

    public function show()
    {
        echo "The show method of the controller with no request data passed.\n";
        return "\n\nSome return value from the show method.\n\n";
    }
}

$middleware = [
    SampleMiddleware::class,
    AnotherMiddleware::class,
    YetAnotherMiddleware::class
];

$_POST = [
    'name'  => 'Demo User',
    'email' => 'demouser456@email.com'
];

echo (new route)->handle($middleware, controller::class, 'index');

$_POST = [];

echo (new route)->handle($middleware, controller::class, 'show');

That’s that. Look through that, play with that code and see how everything works. How everything is echoed and returned shows how the middleware pattern executes and should help understand how everything works.

Comments

No Comments. Be the first to make a comment.

Need to Register or Login to comment on post