Middleware is a layer of software that acts as a bridge between different systems, applications, or components. It facilitates communication, data exchange, and functionality between these systems.
We will create a web middleware application that handles HTTP requests and performs tasks such as session management, authentication, error handling, etc. To build our own web middleware application, we will utilize the Chain of Responsibility (CoR) design pattern. If you are unfamiliar with CoR, I have provided a detailed explanation here.
The main purpose of the CoR is to a create a chain of objects and pass a request along the chain to be examined. Each object in turn examines the request and tries to handle it or passes it if it fails on to the next object in the chain.
What We Need
Let's take a look at what we need to create our own web middleware application. In this implementation, we'll require a handler interface, several concrete handler implementations, and a client.
Handler Interface: Defines an interface for handling requests. Optionally implements a successor link - MiddlewareInterface
, for example.
Concrete Handlers: Implements the handler interface for different middlewares — AuthorizationMiddleware
, ValidationMiddleware
, LoggingMiddleware
, etc.
Client: The client that initiates the request to a concrete handler object on the chain.
Handler Interface
The middleware interface specifies a common contract for chaining objects and processing a request. Also, it ensures that all concrete handler classes follow the same method signatures.
interface MiddlewareInterface
{
public function setNext(MiddlewareInterface $next): MiddlewareInterface;
public function handle(array $request): void;
}
Here, the MiddlewareInterface
defines two abstract methods – setNext()
and handle()
. The setNext()
method is responsible for adding a handler to the chain, while the handle()
method processes the request.
Base Middleware
We need an abstract base class to define key methods that will be used by child classes. This base class provides the default structure for chaining middleware.
abstract class BaseMiddleware implements MiddlewareInterface
{
private ?MiddlewareInterface $next = null;
public function setNext(MiddlewareInterface $next): MiddlewareInterface
{
$this->next = $next;
return $next;
}
protected function next(array $request): void
{
if ($this->next) {
$this->next->handle($request);
}
}
}
Here the setNext()
method enables adding a concrete middleware object to the chain, while next()
method passes the request to the subsequent middleware.
Concrete Handlers
Now we need some concrete middleware handlers. Let's create them:
// Authorization Middleware
class AuthorizationMiddleware extends BaseMiddleware
{
public function handle(array $request): void
{
if (empty($request['user']) || !$request['user']['isAuthorized']) {
die("403 Forbidden: Unauthorized Access.");
}
$this->next($request);
}
}
// Validation Middleware
class ValidationMiddleware extends BaseMiddleware
{
public function handle(array $request): void
{
if (empty($request['data']) || !isset($request['data']['name'])) {
die("400 Bad Request: Validation Failed.");
}
$this->next($request);
}
}
// Logging Middleware
class LoggingMiddleware extends BaseMiddleware
{
public function handle(array $request): void
{
echo "Logging Request: " . json_encode($request) . "\n";
$this->next($request);
}
}
Here the AuthorizationMiddleware
verifies user authorization, the ValidationMiddleware
validates incoming request data, and the LoggingMiddleware
logs the request.
Keep in mind that if you intend to use this application in production, the concrete middleware classes will need significant improvement and redesign to align with best practices.
Client
We now have all the necessary building blocks to build our application. It's time to set up the middleware chain. Below is a simplified implementation of a web middleware application:
// Create middleware instances
$authMiddleware = new AuthorizationMiddleware();
$validationMiddleware = new ValidationMiddleware();
$loggingMiddleware = new LoggingMiddleware();
// Link middleware in chain
$authMiddleware
->setNext($validationMiddleware)
->setNext($loggingMiddleware);
// Define a mock HTTP request
$request = [
'user' => ['isAuthorized' => true],
'data' => ['name' => 'John Doe']
];
// Start the middleware chain
$authMiddleware->handle($request);
In the client code, we create instances of specific middleware classes: AuthorizationMiddleware
, ValidationMiddleware
, and LoggingMiddleware
. These middleware objects act as handlers in the Chain of Responsibility.
We chain the handlers by calling the setNext()
method on each middleware object in the desired order: $authMiddleware
, $validationMiddleware
, and $loggingMiddleware
. This sets up the sequence in which the middleware will process the request.
Finally, we initiate the chain by calling the handle()
method on the first middleware object, $authMiddleware
, passing in a mock HTTP request. Each middleware in the chain processes the request and if necessary, forwards it to the next middleware.
Is It CoR Compliant?
Yes, this implementation conforms to the Chain of Responsibility design pattern. In CoR, a request is passed through a sequence of handler objects, each of which can process the request, pass it to the next handler, or stop further processing if needed. Here's how we ensured them:
Creating Chain: Middleware instances ($authMiddleware
, $validationMiddleware
, and $loggingMiddleware
) are linked using the setNext()
method creating a chain where each middleware knows about the next.
Handling Request: The first middleware ($authMiddleware
) receives the request. If it succeeds, it forwards the request to the next middleware ($validationMiddleware
) using next()
method.
Terminating Early: If any middleware fails authorization or validation, it terminates the chain by not calling the next()
method.
AuthorizationMiddleware
, ValidationMiddleware
, and LoggingMiddleware
only focus on their specific logic. They are not aware of the logic of other middleware in the chain meaning each handler is independent. They are decoupled too.
That's it.
What’s next? You could implement robust concrete handlers that extend the base middleware and handle more specific logic as needed. Additionally, you could improve the client code. You may collect middleware classes from a configuration system and resolve those classes' instances through a dependency injection manager. You could then chain those instances dynamically.
Since you’ve made it this far, hopefully you enjoyed the reading! Please share the article.
Top comments (0)