It appears that I'm definitely using a "bubble" approach.
I've mulled this over even more and have come up with something which is a bit of a combination of Java/AWT event handling, JavaScript event handling, and a bit of custom behaviour.
I've used a prototype factory inside the event dispatcher so that once an event has been created the first time it's just cloned each time (using a custom clone method which allows some injection to private properties).
This one is a lot like the one in V3, except for the centralized dispatcher, the ability to cancel bubbling and the ability to bind a listener to specific event source.
I've broken my "no type hinting on class names" rule here under the idea that an event is about as concrete as an exception.
--------------------------------------------------------------------
EventDispatcher
Code: Select all
/**
* Interface for an EventDispatcher for the sake of Dependency Injection
*/
interface EventDispatcher {
/**
* Create the event for the given event type.
* @param string $eventType
* @param object $source
* @param string[] $properties the event will contain
*/
public function createEvent($eventType, $source, array $properties);
/**
* Bind an event listener to this dispatcher; optionally restricted to the given event source.
* @param EventListener $listener
* @param object $source, optional
*/
public function bindEventListener(EventListener $listener, $source = null);
/**
* Dispatch the given Event to all suitable listeners.
* @param Event $evt
* @param string $target method
*/
public function dispatchEvent(Event $evt, $target);
}
--------------------------------------------------------------------
Event
Code: Select all
/**
* The base class for all events.
* I don't mind this being a conrete implementation in the same way Exceptions are.
*/
class Event {
/**
* The event source.
* @var object
*/
private $_source;
/**
* The state of the event (true if bubbling was cancelled).
* @var boolean
*/
private $_bubbleCancelled = false;
/**
* Get the event source.
* @return object
*/
public function getSource() {
return $this->_source;
}
/**
* Cancel event bubbling (once called this event will never bubble further up the stack).
* @param boolean $cancel
*/
public function cancelBubble($cancel = true) {
$this->_bubbleCancelled = $cancel;
}
/**
* Check if the event bubble has been burst.
* @return boolean
*/
public function bubbleCancelled() {
return $this->_bubbleCancelled;
}
/**
* Make a new Event for the given $source.
* @param object $source
* @return Event
*/
public function cloneFor($source) {
$evt = clone $this;
$evt->_source = $source;
return $evt;
}
}
--------------------------------------------------------------------
SendEvent
Code: Select all
/**
* An example event (called when send operations are performed).
*/
class SendEvent extends Event {
/** Example of what an event may contain */
const STATE_PENDING = 0x001;
const STATE_SUCCESS = 0x010;
const STATE_FAILED = 0x100;
public $message;
public $state;
public $failedRecipients = array();
/**
* Get the message which is being sent.
*/
public function getMessage() {
return $this->message;
}
/**
* Get recipients which couldn't be delivered to.
*/
public function getFailedRecipients() {
return $this->failedRecipients;
}
/**
* Get the state of this event.
*/
public function getState() {
return $this->state;
}
}
--------------------------------------------------------------------
EventListener
Code: Select all
/**
* Nothing more than an identity for all EventListeners.
*/
interface EventListener {
}
--------------------------------------------------------------------
SendListener
Code: Select all
/**
* An example Listener interface for the example SendEvent.
*/
interface SendListener extends EventListener {
/**
* Handle the event before sending is performed.
* @param SendEvent $evt
*/
public function beforeSendPerformed(SendEvent $evt);
/**
* Handle the event once sending has been performed.
* @param SendEvent $evt
*/
public function sendPerformed(SendEvent $evt);
}
--------------------------------------------------------------------
SimpleEventDispatcher
Code: Select all
/**
* What an implementation of an EventDispatcher may look like.
* Of course, there would only be one of these.
*/
class SimpleEventDispatcher implements EventDispatcher {
private $_eventMap = array();
private $_listenerMap = array();
private $_listeners = array();
private $_prototypes = array();
private $_bubbleQueue = array();
/**
* Create a new EventDispatcher with knowledge of the given events in the classMap.
* @param string[] $classMap
*/
public function __construct(array $classMap) {
foreach ($classMap as $type => $spec) {
$this->_eventMap[$type] = $spec['event'];
$this->_listenerMap[$spec['event']] = $spec['listener'];
}
}
/**
* Bubbles an event up the stack, calling $target on each listener.
*/
private function _bubble(Event $evt, $target) {
if (!$evt->bubbleCancelled() && $listener = array_shift($this->_bubbleQueue)) {
$listener->$target($evt);
$this->_bubble($evt, $target);
}
}
/**
* Creates a stack of event listeners for the given event.
*/
private function _prepareBubbleQueue(Event $evt) {
$this->_bubbleQueue = array();
$evtClass = get_class($evt);
foreach ($this->_listeners as $listener) {
$instance = $listener['instance'];
if (array_key_exists($evtClass, $this->_listenerMap)
&& ($instance instanceof $this->_listenerMap[$evtClass])
&& (!$listener['source'] || $listener['source'] === $evt->getSource())) {
$this->_bubbleQueue[] = $instance;
}
}
}
/**
* Create an event of $type for $source with $properties.
*/
public function createEvent($type, $source, array $properties) {
if (!array_key_exists($type, $this->_prototypes)) {
$class = $this->_eventMap[$type];
$this->_prototypes[$type] = new $class();
}
$evt = $this->_prototypes[$type]->cloneFor($source);
foreach ($properties as $key => $value) {
$evt->$key = $value;
}
return $evt;
}
/**
* Bind an event listener to this dispatcher; optionally to the given $source.
*/
public function bindEventListener(EventListener $listener, $source = null) {
$this->_listeners[] = array('instance' => $listener, 'source' => $source);
}
/**
* Dispatch an event to all listeners $target method.
*/
public function dispatchEvent(Event $evt, $target) {
$this->_prepareBubbleQueue($evt);
$this->_bubble($evt, $target);
}
}
-----------------------------------------------------------------
And finally, a rough example of how it would go together:
Code: Select all
/**
* An example of a class which may use an EventDispatcher.
*/
class ExampleEventSource {
private $_dispatcher;
public function __construct(EventDispatcher $dispatcher) {
$this->_dispatcher = $dispatcher;
}
public function addEventListener(SendListener $listener) {
$this->_dispatcher->bindEventListener($listener, $this);
}
public function send($message) {
$evt = $this->_dispatcher->createEvent('send', $this, array('message'=>$message));
$this->_dispatcher->dispatchEvent($evt, 'beforeSendPerformed');
if (!$evt->bubbleCancelled()) {
echo "Message sent!\n"; //whatever!
$this->_dispatcher->dispatchEvent($evt, 'sendPerformed');
} else {
echo "Event bubble cancelled!\n";
}
}
}
/**
* An Example implementation of the example SendListener.
*/
class ASendListener implements SendListener {
public function beforeSendPerformed(SendEvent $evt) {
echo "Listener A beforeSendPerformed()\n";
}
public function sendPerformed(SendEvent $evt) {
echo "Listener A sendPerformed()\n";
}
}
/**
* An example implementation of the example Send listener.
*/
class BSendListener implements SendListener {
public function beforeSendPerformed(SendEvent $evt) {
}
public function sendPerformed(SendEvent $evt) {
echo "Listener B sendPerformed()\n";
}
}
//And it looks a bit like this!
//I can't really think of the best way to abstract such knowledge of types
$dispatcher = new SimpleEventDispatcher(
array('send' => array('event' => 'SendEvent', 'listener' => 'SendListener'))
);
$source = new ExampleEventSource($dispatcher);
$source->addEventListener(new ASendListener());
$source->addEventListener(new BSendListener());
$source->send("abc");
The above outputs:
Code: Select all
Listener A beforeSendPerformed()
Message sent!
Listener A sendPerformed()
Listener B sendPerformed()
If I change that first send listener to cancelBubble() on the event:
Code: Select all
/**
* An Example implementation of the example SendListener.
*/
class ASendListener implements SendListener {
public function beforeSendPerformed(SendEvent $evt) {
echo "Listener A beforeSendPerformed()\n";
$evt->cancelBubble();
}
public function sendPerformed(SendEvent $evt) {
echo "Listener A sendPerformed()\n";
}
}
Then it outputs:
Code: Select all
Listener A beforeSendPerformed()
Event Bubble cancelled!
I like it all, apart from the fact my send() method has to manually check if bubbleCancelled() returns true before continuing, although that's a fairly specific use case.