PHP Event Handling

I built the below to achieve some very basic event handling functionality. However, I would like to review some other as well. So if you would like to share your own interpretation and/or solution for handling events in PHP it would be appreciated. Pretty much just looking for some insight and discussion regarding handling events and classes designed to achieve such a task in PHP. I’m not really looking for anything just thrown together but solutions that have been clearly thought through for a particular requirement or application.

thanks


<?php 
/*
* Manages events 
*/
class EventHandler {
	
	private
	
	/*
	* Stack of object hashes with event keys 
	* and associated handlers to each event.
	*/
	$_arrEvents;
	
	public function __construct() {
		$this->_init();
	}
	
	private function _init() {
		$this->_arrEvents = array();
	}
	
	/*
	* Get objects unique foorprint for reference in events array 
	* 
	* @param obj object to get unique foot print for
	* @return str unique object foot print
	*/
	private function _getFootprint($obj) {
		return spl_object_hash($obj);
	}
	
	/*
	* Subscribe handler to event for object
	* 
	* @param obj listen to this object
	* @param str for this event
	* @param arr [obj,method] call this handler on event
	*/
	public function subscribe($objDispatcher,$strEvt,$arrHandler) {
		$strFootprint = $this->_getFootprint($objDispatcher);
		$this->_arrEvents[$strFootprint][$strEvt][] = $arrHandler; 
	}
	
	/*
	* Fire event
	* 
	* @param obj target
	* @param str event name
	*/
	public function fire($objTarget,$strEvt) {
		
		$strTarget = $this->_getFootprint($objTarget);
		
		if(isset($this->_arrEvents[$strTarget],$this->_arrEvents[$strTarget][$strEvt])) {
			
			/*
			* Call event listeners 
			*/
			foreach($this->_arrEvents[$strTarget][$strEvt] as $arrHandler) {
				array_shift($arrHandler)->{array_shift($arrHandler)}(array('target'=>$objTarget,'event'=>$strEvt));
			}
			
			/*
			* Bubble event 
			*/
			foreach($this->_arrEvents[$strTarget][$strEvt] as $arrHandler) {
				$this->fire(array_shift($arrHandler),$strEvt);
			}
		}
		
	}
	
}
?>

It’s late, but this sounds allot like Observer pattern

IMO, there isn’t a better Event Handler implementation out there that beats Symfony’s.

You can quickly browse the source over at Git.

As an aside, I’d be interested to know why you chose to code this segment the way you have…


    $_arrEvents;
    
    public function __construct() {
        $this->_init();
    }
    
    private function _init() {
        $this->_arrEvents = array();
    }

Why not assign the property directly in the constructor?

+1 for Symfony’s offering if you want something really robust with lots of support.

eZ Components SignalSlot

http://www.ezcomponents.org/docs/api/latest/introduction_SignalSlot.html

Also if you use a SplObjectStorage instead of an array, you can do away with the spl_object_hash() and use an object as an index directly.

I have yet to use symphony or even glance at the code, but I’ll take a look.

To form a separation between internal and external configuration of an object the constructor assigns parameters to properties and initiation method(s) configure internal components.

I would like to keep this functionality isolated and decoupled from the rest of the application code which is the reason for not using the observer pattern.

Nothing below 5.3 implements array access with SplObjectStorage which makes it impractical given the environment I’m currently working on.

This is interesting and bares many similarities with what I was doing. The signatures are similar to what I had envisioned on my own, although I like the grammar more.

Ah didn’t realise it was added later.

It was inspired from Qt Signal Slot…

http://doc.trolltech.com/4.6/signalsandslots.html

That makes absolutely no sense. The fact is in this implementation both the constructor and init are both useless methods. You can simply drop both methods and just have: private $event = array();

You can also join the “Call event listeners” and “Bubble event” foreach together.

Was bored and had nothing to do during the trip to class. So in my own style felt like writing out this event handler in my own way.


<?php

class EventHandler
{
  # $events {
  #   string dispatcher {
  #     string event {
  #       [#] {
  #         [0] object handler,
  #         [1] string method
  #       }
  #     }
  #   }
  # }
  protected $events = array();

  #-------------------------------------------------------------------------------
  # Public Interface

  # EventHandler ( object $dispatcher, string $event, array $handler )
  # throws Exception
  public function subscribe ( $dispatcher, $event, array $handler )
  {
    if ( !is_object( $handler[0] ) && !method_exists( $handler[0], $handler[1] ) )
      throw new Exception( "Invalid handler assigned." );

    $sig = $this->signature( $dispatcher );
    $this->events[ $sig ][ $event ][] = $handler;

    return $this;
  }

  # EventHandler ( object $target, string $event )
  public function fire ( $target, $event )
  {
    $sig = $this->signature( $target );

    if ( isset( $this->events[ $sig ][ $event ] ) ) {
      foreach ( $this->events[ $sig ][ $event ] as $handler ) {
        $handler[0]->{ $handler[1] }( array(
          'target' => $target, 'event'  => $event ) );

        $this->fire( $handler[0], $event );
      }
    }

    return $this;
  }

  #-------------------------------------------------------------------------------
  # Internal Interface

  # string ( object $object )
  protected function signature ( $object ) {
    return spl_object_hash( $object );
  }
}

This isn’t quite what you’re after but it may help… this is something I sometimes use for debugging… it works but it’s rather (totally) hacky (see: eval, static methods…)

It won’t work on php 5.2 because it uses namespaces but you could easily rework it to use class prefixes instead.

The reason this is good is it’s COMPLETLEY decoupled from implementation code.


<?
class Wrapper {
	public $object;
	private $eventHandler;
	
	public function __call($func, $args) {
		if (EventListener::triggerEvent($this, EventListener::BEFORE, $func, $args) !== false) {
			$return = call_user_func_array(array($this->object, $func), $args);
			EventListener::triggerEvent($this, EventListener::AFTER, $func, $args);
			return $return;
		}
	}
	
	public function __set($name, $value) {
		if (EventListener::triggerEvent($this, EventListener::BEFORE, EventListener::WRITE_PROPERTY, func_get_args()) !== false) {
			$this->object->$name = $value;
			EventListener::triggerEvent($this, EventListener::AFTER, EventListener::WRITE_PROPERTY, func_get_args());
		}		
	}
	
	public function __get($name) {
		if (EventListener::triggerEvent($this, EventListener::BEFORE, EventListener::READ_PROPERTY, func_get_args()) !== false) {
			$return = $this->object->$name;
			EventListener::triggerEvent($this, EventListener::BEFORE, EventListener::READ_PROPERTY, func_get_args());
			return $return;
		}
	}
} 

class EventListener {
	const READ_PROPERTY = '__get';
	const WRITE_PROPERTY = '__set';
	
	const BEFORE = 1;
	const AFTER = 2;
	
	public static $eventHandlers = array();
	
	public function listenTo($eventHandler, $object) {
		$hash = spl_object_hash($object);
		if (!isset(self::$eventHandlers[$hash])) self::$eventHandlers[$hash] = array();
		self::$eventHandlers[$hash][] = $eventHandler;		
	}
	
	public static function triggerEvent($object, $when, $event, $args) {
		$hash = spl_object_hash($object);
		foreach (self::$eventHandlers[$hash] as $eventHandler) {
			if ($eventHandler->handleEvent($event, $when, $args) === false){
				break;
				return false;
			}
		}
	}	
}



function __autoload($className) {
	$classDefinition = str_replace(array('<?php', '?>'), '', file_get_contents($className . '.php'));
	
	eval('namespace Implementation;
		 ' . $classDefinition);
	
	eval('class ' . $className . ' extends Wrapper {
	public function __construct() {
		$reflect = new ReflectionClass(\\'\\\\Implementation\\\\' . $className . '\\');
		$this->object = $reflect->newInstanceArgs(func_get_args());
	}
}');
	
}

interface EventHandler {
	public function handleEvent($event, $when, $args);	
}
?>

Implementation code:

First define a class we’re going to watch. Must be in its own file.

foo.php:


<?php 
class Foo {
	public function __construct() {
		
	}
	
	public function bar() {
		
	}
	
	public function test() {
		$this->bar();
	}	
}
?>


class FooEventHandler implements EventHandler {
	public function handleEvent($event, $when, $args) {
		switch ($when) {
			case EventListener::BEFORE: $whenText = 'before'; break;
			case EventListener::AFTER: $whenText = 'after'; break;
		}
		echo 'event trigered: ' . $whenText . ' ' . $event . ' with args: ';
		var_dump($args);
		echo '<br />';
	}
}

$eventListener = new EventListener;
$eventHandler = new FooEventHandler;

$foo = new Foo;
$eventListener->listenTo($eventHandler, $foo);

$foo->bar(1, 2, 3);
$foo->test();

Which prints:


event trigered: before bar with args: array(3) { [0]=>  int(1) [1]=>  int(2) [2]=>  int(3) }
event trigered: after bar with args: array(3) { [0]=> int(1) [1]=> int(2) [2]=> int(3) }
event trigered: before test with args: array(0) { }
event trigered: after test with args: array(0) { } 

There is no knowledge of the event handler in the class definition of Foo, which means any class can be easily watched.

As I said, totally hacky. Along with probably shockingly bad performance but it does work… and allows you to catch reading/writing of properties.

What it doesn’t catch is events fired within the object using $this, although doing a str_replace() on the class definition could fix that (and make it even more hacky!!)

@TomB, There is a bug in PHP 5.3.1 with references & __call(), http://bugs.php.net/bug.php?id=50394

Then it’s good php 5.3.2 was released yesterday :slight_smile:

I think I’d use a DI style container to deal with the decorating of objects, rather than __autoload.


interface Event
{
}

class Events
{
	protected $events;

	function __construct()
	{
		$this->events = array();
	}

	function fire(Event $event)
	{
		$class = get_class($event);
		if (isset($this->events[$class]))
			foreach($this->events[$class] as $fn)
				$fn($event);
	}

	function connect($event, Closure $fn)
	{
		if (isset($this->events[$event]))
			$this->events[$event][] = $fn;
		else
			$this->events[$event] = array($fn);
	}

	function connectInstance($event, $instance, $method)
	{
		$this->connect($event,
			function(Event $event) use ($instance, $method)
			{
				return $instance->$method($event);
			}
		);
	}

	function connectIndirect($event, Closure $getInstance, $method)
	{
		$this->connect($event,
			function(Event $event) use ($getInstance, $method)
			{
				return $getInstance()->$method($event);
			}
		);
	}
}

class EventA implements Event
{
}

interface A
{
	function onA(EventA $event);
}

class HelloWorld implements A
{
	function onA(EventA $event)
	{
		echo 'Hello World!', "\
";
	}
}

$container = new Container();
$events = new Events();

$container->registerShared('A', 'HelloWorld');    // wire container

$events->connectIndirect('EventA', $container->getClosure('A'), 'onA');

$events->fire(new EventA());

Container::getClosure($interface) returns a closure taking no parameters and returns an object implementing interface $interface.

Ok, I don’t fully understand that, but that won’t track events for the entire life of the object will it? They have to be fired by $events? Unless i’m missing something…

Better example?


class DataChangedEvent implements Event
{
	public $key;

	function __construct($key)
	{
		$this->key = $key;
	}
}

interface Cache
{
	function delete(DataChangedEvent $event);
}

class ApcCache implements Cache
{
	function delete(DataChangedEvent $event) { apc_delete($event->key); }
}

class Data
{
	protected $events;

	function __construct()
	{
		$this->events = new Events();
	}

	function getEvents() { return $this->events; }

	function change($key, $value)
	{
		$this->events->fire(new DataChangedEvent($key));
	}
}

$container = new Container();

$container->registerShared('Cache', 'ApcCache');

$data = new Data();
$data->getEvents()->connectIndirect('DataChangedEvent', $container->getClosure('Cache'), 'delete');

$data->change('a', 1);

That makes more sense, but it introduces the Events dependency in the Data object, which is what oddz was trying to avoid.

My method, while messy, allows you to add a hook to an arbitrary method in an arbitrary object, without changing the class definition.

Often it is better to have a list of pre-defined hookable events… however… that introduces the dependency which isn’t ideal imho, for example my method would allow for an incredibly powerful plugin system.

edit: My method would be a lot nicer if you could move classes between namespaces (or renamed) at runtime… allowing for classes which have been hooked to be replaced in the global namespace with the wrapper class and moved on the fly to the other namespace.