Event Listening plugin design, round four!

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Event Listening plugin design, round four!

Post by Chris Corbyn »

I do this differently everytime I write this damn thing! :P

For those who don't know, I write a library who's plugin system allows modification of "events" prior to those events occuring (e.g. a message being sent, a response being received from a remote server etc). The last version of this created new objects for every single event, even if that was just to send a short command (4 characters) to a remote server a completely new CommandEvent would be instantiated and relayed around a list of observers (EventListeners) who could listen for a CommandEvent and modify it accordingly or simple take note on it without modifying it.

This is in my eyes today, just a bit overkill and too heavy for something observer-ish.

Since I'm rewriting this (surprisingly rather trivial, drop-in near the end) little part of my library I'm trying to make it a bit lighter and a bit more flexible.

Things which were not so good in the old design:

* Too heavy
* There were desires for their to be the ability to "cancel" and event, but this was not implemented

So I'm trying a new approach this time.... more of a filter chain. The Event objects are not needed since they were merely wrappers, and the ability to cancel an event is trivial (break the chain).

I'm looking for critique from somebody with brains really :P So here's my mock-up of how it would work. Any thoughts?

Code: Select all

<?php
 
error_reporting(E_ALL | E_STRICT);
 
/**
 * A pretty standard FilterChain interface.
 */
interface EventListenerChain {
  /**
   * Filter $object through a list of Filters.
   * @param string $eventType
   * @param object $object
   * @return object
   */
  public function handleEvent($eventType, $object);
}
 
/**
 * A container for storing and running EventListeners.
 */
interface EventDispatcher extends EventListenerChain {
  /**
   * Add a new EventListener.
   * @param EventListener $listener
   */
  public function addEventListener(EventListener $listener);
  /**
   * Dispatch an event of $eventType to relevant EventListeners.
   * The original object, or a (compatible) modification of it should be returned.
   * @param string $eventType
   * @param object $object
   * @return object
   */
  public function dispatchEvent($eventType, $object);
}
 
/**
 * A pretty standard Filter interface.
 */
interface EventListener {
  /**
   * Filter $object and run the next filter in the FilterChain.
   * @param string $eventType
   * @param object $object
   * @param FilterChain $chain
   */
  public function handleEvent($eventType, $object, EventListenerChain $chain);
}
 
/**
 * A simple EventListener demonstration.
 */
class FooListener implements EventListener {
  public function handleEvent($eventType, $object, EventListenerChain $chain) {
    $chain->handleEvent($eventType, $object);
  }
}
 
/**
 * A simple EventListener demonstration.
 */
class BarListener implements EventListener {
  public function handleEvent($eventType, $object, EventListenerChain $chain) {
    if ('command' == $eventType) {
      $object .= 'blah';
    }
    $chain->handleEvent($eventType, $object);
  }
}
 
/**
 * A simple event dispatcher demonstration.
 */
class SimpleDispatcher implements EventDispatcher {
  private $_queue = array();
  private $_used = array();
  private $_result;
  
  public function handleEvent($eventType, $object) {
    if ($listener = array_shift($this->_queue)) {
      $this->_used[] = $listener;
      $listener->handleEvent($eventType, $object, $this);
    } else {
      $this->_result = $object;
    }
  }
  
  public function addEventListener(EventListener $listener) {
    $this->_queue[] = $listener;
  }
  
  public function dispatchEvent($eventType, $object) {
    $this->_result = null;
    while ($listener = array_pop($this->_used)) {
      array_unshift($this->_queue, $listener);
    }
    $this->handleEvent($eventType, $object);
    return $this->_result;
  }
}
 
$dispatcher = new SimpleDispatcher();
$dispatcher->addEventListener(new FooListener());
$dispatcher->addEventListener(new BarListener());
 
echo $dispatcher->dispatchEvent('command', 'abc');
//abcblah
EDIT | Hmm... it seems that rather have the EventDispatcher "return" the filtered value, it should probably invoke some method on it's caller which puts it at the end of the chain.

Code: Select all

$dispatcher->dispatchEvent('command', 'abc', $this);
//whichever object calls it passes itself ready for the event result to be received
My original code had the intention of checking for empty return values if an event was cancelled but that logic is flawed since there may sometimes be valid reasons for empty values.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: Event Listening plugin design, round four!

Post by Chris Corbyn »

Ok, changed it according to my edit. Terminology change too. The event is "bubbled" now, not "filtered".

Code: Select all

<?php
 
error_reporting(E_ALL | E_STRICT);
 
/**
 * The end of a chain of EventListeners.
 */
interface EventEndpoint {
  /**
   * Runs when all EventListeners have been executed.
   * @param string $eventType
   * @param object $object
   */
  public function eventReceived($eventType, $object);
}
 
/**
 * A bubble-chain of events.
 */
interface EventBubble {
  /**
   * Bubble $object through a list of EventListeners.
   * @param object $object
   */
  public function bubble($object);
}
 
/**
 * A container for storing and running EventListeners.
 */
interface EventDispatcher extends EventBubble {
  /**
   * Add a new EventListener.
   * @param EventListener $listener
   */
  public function addEventListener(EventListener $listener);
  /**
   * Dispatch an event of $eventType to relevant EventListeners.
   * The original object, or a (compatible) modification of it should be returned.
   * @param string $eventType
   * @param object $object
   * @param EventEndpoint $endpoint
   * @return object
   */
  public function dispatchEvent($eventType, $object, EventEndpoint $endpoint);
}
 
/**
 * An EventListener which receives an event as it bubbles up a chain.
 */
interface EventListener {
  /**
   * Filter $object and continue bubbling.
   * @param string $eventType
   * @param object $object
   * @param EventBubble $bubble
   */
  public function handleEvent($eventType, $object, EventBubble $bubble);
}
 
/**
 * A simple EventListener demonstration.
 */
class FooListener implements EventListener {
  public function handleEvent($eventType, $object, EventBubble $bubble) {
    $bubble->bubble($object);
  }
}
 
/**
 * A simple EventListener demonstration.
 */
class BarListener implements EventListener {
  public function handleEvent($eventType, $object, EventBubble $bubble) {
    if ('whatever' == $eventType) {
      $object .= 'def';
    }
    $bubble->bubble($object);
  }
}
 
/**
 * A simple event dispatcher demonstration.
 */
class SimpleDispatcher implements EventDispatcher {
  private $_queue = array();
  private $_used = array();
  private $_eventType;
  private $_endpoint;
  
  public function bubble($object) {
    if ($listener = array_shift($this->_queue)) {
      $this->_used[] = $listener;
      $listener->handleEvent($this->_eventType, $object, $this);
    } else {
      $this->_endpoint->eventReceived($this->_eventType, $object);
    }
  }
  
  public function addEventListener(EventListener $listener) {
    $this->_queue[] = $listener;
  }
  
  public function dispatchEvent($eventType, $object, EventEndpoint $endpoint) {
    $this->_eventType = $eventType;
    $this->_endpoint = $endpoint;
    while ($listener = array_pop($this->_used)) {
      array_unshift($this->_queue, $listener);
    }
    $this->bubble($object);
  }
}
 
class Something implements EventEndpoint {
  private $_dispatcher;
  
  public function __construct(EventDispatcher $dispatcher) {
    $this->_dispatcher = $dispatcher;
  }
  
  public function whatever($foo) {
    $this->_dispatcher->dispatchEvent('whatever', $foo, $this);
  }
  
  public function eventReceived($eventType, $object) {
    if ('whatever' == $eventType) {
      echo 'Whatever! ' . $object;
    }
  }
}
 
$dispatcher = new SimpleDispatcher();
$dispatcher->addEventListener(new FooListener());
$dispatcher->addEventListener(new BarListener());
 
$something = new Something($dispatcher);
$something->whatever('abc');
//Whatever! abcdef
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Re: Event Listening plugin design, round four!

Post by Ambush Commander »

I haven't looked over the code carefully enough, but is the code inspired by JavaScript's event handling mechanisms? (If not, you might want to look at it; it sounds like a rip-and-steal of the interface (with all the bad browser-incompatibility stuff stripped out) would suite things very nicely).
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: Event Listening plugin design, round four!

Post by Chris Corbyn »

I think it may be slightly different in that the object which tells the dispatcher to dispatch an event is the last to receive it after it bubble up all the event listeners. I believe in JS, the event occurs and the bubbles up all the event listeners.

I guess the MDC would be a good place to snoop over their event handling API however, that's true :)
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Re: Event Listening plugin design, round four!

Post by Ambush Commander »

Well, not really. Since a web browser is an asynchronous environment, there is no "object" per se that dispatches an event. The event occurs, the browser creates the appropriate event object, and then sends it off to all eligible event handlers in the proper order (which is complicated due to the browser-wars, and something you probably don't need to worry about).
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: Event Listening plugin design, round four!

Post by Chris Corbyn »

Actually, my only reason for wanting to avoid Event objects is because instantiation of objects is slower than desirable in PHP (I haven't even benchmarked what the impact of this would be in something which has to deal with network latency which is blatently more of an issue). But since they're simple objects and the number of events is limited, I wonder if a "prototype factory" using __clone() on little event objects would combat such overhead.

I'll do some benchmarking later since passing actual events (wit a common API) to actual listeners is definitely cleaner. It means I can hide the fact it's a chain too by adding an $evt->cancelBubble() method which is handled somewhere else.

EDIT | Missed your reply; thanks for the link :) Will read when I get to work... running a tad late :oops:
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Re: Event Listening plugin design, round four!

Post by Ambush Commander »

Prototype factory works well, in fact, we've used this pattern successfully in HTML Purifier (however, making sure large data-structures were passed by reference did a much better job at reducing memory usage).
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Re: Event Listening plugin design, round four!

Post by Chris Corbyn »

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.
Post Reply