Sjoerd Maessen blog

PHP and webdevelopment

PHP hook, building hooks in your application

with 3 comments

Introduction
One of the real challenges in building any type of framework, core or application is making it possible for the developers to hook into the business logic at specific points. Since PHP is not event based, nor it works with interrupts you have to come up an alternative.

The test case
Lets assume we are the main developers of a webshop framework. Programmers can use our framework to build complete webshops. Programmers can manage the orders that are placed on the webshop with the order class. The order class is part of our framework and we don’t want it to be extended by any programmer. However we don’t want to limit to programmers in their possibilities to hook into the orders process.

For example programmers should be able to send an email to the webshopowner if an order changes from one specific delivery status to another. This functionality is not part of the default behavior in our framework and is custom for the progammers webshop implementation.

Like said before, PHP doesn’t provide interrupts or real events so we need to come up with another way to implement hooks into our application. Lets take a look at the observer pattern.

Implementing the Observer pattern
The observer pattern is a design-pattern that describes a way for objects to be notified to specific state-changes in objects of the application.

For the first implementation we can use SPL. The SPL provides in two simple objects:

SPLSubject

  • attach (new observer to attach)
  • detach (existing observer to detach)
  • notify (notify all observers)

SPLObserver

  • update (Called from the subject (i.e. when it’s value has changed).
  1. <?php
  2. /**
  3.  * Order class, adding, inserting, updating and deleting orders
  4.  *
  5.  * @package Shop
  6.  */
  7. class Order implements SplSubject
  8. {
  9.         // Order statuses
  10.         const STATUS_PAID = 1;
  11.         const STATUS_PICKED = 2;
  12.         const STATUS_SHIPPED = 3;
  13.         const STATUS_DELIVERED = 4;
  14.        
  15.         /**
  16.          * OrderRef
  17.          * @var int $iOrderRef
  18.          */
  19.         protected $iOrderRef;
  20.         /**
  21.          * Status
  22.          * @var int $iStatus
  23.          */
  24.         protected $iStatus;
  25.         /**
  26.          * List of attached observers
  27.          * @var array
  28.          */
  29.         private $aObservers = array();
  30.        
  31.         /**
  32.          * Constructor
  33.          *
  34.          * @param integer $iOrderRef
  35.          */
  36.         public function __construct($iOrderRef)
  37.         {
  38.                 if(!is_int($iOrderRef)) {
  39.                         throw new InvalidArgumentException(sprintf(‘Orderref should be of type integer type "%s" is given’, gettype($iOrderRef)));
  40.                 }
  41.                 $this->iOrderRef = $iOrderRef;
  42.                
  43.                 // Get order information from the database or an other resources
  44.                 $this->iStatus = Order::STATUS_SHIPPED;
  45.         }
  46.        
  47.         /**
  48.          * Attach an observer
  49.          *
  50.          * @param SplObserver $oObserver
  51.          * @return void
  52.          */
  53.         public function attach(SplObserver $oObserver)
  54.         {
  55.                 $sHash = spl_object_hash($oObserver);
  56.                 if (isset($this->aObservers[$sHash])) {
  57.                         throw new Exception(‘Observer is already attached’);
  58.                 }
  59.  
  60.                 $this->aObservers[$sHash] = $oObserver;
  61.         }
  62.  
  63.         /**
  64.          * Detach observer
  65.          *
  66.          * @param SplObserver $oObserver
  67.          * @return void
  68.          */
  69.         public function detach(SplObserver $oObserver)
  70.         {
  71.                 $sHash = spl_object_hash($oObserver);
  72.                 if (!isset($this->aObservers[$sHash])) {
  73.                         throw new Exception(‘Observer not attached’);
  74.                 }
  75.                 unset($this->aObservers[$sHash]);
  76.         }
  77.  
  78.         /**
  79.          * Notify the attached observers
  80.          *
  81.          * @param string $sEvent, name of the event
  82.          * @param mixed $mData, optional data that is not directly available for the observers
  83.          * @return void
  84.          */
  85.         public function notify()
  86.         {
  87.                 foreach ($this->aObservers as $oObserver) {
  88.                         try {
  89.                                 $oObserver->update($this);
  90.                         } catch(Exception $e) {
  91.  
  92.                         }
  93.                 }
  94.         }
  95.  
  96.         /**
  97.          * Add an order
  98.          *
  99.          * @param array $aOrder
  100.          * @return void
  101.          */
  102.         public function delete()
  103.         {
  104.                 $this->notify();
  105.         }
  106.        
  107.         /**
  108.          * Return the order reference number
  109.          *
  110.          * @return int
  111.          */
  112.         public function getRef()
  113.         {
  114.                 return $this->iOrderRef;
  115.         }
  116.        
  117.         /**
  118.          * Return the current order status
  119.          *
  120.          * @return int
  121.          */
  122.         public function getStatus()
  123.         {
  124.                 return $this->iStatus;
  125.         }
  126.        
  127.         /**
  128.          * Update the order status
  129.          */
  130.         public function updateStatus($iStatus)
  131.         {
  132.                 $this->notify();
  133.                 // …
  134.                 $this->iStatus = $iStatus;
  135.                 // …
  136.                 $this->notify();
  137.         }
  138. }
  139.  
  140. /**
  141.  * Order status handler, observer that sends an email to secretary
  142.  * if the status of an order changes from shipped to delivered, so the
  143.  * secratary can make a phone call to our customer to ask for his opinion about the service
  144.  *
  145.  * @package Shop
  146.  */
  147. class OrderStatusHandler implements SplObserver
  148. {
  149.         /**
  150.          * Previous orderstatus
  151.          * @var int
  152.          */
  153.         protected $iPreviousOrderStatus;
  154.         /**
  155.          * Current orderstatus
  156.          * @var int
  157.          */
  158.         protected $iCurrentOrderStatus;
  159.        
  160.         /**
  161.          * Update, called by the observable object order
  162.          *
  163.          * @param Observable_Interface $oSubject
  164.          * @param string $sEvent
  165.          * @param mixed $mData
  166.          * @return void
  167.          */
  168.         public function update(SplSubject $oSubject)
  169.         {
  170.                 if(!$oSubject instanceof Order) {
  171.                         return;
  172.                 }
  173.                 if(is_null($this->iPreviousOrderStatus)) {
  174.                         $this->iPreviousOrderStatus = $oSubject->getStatus();
  175.                 } else {
  176.                         $this->iCurrentOrderStatus = $oSubject->getStatus();
  177.                         if($this->iPreviousOrderStatus === Order::STATUS_SHIPPED && $this->iCurrentOrderStatus === Order::STATUS_DELIVERED) {
  178.                                 $sSubject = sprintf(‘Order number %d is shipped’, $oSubject->getRef());
  179.                                 //mail(‘secratary@example.com’, ‘Order number %d is shipped’, ‘Text’);
  180.                                 echo ‘Mail sended to the secratary to help her remember to call our customer for a survey.’;
  181.                         }
  182.                 }
  183.         }
  184. }
  185.  
  186. $oOrder = new Order(26012011);
  187. $oOrder->attach(new OrderStatusHandler());
  188. $oOrder->updateStatus(Order::STATUS_DELIVERED);
  189. $oOrder->delete();
  190. ?>

There are several problems with the implementation above. To most important disadvantage is that we have only one update method in our observer. In this update method we don’t know when and why we are getting notified, just that something happened. We should keep track of everything that happens in the subject. (Or use debug_backtrace… just joking, don’t even think about using it that way ever!).

Taking it a step further, events
Lets take a look at the next example, we will extend the Observer implementation with some an additional parameter for the eventname that occured.

Finishing up, optional data

  1. <?php
  2.  
  3. /**
  4.  * Any object whose state may be of interest, and in whom another object may register an interest
  5.  *
  6.  * @package Interface
  7.  * @version $Id$
  8.  */
  9. interface Observable_Interface
  10. {
  11.         /**
  12.          * Attach, attaches an observer that will be notified on specific events
  13.          *
  14.          * @return void
  15.          */
  16.         public function attachObserver(Observer_Interface $oObserver);
  17.        
  18.         /**
  19.          * Detach, detach an observer
  20.          *
  21.          * @return boolean
  22.          */
  23.         public function detachObserver(Observer_Interface $oObserver);
  24. }
  25.  
  26. /**
  27.  * Any object that wishes to be notified when the state of another object changes
  28.  *
  29.  * @package Interface
  30.  * @version $Id$
  31.  */
  32. interface Observer_Interface
  33. {
  34.         /**
  35.          * Update, called by the observable on specific events
  36.          *
  37.          * @param object Observable implements Observable_Interface
  38.          * @param string Event, for example: "onBeforeAdd", "onBeforeAfter", "onAdd"
  39.          * @param mixed Data, optional data that can’t be retrieved directly from the Observable
  40.          * @return void
  41.          */
  42.         public function update(Observable_Interface $oObservable, $sEvent, $mData=null);
  43. }
  44.  
  45. /**
  46.  * Order class, handles adding, inserting, updating and deleting orders
  47.  *
  48.  * @package Shop
  49.  */
  50. class Order implements Observable_Interface
  51. {
  52.         // Order statuses
  53.         const STATUS_PAID = 1;
  54.         const STATUS_PICKED = 2;
  55.         const STATUS_SHIPPED = 3;
  56.         const STATUS_DELIVERED = 4;
  57.        
  58.         /**
  59.          * OrderRef
  60.          *
  61.          * @var int $iOrderRef
  62.          */
  63.         protected $iOrderRef;
  64.        
  65.         /**
  66.          * Status
  67.          *
  68.          * @var int $iStatus
  69.          */
  70.         protected $iStatus;
  71.        
  72.         /**
  73.          * List of attached observers
  74.          *
  75.          * @var array
  76.          */
  77.         private $aObservers = array();
  78.  
  79.         /**
  80.          * Constructor
  81.          *
  82.          * @param integer $iOrderRef
  83.          */
  84.         public function __construct($iOrderRef)
  85.         {
  86.                 if(!is_int($iOrderRef)) {
  87.                         throw new InvalidArgumentException(sprintf(‘Orderref should be of type integer type "%s" is given’, gettype($iOrderRef)));
  88.                 }
  89.                
  90.                 $this->iOrderRef = $iOrderRef;
  91.                
  92.                 // Get order information from the database or something else…
  93.                 $this->iStatus = Order::STATUS_SHIPPED;
  94.         }
  95.        
  96.         /**
  97.          * Attach an observer
  98.          *
  99.          * @param Observer_Interface $oObserver
  100.          * @return void
  101.          */
  102.         public function attachObserver(Observer_Interface $oObserver)
  103.         {
  104.                 $sHash = spl_object_hash($oObserver);
  105.                 if (isset($this->aObservers[$sHash])) {
  106.                         throw new Exception(‘Observer is already attached’);
  107.                 }
  108.  
  109.                 $this->aObservers[$sHash] = $oObserver;
  110.         }
  111.  
  112.         /**
  113.          * Detach observer
  114.          *
  115.          * @param Observer_Interface $oObserver
  116.          * @return void
  117.          */
  118.         public function detachObserver(Observer_Interface $oObserver)
  119.         {
  120.                 $sHash = spl_object_hash($oObserver);
  121.                 if (!isset($this->aObservers[$sHash])) {
  122.                         throw new Exception(‘Observer not attached’);
  123.                 }
  124.                 unset($this->aObservers[$sHash]);
  125.         }
  126.  
  127.         /**
  128.          * Notify the attached observers
  129.          *
  130.          * @param string $sEvent, name of the event
  131.          * @param mixed $mData, optional data that is not directly available for the observers
  132.          * @return void
  133.          */
  134.         public function notifyObserver($sEvent, $mData=null)
  135.         {
  136.                 foreach ($this->aObservers as $oObserver) {
  137.                         try {
  138.                                 $oObserver->update($this, $sEvent, $mData);
  139.                         } catch(Exception $e) {
  140.  
  141.                         }
  142.                 }
  143.         }
  144.  
  145.         /**
  146.          * Add an order
  147.          *
  148.          * @param array $aOrder
  149.          * @return void
  150.          */
  151.         public function add($aOrder = array())
  152.         {
  153.                 $this->notifyObserver(‘onAdd’);
  154.         }
  155.        
  156.         /**
  157.          * Return the order reference number
  158.          *
  159.          * @return int
  160.          */
  161.         public function getRef()
  162.         {
  163.                 return $this->iOrderRef;
  164.         }
  165.        
  166.         /**
  167.          * Return the current order status
  168.          *
  169.          * @return int
  170.          */
  171.         public function getStatus()
  172.         {
  173.                 return $this->iStatus;
  174.         }
  175.        
  176.         /**
  177.          * Update the order status
  178.          */
  179.         public function updateStatus($iStatus)
  180.         {
  181.                 $this->notifyObserver(‘onBeforeUpdateStatus’);
  182.                 // …
  183.                 $this->iStatus = $iStatus;
  184.                 // …
  185.                 $this->notifyObserver(‘onAfterUpdateStatus’);
  186.         }
  187. }
  188.  
  189. /**
  190.  * Order status handler, observer that sends an email to secretary
  191.  * if the status of an order changes from shipped to delivered, so the
  192.  * secratary can make a phone call to our customer to ask for his opinion about the service
  193.  *
  194.  * @package Shop
  195.  */
  196. class OrderStatusHandler implements Observer_Interface
  197. {
  198.         protected $iPreviousOrderStatus;
  199.         protected $iCurrentOrderStatus;
  200.        
  201.         /**
  202.          * Update, called by the observable object order
  203.          *
  204.          * @param Observable_Interface $oObservable
  205.          * @param string $sEvent
  206.          * @param mixed $mData
  207.          * @return void
  208.          */
  209.         public function update(Observable_Interface $oObservable, $sEvent, $mData=null)
  210.         {
  211.                 if(!$oObservable instanceof Order) {
  212.                         return;
  213.                 }
  214.                
  215.                 switch($sEvent) {
  216.                         case ‘onBeforeUpdateStatus’:
  217.                                 $this->iPreviousOrderStatus = $oObservable->getStatus();
  218.                                 return;
  219.                         case ‘onAfterUpdateStatus’:
  220.                                 $this->iCurrentOrderStatus = $oObservable->getStatus();
  221.                                
  222.                                 if($this->iPreviousOrderStatus === Order::STATUS_SHIPPED && $this->iCurrentOrderStatus === Order::STATUS_DELIVERED) {
  223.                                         $sSubject = sprintf(‘Order number %d is shipped’, $oObservable->getRef());
  224.                                         //mail(‘secratary@example.com’, ‘Order number %d is shipped’, ‘Text’);
  225.                                         echo ‘Mail sended to the secratary to help her remember to call our customer for a survey.’;
  226.                                 }
  227.                 }
  228.         }
  229. }
  230.  
  231. $oOrder = new Order(26012011);
  232. $oOrder->attachObserver(new OrderStatusHandler());
  233. $oOrder->updateStatus(Order::STATUS_DELIVERED);
  234. $oOrder->add();
  235. ?>

Now we are able to take action on different events that occur.

Disadvantages
Although this implementation works quite well there are some drawbacks. One of those drawbacks is that we need to dispatch an event in our framework, if we don’t programmers can’t hook into our application. Triggering events everywhere give us a small performance penalty however I do think this way of working gives the programmers a nice way to hook into your application on those spots that you want them to hook in.

Just for the record
Notice that this code is just an example and can still use some improvements, for example: each observer is initialized even it will maybe never be notified, therefore I suggest to make use of lazy in some cases for loading the objects. There are other systems to hook into an application, more to follow!

Written by Sjoerd Maessen

May 23rd, 2011 at 8:02 pm

Posted in API

Tagged with , , ,

3 Responses to 'PHP hook, building hooks in your application'

Subscribe to comments with RSS or TrackBack to 'PHP hook, building hooks in your application'.

  1. Hi Sjoerd,

    Looks good, nice way to use the SPL libraries.

    Cheers

    RobT

    24 May 11 at 7:47 am

  2. Oh yeah, I built a similar example without a real observer pattern which looks more like the evenlistener setup using in languages like Flex

    have a look at :

    http://sourcerer.nl/eventhandlers.phps

    RobT

    24 May 11 at 9:26 am

  3. Thx @RobT, your eventlistener example does indeed resembles a lot the way it works in Flex. Thx for contributing.

    Sjoerd Maessen

    24 May 11 at 9:45 am

Leave a Reply