Implementing a Chain of Responsibility Design Pattern in Middleware

Onur Yüksel
Jotform Tech
Published in
8 min readApr 2, 2023
An illustration about chain of responsibility (source: refactoring.guru)

Modern software systems often involve complex subsystems for functions like authentication, authorization, logging, and more. Managing these subsystems and flows can become challenging as the application surface grows. A chain of responsibility design pattern provides a solution to this problem by creating a chain of interconnected components, each responsible for handling a specific type of task.

In this article, we will explore the chain of responsibility design pattern, which is a behavioral design pattern. We will delve into what the chain of responsibility is, examine the pros and cons of the pattern, and create a router middleware using this pattern. Let’s get started!

What is chain of responsibility?

The chain of responsibility is a design pattern described in the book Design Patterns: Elements of Reusable Object-Oriented Software. This pattern allows a request to be passed along a chain of objects until one of the objects can handle the request or the chain ends.

As the name suggests, each object in the chain is responsible for handling a specific task or request. If an object receives a request that it cannot handle, it passes the request to the next object in the chain.

Using this design pattern improves the flexibility and reusability of the code. Request-handling processes can modify the request or check its requirements without affecting the overall behavior of the system. This makes it easier to maintain and extend the code over time.

The pros and cons

Here’s a breakdown of the advantages and disadvantages of implementing a chain of responsibility design pattern.

Pros:

  1. Flexibility: The pattern allows you to add, remove, or modify objects without affecting the overall behavior of the system.
  2. Reusability: Objects in the chain can be reused in other chains or other parts of the system, reducing development time on subsequent projects.
  3. Open/Closed Principle: The chain of responsibility design pattern follows the open/closed principle, which means you can add new objects to the chain without modifying the core logic.
  4. Decoupling: The pattern decouples the sender of a request from its receiver, allowing you to change the handling of the request without affecting the rest of the system.

Cons:

  1. Performance: Since each request passes through multiple objects, it can affect the overall performance of the system.
  2. Complexity: A chain involves multiple objects that interact with each other, increasing the complexity of the system.
  3. Debugging: Debugging can be challenging when requests are passing between objects.

Overall, the chain of responsibility pattern can provide flexibility and reusability by providing a strong logic to pass requests between objects. However, it can also impact performance, increase complexity, and make debugging more difficult.

Middleware

In a web application, middleware forms a chain between the router and request handler. When a request is received by the server, it first passes through any defined middleware functions before reaching the route handler function.

Middleware functions can perform a variety of tasks, such as logging, authentication, authorization, and modifying the request or response objects. Each middleware function can modify the request or response object before passing it on to the next middleware function in the chain.

It’s worth noting that the definition of middleware presented above also applies to the chain of responsibility design pattern. In other words, middleware can be viewed as a type of chain of responsibility. As such, it is considered a best practice to develop middleware using the chain of responsibility pattern.

Creating a middleware class

The middleware is an abstract class because we don’t use the class as an instance directly. Instead, we will create new middleware classes like “SampleMiddleware” that inherit from Middleware.

abstract class Middleware {
private $nextMiddleware;

public function __construct(Middleware $next = null)
{
$this->nextMiddleware = $next;
}

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

public function setNext(Middleware $next)
{
$this->nextMiddleware = $next;
}
}

The “Middleware” class we’ve created follows a similar logic to that of a linked list data structure. With this abstract class structure in place, we can create a one-way list of middleware by linking instances of the middleware classes together, effectively handling the chain part of the logic.

<?php

abstract class Middleware {
private $nextMiddleware;

public function __construct(Middleware $next = null)
{
$this->nextMiddleware = $next;
}

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

public function setNext(Middleware $next)
{
$this->nextMiddleware = $next;
}

public function handle($request) {
$this->process($request);
if ($this->nextMiddleware !== null) {
$this->nextMiddleware->handle($request);
}
}

abstract protected function process($request);
}

Now, we have added the responsibility part to our middleware. Each instance of the class handles a request by invoking the overridden process() method, and if there is another middleware instance in the chain, it calls the handle() function of that instance. This creates a chain in which each node can call the next member of the chain, effectively passing the request down the line.

Creating middleware for testing

We have created an abstract class for the “Middleware” logic. Now we can create some middleware to test.

<?php

include_once "middleware.php";

class LogMiddleware extends Middleware {
protected function process($request)
{
file_put_contents('log.json', json_encode($request), FILE_APPEND);
}
}

I created a simple middleware to log requests to a file. This was achieved by extending the Middleware abstract class and overriding the process() function.

<?php

include_once "middleware.php";

class AuthenticationMiddleware extends Middleware
{
protected function process($request)
{
if (!isset($request["authenticated"]) || $request["authenticated"] !== true) {
throw new Exception("Authentication fail !");
}
}
}

I’ve just created a middleware for testing authentication. Note that this is not a real authentication logic, but rather a fake implementation to test the middleware. Since authentication is a critical process for handling requests, I’m throwing an exception and ending the chain if the request is not authenticated.

Testing middleware

<?php

include_once "LogMiddleware.php";
include_once "AuthenticationMiddleware.php";


$request = [
'authenticated' => true,
'ip' => "127.0.0.1",
'username' => 'onuryukseldev'
];
$middlewareChain = new LogMiddleware(new AuthenticationMiddleware());
$middlewareChain->handle($request);
echo "Chain Of Responsibility is the best for creating middleware !";

I’ve just created a fake request and a middleware chain by instantiating a LogMiddleware and passing an AuthenticationMiddleware instance to its constructor. To trigger the chain, I called the handle() method of the LogMiddleware.

The expected behavior is as follows: LogMiddleware creates a log for the request, and then AuthenticationMiddleware checks the authentication status. If authentication is successful, the message “Chain of responsibility is the best for creating middleware!” displays. However, if the authenticated field is removed from the $request variable, the middleware chain will not process with the next middleware, and the message “Fatal error: Uncaught Exception: Authentication failed!” will be displayed.

Implementing our middleware to a router

In a previous article, I discussed how to create a router in PHP that allows us to route incoming HTTP requests to the appropriate controller method. Now, I’ll discuss how to integrate a middleware system with this router to handle complex request-processing logic.

If you’re interested in learning how to create a router in PHP or need a refresher, check out my previous article, “Building a Basic Router with PHP: A Step-by-Step Guide for Web Developers.”

First, I’m unhappy with referring to my middleware as nested constructor blocks. Instead, I would like to create a wrapper for my chain. This will allow me to create different chains and make my code more understandable.

<?php

include_once "middleware.php";

class MiddlewareChain {
private $head;

public function __construct() {
$this->head = null;
}

public function addMiddleware(Middleware $middleware) {
if ($this->head === null) {
$this->head = $middleware;
} else {
$current = $this->head;
while ($current->getNext() !== null) {
$current = $current->getNext();
}
$current->setNext($middleware);
}
}

public function handle($request) {
if ($this->head !== null) {
$this->head->handle($request);
}
}
}

I have encapsulated my code with MiddlewareChain. This way, instead of creating a middleware class and adding other middleware in its constructor, I am creating a MiddlewareChain and triggering the first handle() function by using the MiddlewareChain class’s handle() function.

In my previous article, we created the following Router class:

<?php

class Router {
protected $routes = []; // stores routes

public function addRoute(string $method, string $url, closure $target) {
$this->routes[$method][$url] = $target;
}

public function matchRoute() {
$method = $_SERVER['REQUEST_METHOD'];
$url = $_SERVER['REQUEST_URI'];
if (isset($this->routes[$method])) {
foreach ($this->routes[$method] as $routeUrl => $target) {
// Use named subpatterns in the regular expression pattern to capture each parameter value separately
$pattern = preg_replace('/\/:([^\/]+)/', '/(?P<$1>[^/]+)', $routeUrl);
if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
// Pass the captured parameter values as named arguments to the target function
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); // Only keep named subpattern matches
call_user_func_array($target, $params);
return;
}
}
}
throw new Exception('Route not found');
}
}

To add MiddlewareChain to our router, we can easily update the addRoute and matchRoute functions to add a MiddlewareChain to the $routes array.

<?php

include_once "MiddlewareChain.php";

class Router {
protected $routes = []; // stores routes

public function addRoute(string $method, string $url, closure $target, MiddlewareChain $middleware = null) {
$this->routes[$method][$url]["target"] = $target;
$this->routes[$method][$url]["middleware"] = $middleware;
}

public function matchRoute() {
$method = $_SERVER['REQUEST_METHOD'];
$url = $_SERVER['REQUEST_URI'];
if (isset($this->routes[$method])) {
foreach ($this->routes[$method] as $routeUrl => $route) {
// Use named subpatterns in the regular expression pattern to capture each parameter value separately
$pattern = preg_replace('/\/:([^\/]+)/', '/(?P<$1>[^/]+)', $routeUrl);
if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
// Pass the captured parameter values as named arguments to the route function
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); // Only keep named subpattern matches
call_user_func_array($route['target'], $params);
return;
}
}
}
throw new Exception('Route not found');
}
}

I have just modified the route array by creating a new sub-array to include the target and middleware. As a result, I have also updated the matchRoute() logic to call the “target” when it is fired. Now, we can implement the chain logic to handle middleware.

public function matchRoute() {
$method = $_SERVER['REQUEST_METHOD'];
$url = $_SERVER['REQUEST_URI'];
if (isset($this->routes[$method])) {
foreach ($this->routes[$method] as $routeUrl => $route) {
if (isset($route["middleware"])) {
$route["middleware"]->handle($_REQUEST); // just calling the handle function of the chain.
}

// Use named subpatterns in the regular expression pattern to capture each parameter value separately
$pattern = preg_replace('/\/:([^\/]+)/', '/(?P<$1>[^/]+)', $routeUrl);
if (preg_match('#^' . $pattern . '$#', $url, $matches)) {
// Pass the captured parameter values as named arguments to the route function
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); // Only keep named subpattern matches
call_user_func_array($route['target'], $params);
return;
}
}
}
throw new Exception('Route not found');
}

If $route[“middleware”] is defined, we can start the chain by calling the handle() function. Then, we can create our new route with the middleware chain.

<?php

include_once "LogMiddleware.php";
include_once "TestMiddleware.php";
include_once "MiddlewareChain.php";
include_once "router.php";

$defaultChain = new MiddlewareChain();
$defaultChain->addMiddleware(new TestMiddleware());
$defaultChain->addMiddleware(new LogMiddleware());

$router = new Router();

$router->addRoute('GET', '/test-middleware', function () {
echo "My middleware works!";
exit;
}, $defaultChain);

$router->matchRoute();

I have just added a new middleware called TestMiddleware that echoes “Middleware runs!” to see if my middleware is working as expected. Then, I created a chain called $defaultChain and passed it to my simple ‘test-middleware’ route.

Here is the result: Message from ‘/test-middleware’:

Middleware runs ! My middleware works!

Conclusion

In this article, we have learned about the chain of responsibility. We have created a simple middleware and implemented it into our existing router to understand the chain of responsibility concept. I believe that instead of creating artificial examples, it is easier to understand design patterns by implementing them in basic functions.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in Jotform Tech

Welcome to Jotform official tech blog. Read about software engineering and how Jotform engineers build the easiest form builder.

Written by Onur Yüksel

Jr Backend Developer @Jotform. Passionate about building scalable apps. Sharing insights on tech, best practices, and experiences. Let's connect!

No responses yet

Write a response