The Chain-of-responsibility programming design pattern (explained using PHP)

The Chain-of-responsibility programming design pattern (explained using PHP) Thumbnail image

The Chain-of-responsibility programming design pattern (explained using PHP) Table of contents

  1. A quick and basic example
  2. How Laravel uses the chain of command design pattern in it's middleware

    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.

    1. interface HandlerInterface
    2. {
    3.  
    4.     /** set the next handler */
    5.     public function setNext(HandlerInterface $next);
    6.  
    7.     /** run this handler's code */
    8.     public function handle($data = null);
    9.  
    10.     /** run the next handler  */
    11.     public function next($data = null);
    12. }
    13.  
    14. abstract class AbstractHandler implements HandlerInterface
    15. {
    16.     // the next one to process after this
    17.     /** @var  HandlerInterface */
    18.     protected $next;
    19.  
    20.     /** set the next handler */
    21.     public function setNext(HandlerInterface $next)
    22.     {
    23.         $this->next = $next;
    24.     }
    25.  
    26.     /** run the next handler */
    27.     public function next($data = null)
    28.     {
    29.         if ($this->next) {
    30.             // go to next one:
    31.             return $this->next->handle($data);
    32.         }
    33.         // else, no more to do
    34.     }
    35.  
    36. }
    37.  
    38. class IPCheckHandler extends AbstractHandler
    39. {
    40.     // should not really hard code this in! But this is a demo...
    41.     const BANNED_IPS = ['123.123.123.123'];
    42.  
    43.     public function handle($data = null)
    44.     {
    45.         var_dump(__METHOD__ . " - checking that request is not from banned IP");
    46.         if (in_array($data['ip'], self::BANNED_IPS)) {
    47.             throw new \Exception("Invalid IP");
    48.         }
    49.  
    50.         // ok, all good, go to the next one:
    51.         return $this->next($data);
    52.     }
    53.  
    54.  
    55. }
    56.  
    57. class MustBeLoggedInHandler extends AbstractHandler
    58. {
    59.     public function handle($data = null)
    60.     {
    61.         var_dump(__METHOD__ . " - checking that user is logged in");
    62.         if (empty($data['user_id'])) {
    63.             throw new \Exception("Must be logged in");
    64.         }
    65.  
    66.         // ok, all good, go to the next one:
    67.         return $this->next($data);
    68.     }
    69. }
    70.  
    71. class MustBeAdminUserHandler extends AbstractHandler
    72. {
    73.     public function handle($data = null)
    74.     {
    75.         var_dump(__METHOD__ . " - checking if admin user");
    76.         if (empty($data['is_admin'])) {
    77.             throw new \Exception("Must be admin user");
    78.         }
    79.  
    80.         // ok, all good, go to the next one:
    81.         return $this->next($data);
    82.     }
    83. }
    84.  
    85. // some dummy data here - it would actually be some form of request object, this is just for a quick demo
    86. $data = [
    87.     'ip' => '127.0.0.1',
    88.     'requested_uri' => '/home',
    89.     'user_id' => 123,
    90.     'is_admin' => true,
    91. ];
    92.  
    93.  
    94. // instantiate the handlers:
    95. $mustBeLoggedIn = new MustBeLoggedInHandler();
    96. $ipCheck = new IPCheckHandler();
    97. $adminCheck = new MustBeAdminUserHandler();
    98.  
    99. // set the chain (only the first two need to be set up - the last item doesn't have a 'next' handler):
    100. $mustBeLoggedIn->setNext($ipCheck);
    101. $ipCheck->setNext($adminCheck);
    102.  
    103. // and run the first one:
    104. $mustBeLoggedIn->handle($data);
    105. /* output:
    106. string(63) "MustBeLoggedInHandler::handle - checking that user is logged in"
    107. string(68) "IPCheckHandler::handle - checking that request is not from banned IP"
    108. string(55) "MustBeAdminUserHandler::handle - checking if admin user"
    109. */
    110.  
    111. var_dump("Now doing it, but with invalid data:");
    112.  
    113. // now to try it with some data that will cause one the handlers to throw an exception:
    114. $data = [
    115.     'ip' => '127.0.0.1',
    116.     'requested_uri' => '/home',
    117.     'user_id' => null, /* !! */
    118.     'is_admin' => true,
    119. ];
    120. $mustBeLoggedIn->handle($data);
    121. /* output:
    122. string(63) "MustBeLoggedInHandler::handle - checking that user is logged in"
    123.  
    124. In design_pattern_demo.php line 65:
    125.                      
    126.   Must be logged in  
    127. */
    128.  
    129.  
    130. // 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:

    1.    public function handle($request, Closure $next, ...$attributes)
    2.     {
    3.    
    4.          // do whatever the middleware  needs to do here...
    5.  
    6.         return $next($request);
    7.     }

    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

    webdevetc profile pic
    webdevetc

    I am a 29 year old freelance backend web developer from London, mostly focusing on PHP and Laravel lately. This is my site - I mostly write about PHP here. Contact me here (especially for any contracting jobs early 2019 in London ;) ).

    Leave a Comment