Building a PHP Publish / Subscribe System

First post in a while. I have been busy working on some large application and finishing up my Computing Science Degree. Now that the piece of paper is safely secured above my fireplace, I’ve been trying to find time to do a new post.

I have been building a large application and one of the requirements is to trigger a certain action when an event happens. For example, trigger the sending of an alert email when a database row is updated. Some programmers may be tempted to simply hard-code this functionality into the model class, however this doesn’t give very strict class encapsulation, and can quickly become un-maintainable.

I use the Dojo Javscript toolkit for most of my dynamic front-end functionality. It has implemented a publish/subscribe system using dojo.publish() and dojo.subscribe(). This article will describe the implementation of a similar system using PHP.

Overview

The concept I wanted to implement consists of three parts:

  1. The Listener
This is a class that is interested in the events that are thrown by some other class. Keeping with the above example, tihs class may implement the functionality of sending the alert emails when the database row is updated
  1. The Event
To give the listener some data to act on, an event (message) is passed from the triggering object to the listener. This event may contain information such as the column values for the updated row.
  1. The Dispatcher
The dispatcher is the glue that holds everything together. When an object wants to trigger and event it passes it to the dispatcher _(publishes it)_. When a listener class wants to listen for an event, it will ask the dispatcher to inform it when the event of a specified type is passed (_subscribe_).

Some other food for thought:

  • A listener may only want to subscribe to an event that is published by a certain resource, so there must be a way to track who published the event
  • A listener may want to subscribe to all events that are published by a resource or to a specific event independent of the resource, so wildcards should be permitted

Implementation

The Event Object

The event object, in its simplicity, is a very basic class. As mentioned above, it needs to track a minimum of:

  1. The resource that published the event
  2. The name (/type) of the event
  3. Some (optional) data to be passed to the listener

A very simple example of an event object may be the following:

class Event {
    /**
     * The name of the resource publishing this event
     * @var string
     */
    public $resourceName;

    /**
     * The name of this event
     * @var string
     */
    public $eventName;

    /**
     * Any data associated with this event
     * @var mixed
     */
    public $data;

    /**
     * @param string $resourceName  name of the publisher
     * @param string $eventName     name of the event
     * @param mixed $data           [OPTIONAL] Additional event data
     */
    public function __construct($resourceName, $eventName, $data=null)
    {
        $this->resourceName = $resourceName;
        $this->eventName = $eventName;
        $this->data = $data;
    }
}

Depending on your goals, you may want to make $resourceName, $eventName, and $data private attributes and use getters / setters. Additionally, if you do not wish to pass the eventName and, rather, use object inheritance you could implement the event object in either of the following ways:

Eg 1: Manually set the event name.

class UpdateEvent extends Event
{
    public $eventName = 'Update';

    public function __construct($resourceName, $data=null)
    {
        $this->resourceName = $resourceName;
        $this->data = $data;
    }
}

Eg 2: Automatically parse the event name from the class name

class UpdateEvent extends Event
{
    public function __construct($resourceName, $data=null)
    {
        parent::__construct($resourceName, get_class($this), $data);
    }
}

The Listener Object

The Listener object is also has a very simple job. It simply accepts an event object and does something with it. The only catch is that all listener objects should have the same method which accepts the event. Because of this, it makes sense to use an interface.

interface ListenerInterface
{
    /**
     * Accepts an event and does something with it
     *
     * @param Event $event  The event to process
     */
    public function publish(Event $event);
}

I am going to implement a very simple listener class for the purpose of this demo. This listener outputs a string saying what event was fired, and for which resource. EG: published a .

class EchoListener implements ListenerInterface
{

    public function publish(Event $event)
    {
        echo "{$event->resourceName} published a {$event->eventName}";
    }
}

The Dispatch Object

The dispatch object is by far the most complex of all the components. It handles both the subscription of listeners and the dispatching of events. For my actual implementation I needed more than one backend for my dispatch object (Both a hard-coded memory based one, and a user-defined backend based on mysql). For simplicities sake I am going to demonstrate only the memory-based backend here, and leave the implementation of multiple-backends as an exercise to the user.

Without further adeau, here is the class in its entirety. I will discuss each part individually below:

class Dispatcher {

    /**
     * Associative array of listeners.
     * Indicies are: [resourceName][event][listener hash]
     *
     * @var array
     */
    protected $_listeners = array();

    /**
     * Subscribes the listener to the resource's events.
     * If $resourceName is *, then the listener will be dispatched when the specified event is fired
     * If $event is *, then the listener will be dispatched for any dispatched event of the specified resource
     * If $resourceName and $event is *, the listener will be dispatched for any dispatched event for any resource
     *
     * @param Listener $listener
     * @param String $resourceName
     * @param Mixed $event
     * @return Dispatcher
     */
    public function subscribe(Listener $listener, $resourceName='*', $event='*'){
        $this->_listeners[$resourceName][$event][spl_object_hash($listener)] = $listener;
        return $this;
    }

    /**
     * Unsubscribes the listener from the resource's events
     *
     * @param Listener $listener
     * @param String $resourceName
     * @param Mixed $event
     * @return Dispatcher
     */
    public function unsubscribe(Listener $listener, $resourceName='*', $event='*'){
        unset($this->_listeners[$resourceName][$event][spl_object_hash($listener)]);
        return $this;
    }

    /**
     * Publishes an event to all the listeners listening to the specified event for the specified resource
     *
     * @param Event $event
     * @return Dispatcher
     */
    public function publish(Event $event ){
        $resourceName = $event->resourceName;
        $eventName = $event->eventName;

        //Loop through all the wildcard handlers
        if(isset($this->_listeners['*']['*'])){
            foreach($this->_listeners['*']['*'] as $listener){
                $listener->publish($event);
            }
        }

        //Dispatch wildcard Resources
        //These are events that are published no matter what the resource
        if(isset($this->_listeners['*'])){
            foreach($this->_listeners['*'] as $event =>; $listeners){
                if($event == $eventName){
                    foreach($listeners as $listener){
                        $listener->publish($event);
                    }
                }
            }
        }

        //Dispatch wildcard Events
        //these are listeners that are dispatched for a certain resource, despite the event
        if(isset($this->_listeners[$resourceName]['*'])){
            foreach($this->_listeners[$resourceName]['*'] as $listener){
                $listener->publish($event);
            }
        }

        //Dispatch to a certain resource event
        if(isset($this->_listeners[$resourceName][$eventName])){
            foreach($this->_listeners[$resourceName][$eventName] as $listener){
                $listener->publish($event);
            }
        }

        return $this;
    }
}

Listeners Attribute

This associative array tracks all the listeners that are subscribed to events. It supports wildcard resources and wildcard events, allowing for a listener to be subscribed to a certain event and resource, all events of a certain resource, all events of a certain type, or all events across all resources. By having an object hash as the final key, well permit multiple listeners to be subscribed to a single event, while still maintaining the unsubscribe functionality (assuming the original object is still present).

Subscribe Function

The subscribe function subscribes a listener to either a specific event for a specific resource and event pairing, or some combination of wildcards.

Unsubscribe Function

Unsubscribes a listener from a resource’s event or wildcard.

Publish Function

The publish function is the workhorse of the Dispatcher class. Every time an event is pubished, the dispatcher searches through the listener array, looking first for any global wildcards (listeners that should be informed on any event), then resource wildcards (listeners that are subscribed to a certain event across all resources), then event wildcards (listeners that are subcribed to all events of a certain resource), and finally events that are subscribed to only one resource’s event.

Putting It Together

Now that we have our three components built, it is time to put it all together. First setup the dispatcher and subscribe some listeners to a resource’s event. Next, publish the event to the dispatcher and, fingers crossed, the event should be propogated to your listeners.

$dispatcher = new Dispatcher();
$echoListener = new EchoListener();
$dispatcher->subscribe($echoListener, 'fooResource', 'barEvent');

//Should output "fooResource published a barEvent"
$dispatcher->publish(new Event('fooResource', 'barEvent'));

Well that was a lot of writing. There are several ways to extend this model to make it a bit more elegant and efficient (such as having the publisher of an event pass an instance of itself – this is especially useful for database models, having the subscribe rules dynamically loaded based on the resource name via, say, Zend_Plugin_Loader, having the resource name automatically filled in in much the same way that the event name can be populated); however, I hope this has given you a starting point from which to implement events in your application.

If you have any questions / find spelling, grammar or coding mistakes please mention them in the comments.