Trac

Event System

The Event System will be centered around the EventManager? and a base Event class that both reside in a Common namespace since they're used for DBAL+ORM. DBAL & ORM will have specific Event classes as needed.

EventManager?

#namespace Doctrine::Common;

class Doctrine_EventManager
{
    private $_listeners = array();

    public function dispatchEvent($event) {
        $argIsCallback = is_string($event);
        $callback = $argIsCallback ? $event : $event->getType();
        
        if (isset($this->_listeners[$callback])) {
            $event = $argIsCallback ? new Doctrine_Event($event) : $event;

            foreach ($this->_listeners[$callback] as $listener) {
                $listener->$callback($event);
            }
        }

        return ! $event->getDefaultPrevented();
    }
    
    public function getListeners($callback = null) {
        return $callback ? $this->_listeners[$callback] : $this->_listeners;
    }
    
    public function hasListeners($callback) {
        return isset($this->_listeners[$callback]);
    }
    
    public function registerEventListener($callbacks, $listener) {
        // TODO: maybe check for duplicate registrations?
        if (is_array($callbacks)) {
            foreach ($callbacks as $callback) {
                $this->_listeners[$callback] = $listener;
            }
        } else {
            $this->_listeners[$callbacks] = $listener;
        }
    }
}

Event

#namespace Doctrine::Common;

class Doctrine_Event
{
    /* Event callback constants */
    const preDelete = 'preDelete';
    const postDelete = 'postDelete';
    //...more


    protected $_type;

    protected $_target;

    protected $_defaultPrevented;


    public function __construct($type, $target = null)
    {
        $this->_type = $type;
        $this->_target = $target;
        $this->_defaultPrevented = false;
    }


    public function getType()
    {
        return $this->_type;
    }


    public function preventDefault()
    {
        $this->_defaultPrevented = true;
    }


    public function getDefaultPrevented()
    {
        return $this->_defaultPrevented;
    }


    public function getTarget()
    {
        return $this->_target;
    }
}  

Example of internal code that fires events:

namespace Doctrine::ORM;
class EntityManager {
    private $_eventManager;
    
    public function getEventManager() {
        return $this->_eventManager;
    }
    
    // some method that fires an event. the EM has lots of those.
    public function save(Entity $entity) {
        // example of an event that has no special event object.
        // this simplifies dispatching down to a one-liner  
        if ( ! $this->_eventManager->dispatchEvent(Event::preSave)) {
            return;
        }
        
        // ... code ...
        
        // alternatively separate method, maybe better & cleaner when a
        // custom event object is involved that must be populated with context
        // information
        if ( ! $this->_postSave()) {
            return;
        }
    }
    
    private function _postSave(..params if necessary..) {
        // example of an event with a custom event object that carries
        // context information.
        if ($this->_eventManager->hasListeners(Event::postSave)) {
            $event = new PostSaveEvent(.. stuff ..);    
            return $this->_eventManager->dispatchEvent($event);
        }

        return true;
    }
    
    //...
}

// example of other classes that trigger events
namespace Doctrine::ORM..
class Query {
    //...
    public function execute(...) {
        // no custom event object
        if ( ! $this->_eventManager->dispatchEvent(Event::preExecute)) {
            return;
        }
        
        // ... code ...
        
        // custom event object
        if ($this->_em->getEventManager()->hasListeners(Event::postExecute)) {
            if ( ! $this->_eventManager->dispatchEvent(new PostExecuteEvent(..stuff for the event..))) {
                return; // These IFs could be merged, if desired.
            }
        }
    }
}

Defining targets

namespace Doctrine::ORM..
class Record {
    //...
    public function save(...) {     
        // custom event object
        if ($this->_em->getEventManager()->hasListeners(Event::postExecute)) {
            // Creating custom event
            $event = new Doctrine_Event('preSave', $this);

            if ( ! $this->_eventManager->dispatchEvent($event) {
                return;
            }
        }

        // ... code ...
    }
}

USER CODE, listener registration

$eventManager = $em->getEventManager();
$myDeleteListener = new MyDeleteListener();
$eventManager->registerEventListener($myDeleteListener, array(Event::preDelete, Event::postDelete));

The listener:
class MyDeleteListener {
   // note that we can use a strict type hint! (IDE users will love it)
   public function preDelete(PreDeleteEvent $event) {
       //...
   }
   
   public function postDelete(PostDeleteEvent $event) {
       //...

       // example of cancelling an event
       $event->preventDefault();
   }
}

# CONSEQUENCES:
a) central repository for all listeners (EventManager?)
b) minimum amount of boilerplate code in event triggering code while keeping good performance
c) NO Listener interfaces & adapter classes needed AT ALL (= a lot less classes, =no empty method implementations etc.)
d) adding new event callbacks is easy (add constant) and does not break user code (as opposed to adding new methods to interfaces)
e) event listener classes dont need to extend a class or implement an interface! they just implement the appropriate methods!
f) Already supports custom events and callbacks out of the box. That means, i.e. someone writes a Behavior for doctrine that triggers an event he can very easily use the Event system to do that with no work and without touching the Doctrine source.

Entity Lifecycle Listeners & Callbacks

Example:

class User extends Entity {
    // Mapping metadata setup
    public static funtion initMetadata($mapping) {
        //...
        $mapping->addLifecycleCallback('validateCreate', Event::prePersist);
        $mapping->addLifecycleListener('MyNamespace::UserListener',
                array('validateCreate' => Event::prePersist));
        //...
    }

    // The callback, must be public
    public function validateCreate() {
        // do stuff
    }
}

// The listener
namespace MyNamespace;
class UserListener {
    public function validateCreate(User $user) {
        // do stuff
    }
}