Event driven PHP development

Abstract

Dispatching and listening to events is a common concept with many advantages in a lot of applications but is hardly used nor natively supported in PHP. In this article I present you my own, very simple yet powerful implementation mimicking JavaScripts event bubbling on DOM-elements.

Introduction

I stumbled across a nice article on phphatesme.com lately which made me think about event based programming in PHP. In jQuery or almost any of the Google Javascript-APIs, at least, events and event listeners are a concept widely spread, used and accepted.

I did some more research regarding event based concepts in PHP and found that symfony components has (as usual) its own solution which is quite similar to what phphatesme.com developed. A closer look showed that although working nicely both solutions seemed a bit limited in their possibilities because both allow listeners to be attached to an exactly named event only. So what I had in mind was more or less an event tree, behaving very similar to browser event bubbling.

Components of an event dispatcher in PHP

Class "Event"

As I tend to use object oriented programming whereever possible I started with a very small class as a skeleton for all types of events. An event only consists of a "name" property and an array of "parameters", both private. The magic method __get is used to the otherwise private properties publicly readable only.

class Event {
    private $name;
    private $parameters = array();

    public function __construct($name, array $parameters = array()) {
        $this->name       = $name;
        $this->parameters = $parameters;
    }

    public function __get($property) {
        if(isset($this->$property)) {
            return $this->$property;
        }
    }
}

Class "Dispatcher"

Afterwards I started building the skeleton for the so called dispatcher which is used to bind listeners to events and notify them when an event is triggered.

class Dispatcher {
    private $callbacks = array();
    private $events    = null;

    public function __construct() {  }
    public function addListener($callback, $event) {  }
    public function notify(Event &$event) {  }
}

If you read carefully you might have noticed the word "tree" and this is exactly what will be used throughout the class to store and manage all events. So the class constructor will initialize the private property "events" to be a SimpleXMLElement:

class Dispatcher {
    ...

    public function __construct() {
        $this->events = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><events></events>');
    }

    ...
}

After that I continued with the "addListener" method. Notice the parameter "$event" which is a string in this case and uses "/" as a separator for the tree/path structure.

class Dispatcher {
    ...

    public function addListener($callback, $event) {
        $return = false;

        if(is_callable($callback) === true) {
            if(is_array($callback)) {
                $id = md5(get_class($callback[0]) . json_encode($callback));
            } else {
                $id = md5(json_encode($callback));
            }

            $matches =& $this->_getEvent($event);

            foreach($matches as $match) {
                $status = $match->xpath('./listener[@id=\'' . $id . '\']');
                $status = ($status === false || count($status) === 0) ? false : true;

                if($status === false) {
                    $listener       = $match->addChild('listener');
                    $listener['id'] = $id;

                    unset($listener);
                }
            }

            if(!isset($this->callbacks[$id])) {
                $this->callbacks[$id] =& $callback;
            }

            $return = true;
        }

        return $return;
    }

    // fetches all existing events matching the passed event name
    private function &_getEvent($node) {
        $matches = $this->events->xpath('/events/' . $node . '[not(@id)]');

        if($matches === false || count($matches) === 0) {
            $matches = $this->_build($node);
        }

        return $matches;
    }

    // adds an event to the class events property and returns it as a pointer
    private function _build($node) {
        $nodes   =  explode('/', $node);
        $pointer =& $this->events;

        foreach($nodes as $node) {
            if(!isset($pointer->$node)) {
                $pointer->addChild($node);
            }

            $pointer =& $pointer->$node;
        }

        return array($pointer);
    }

    ...
}

Being able to add listeners from here on only one thing is missing: a method to trigger an event or, to be precise, notify all registered listeners that an event was triggered.

class Dispatcher {
    ...

    public function notify(Event &$event) {
        $return = false;

        if($this->events !== null) {
            $matches    =& $this->_getEvent($event->name);
            $expression =  '/events/' . $event->name . '[not(@id)]/listener[@id]|/events/' . $event->name . '[not(@id)]/ancestor::*/listener[@id]';
            $matches    =  $this->events->xpath($expression);

            if(is_array($matches)) {
                foreach($matches as $match) {
                    call_user_func_array($this->callbacks[(string) $match['id']], $event->parameters);
                }
            }

            $return = true;
        }

        return $return;
    }

    ...
}

Practical usage example

Usage of the event dispatcher is really simple, just to give you an example:

$dispatcher = new Dispatcher();
$dispatcher->addListener('exampleCallback', 'example')

// trigger the event directly...
    $event = new Event('example', array('exampleParam1', 'exampleParam2'));
    $dispatcher->notify($event);

// or make use of "bubbling"
    $event = new Event('example/subevent', array('exampleParam1', 'exampleParam2'));
    $dispatcher->notify($event);

function exampleCallback($exampleParam1, $exampleParam2) {
    echo $exampleParam1 . ', ' . $exampleParam2;
}