The Chain-of-responsibility programming design pattern (explained using PHP) Table of contents
The Chain of Responsibility design pattern is gaining in popularity, since being part of the PSR-15 standard (HTTP Request Middleware). Here is a quick overview of it. It allows you to chain a bunch of actions, which should be run in a certain order. Very often these actions have the ability to stop processing any further items.
The Chain of Responsibility uses a list of objects, it will cycle through them - unless one of them stops the 'next' one.
You can find a form of this in Laravel's middleware.
A quick and basic example
The example below shows how three items are chained together (first MustBeLoggedInHandler, then IPCheckHandler, then finally MustBeAdminUserHandler).
In this example, if a handler wishes to stop any further actions taking place it throws an exception. However, often you would actually do something like return false and it would halt further processing of any other items in the chain. But for this example I just made it throw exceptions.
You could just code all of this in one big class (going through a large array of things to check/things to do). However that doesn't really sit with the SOLID principles. Using the chain of responsibility pattern lets you follow the open/closed principle, and makes it much easier to extend in the future.
<?php
interface HandlerInterface
{
/** set the next handler: */
public function setNext(HandlerInterface $next);
/** run this handler's code */
public function handle($data = null);
/** run the next handler */
public function next($data = null);
}
abstract class AbstractHandler implements HandlerInterface
{
// the next one to process after this
/** @var HandlerInterface */
protected $next;
/** set the next handler */
public function setNext(HandlerInterface $next)
{
$this->next = $next;
}
/** run the next handler */
public function next($data = null)
{
if ($this->next) {
// go to next one:
return $this->next->handle($data);
}
// else, no more to do
}
}
class IPCheckHandler extends AbstractHandler
{
// should not really hard code this in! But this is a demo...
const BANNED_IPS = ['123.123.123.123'];
public function handle($data = null)
{
var_dump(__METHOD__ . " - checking that request is not from banned IP");
if (in_array($data['ip'], self::BANNED_IPS)) {
throw new \Exception("Invalid IP");
}
// ok, all good, go to the next one:
return $this->next($data);
}
}
class MustBeLoggedInHandler extends AbstractHandler
{
public function handle($data = null)
{
var_dump(__METHOD__ . " - checking that user is logged in");
if (empty($data['user_id'])) {
throw new \Exception("Must be logged in");
}
// ok, all good, go to the next one:
return $this->next($data);
}
}
class MustBeAdminUserHandler extends AbstractHandler
{
public function handle($data = null)
{
var_dump(__METHOD__ . " - checking if admin user");
if (empty($data['is_admin'])) {
throw new \Exception("Must be admin user");
}
// ok, all good, go to the next one:
return $this->next($data);
}
}
// some dummy data here - it would actually be some form of request object, this is just for a quick demo
$data = [
'ip' => '127.0.0.1',
'requested_uri' => '/home',
'user_id' => 123,
'is_admin' => true,
];
// instantiate the handlers:
$mustBeLoggedIn = new MustBeLoggedInHandler();
$ipCheck = new IPCheckHandler();
$adminCheck = new MustBeAdminUserHandler();
// set the chain (only the first two need to be set up - the last item doesn't have a 'next' handler):
$mustBeLoggedIn->setNext($ipCheck);
$ipCheck->setNext($adminCheck);
// and run the first one:
$mustBeLoggedIn->handle($data);
/* output:
string(63) "MustBeLoggedInHandler::handle - checking that user is logged in"
string(68) "IPCheckHandler::handle - checking that request is not from banned IP"
string(55) "MustBeAdminUserHandler::handle - checking if admin user"
*/
var_dump("Now doing it, but with invalid data:");
// now to try it with some data that will cause one the handlers to throw an exception:
$data = [
'ip' => '127.0.0.1',
'requested_uri' => '/home',
'user_id' => null, /* !! */
'is_admin' => true,
];
$mustBeLoggedIn->handle($data);
/* output:
string(63) "MustBeLoggedInHandler::handle - checking that user is logged in"
In design_pattern_demo.php line 65:
Must be logged in
*/
// in a real app you wouldn't be manually doing it one by one - it is just for example purposes :)
How Laravel uses the chain of command design pattern in it's middleware
Laravel passes a callback to it's handle method:
<?php
public function handle($request, Closure $next, ...$attributes)
{
// do whatever the middleware needs to do here...
return $next($request);
}
Want to see more? see the decorator pattern. It is quite similar, however the chain of responsibility has the feature that any of the items in the cycle has the option of stopping any more in the cycle/list from running. The decorator pattern does not have this. Like I said, in the example above I just throw exceptions - it is common to handle errors a with a bit more grace ;)
You can also find examples of this pattern in some implementations of validation (such as form validation, when a user submits a form). You could chain together things such as validating min/max length, correct characters (a-z0-9 alpha numeric), valid URL, etc).
Design pattern type: Behavioural design pattern
Comments →The Chain-of-responsibility programming design pattern