Page 1 of 3
event plugin system
Posted: Sun Mar 01, 2009 8:34 pm
by Luke
I've decided to kind of de-centralize my application framework a bit and allow events to be triggered which plugins can hook into. So far, it's gone very well. Basically, I set up a singleton class called PluginManager which allows me to "trigger" events as well as register plugins. It is an implementation of the observer pattern. The original reason I decided to implement this was because I have a lot "side-effect" functionality. I wanted to keep my controllers very thin. It also allows me to easily turn on and off functionality via an admin panel. The only issue I've had so far is that I am not entirely sure how I should provide contextual information to my plugins. For instance, I have a plugin called "SystemEmails" which sends out e-mails to the user and the admin when somebody registers, cancels their account, etc.
For instance, here is my registration action. After a successful registration, it triggers the "Register" event. The problem is, the SystemEmails plugin needs to have the user's information, which at present, isn't possible.
Code: Select all
/**
* Register Action
*/
public function registerAction() {
$post = array();
$errors = array();
if ($this->isPost()) {
$post = $this->getPost();
$rules = with($this->usrTab->getRules())
->add(new ImpSoft_Rule_FieldAvailable(array('table' => $this->usrTab, 'column' => 'username')), 'username', 'Sorry, that username is already taken')
->add(new ImpSoft_Rule_FieldAvailable(array('table' => $this->usrTab, 'column' => 'email')), 'email', 'Sorry, that e-mail is already taken. Perhaps you already have an account?');
if ($rules->validate($post)) {
try {
$this->usrTab->registerMember(array(
'username' => $post['username'],
'password' => $post['password'],
'email' => $post['email'],
'role' => 'member',
'is_active' => 0
));
$this->getPluginManager()->trigger('Register');
$this->flash('Thank you for signing up! We have sent you an e-mail with an activation link. Once you click that link, your account will be active.');
$this->goToRoute('home');
} catch (Zend_Db_Exception $e) {
// cannot create the user at this time... bummer!
$this->flash('Sorry, we can not allow you to register at this time.');
$this->goToRoute('register');
}
} else {
$errors = $rules->getErrors();
}
}
$this->view->post = $post;
$this->view->errors = $errors;
}
Here is the SystemEmails plugin
Code: Select all
<?php
class ImpSoft_Plugin_SystemEmails extends ImpSoft_Plugin_Abstract {
/**
* @var $swift Swift object
*/
protected $swift;
protected $from;
public function __construct(Swift $swift, $from) {
$this->from = $from;
$this->swift = $swift;
}
public function onRegister() {
$message = new Swift_Message("Welcome!", "Welcome to the Is It Legit wonderness. Good luck.");
$recipient = ""; // this is the contextual information I need...
if ($this->swift->send($message, $this->from, $recipient));
}
}
There is of course, the obvious solution. I could require that contextual information is passed to the "trigger" method as a second argument. I'm just not sure if that is the best way to go about it. I mean, how would I know what information each plugin will need? There's really no way to know. I'm wondering if maybe a registry is a better way. If anybody has any advice, please... let me know

Re: event plugin system
Posted: Sun Mar 01, 2009 10:07 pm
by Chris Corbyn
The way I've handled this sort of thing is to create an "Event object" that contains information associated with that event. You could do it simply by passing an associatve array I guess but using an EventObject can have some examples as things move forward (for example, you may at some point decide your plugins should be able to modify an event or cancel it).
I have a simple EventObject interface:
Code: Select all
interface EventObject {
getSource(); //Object (origin of event)
cancelBubble($cancel = true);
bubbleCancelled(); //Boolean
}
Each Event adds its own methods for contextual data beyond the basics though. It sounds like you don't need the complexity that something like bubble-cancelling offers but using an event object to dispatch the event can help provide the data you need.
Code: Select all
$this->getPluginManager()->dispatchEvent(new RegistrationEvent( ... ));
Re: event plugin system
Posted: Sun Mar 01, 2009 10:47 pm
by Luke
Ok... yea, that sounds like it would suit my needs perfectly. I'll play around with it a bit, thanks Chris
EDIT: Chris, would you mind elaborating a bit on the "bubbling"? I am just curious what exactly that means. I have read about event bubbling before, but I have yet to really understand it.
EDIT (again): also, what exactly would getSource() return? The class name? File name?

Re: event plugin system
Posted: Sun Mar 01, 2009 10:56 pm
by Chris Corbyn
I try to associate listener interfaces with Event types. So that way your "listener", or your plugin in other words can type-hint on the implementation it's expecting so it knows what methods will be in the event:
Code: Select all
interface RegistrationListener {
public function registrationCreated(RegistrationEvent $evt);
}
Now when you create a plugin that needs to respond to a RegistrationEvent it just needs to declare this in its list of implemented interfaces:
Code: Select all
class MyPlugin implements RegistrationListener {
public function registrationCreated(RegistrationEvent $evt) {
$account = $evt->getCreatedAccount(); //Just an example
// .. do stuff ..
}
}
This adds complexity to the plugin manager however since it now needs to know which plugins to invoke based on the evtn type given. I still haven't come up with a way that I'm 100% happy with but what I do is list all of the Event types along with the interfaces they expect listeners on. Knowing the method name you need to invoke is the messy bit. Plugins may respond to more than one type of event so distinct listener methods help to make this possible.
Re: event plugin system
Posted: Mon Mar 02, 2009 2:09 am
by Luke
Wow... I really like that. Dude, I'm going to have fun playing with this idea. Thanks

Re: event plugin system
Posted: Mon Mar 02, 2009 2:39 am
by Weirdan
Just don't overuse your event subsystem. I worked on a project where it got to the point where people started using events for everything, even events like form-create to inject additional fields to the form. It can get very messy.
Re: event plugin system
Posted: Mon Mar 02, 2009 4:17 am
by Chris Corbyn
Weirdan wrote:Just don't overuse your event subsystem. I worked on a project where it got to the point where people started using events for everything, even events like form-create to inject additional fields to the form. It can get very messy.
Agreed. Events should be something that happen as a side-effect of something else. I have points in Swift Mailer where I check for bubble cancelling (in beforeEventPerformed() methods) but that's for specific use-cases. Plugins can get really over-used... I have to work on code occassionally (an eCommerce system) as part of my job where the whole development team find it laughable just how bad it's gotten... it's almost forced to make everything a plugin of this larger system since that seemed like a good idea 6 years ago or whenever the original author wrote it and the architecture just isn't there to do things another way
@Luke, I missed your edits before asking about the event bubbling...
Basically events start somewhere and then "bubble" up through all event listeners until there are no more listeners to send the event to. Bubbling typically refers to the stack on which the listeners exist. Your dispatcher (an Observable object) has to loop through the listener stack (the list of plugins/event listeners) so it has the control to break that loop and prevent the event from being sent to any more listeners (bubble cancelled).
Easier to see in code:
Code: Select all
function dispatchEvent(EventObject $evt) {
$applicableListeners = $this->_findListenersFor($evt);
while (!$evt->bubbleCancelled() && $listener = array_shift($applicableListeners)) {
$listener->handleEvent($evt); //If the listener calls $evt->cancelBubble(true); then this loop breaks
}
}
I think I just solved a problem whilst writing this thread. I've always found it hard to know where to put the logic that decides:
a) Which event listener interfaces apply to each event type
b) What method name is supposed to be invoked in those listeners.
This seems obvious now, but I think the solution may be to add this logic to the EventObject interface:
Code: Select all
abstract class EventObject {
private $_bubbleCancelled = false;
private $_source;
abstract public function invokeListener($listener);
public function __construct($source) {
$this->_source = $source;
}
public function cancelBubble($cancelled = true) {
$this->_bubbleCancelled = $cancelled;
}
public function bubbleCancelled() {
return $this->_bubbleCancelled;
}
public function getSource() {
return $this->_source;
}
}
So now the event dispatcher doesn't need to know (or care) what interfaces apply or what methods to call. I may rewrite some of my own code after this post
An event Object now looks like this:
Code: Select all
class RegistrationEvent extends EventObject {
private $_newAccount;
public function __construct($source, $newAccount) {
$this->_newAccount = $newAccount;
}
public function getNewAccount() {
return $this->_newAccount;
}
public function invokeListener($listener) {
if ($listener instanceof RegistrationListener) {
$listener->registrationCreated($this); //This logic is very much tied to the event/listener marriage
}
}
}
And the dispatcher becomes simplified:
Code: Select all
while (!$evt->bubbleCancelled() && $listener = array_shift($listeners)) {
$evt->invokeListener($listener);
}
That was a lot of thinking out loud!
EDIT | Oh yeah and getSource() might not apply in your system really. I have that method in mine since event always originate within a specific component (some object... usually a Transport object in Swift Mailer). getSource() just returns the origin of the event. You can throw that away and add specific methods though.
Re: event plugin system
Posted: Mon Mar 02, 2009 5:22 am
by josh
Large event systems need event channels as well. Certain parts of the system listen only on certain channels, channels can then be prioritized, used for error checking making it easier to work with / debug, and the same "event name" can refer to a different type of even on different channels. For instance onPayment for a subscription might trigger different events then onPayment for a regular product. For leight use cases you could just pass a flag in the event object that says what kind of payment it was though.
I second what weirddan said, take a look at Magento, they use events for _everything_. All one a "global channel". Makes it pointless because noone knows what event listeners to override.
Re: event plugin system
Posted: Mon Mar 02, 2009 8:43 pm
by Luke
Chris, thank you. This is really really great. This allows flexibility my original plan couldn't even come close to. I hope you don't mind, but I plan on outlining this plugin system on my blog. Of course, I don't mind mentioning that the functionality that originally made me want to build the plugin system was for system e-mails powered by swiftmailer and when it came time to get advice on how to implement it, the author helped me figure it out

Re: event plugin system
Posted: Tue Mar 03, 2009 5:40 am
by Chris Corbyn
Luke wrote:Chris, thank you. This is really really great. This allows flexibility my original plan couldn't even come close to. I hope you don't mind, but I plan on outlining this plugin system on my blog. Of course, I don't mind mentioning that the functionality that originally made me want to build the plugin system was for system e-mails powered by swiftmailer and when it came time to get advice on how to implement it, the author helped me figure it out

Go for it. No credit needed, I didn't invent anything new

Re: event plugin system
Posted: Tue Mar 03, 2009 3:25 pm
by Weirdan
Chris Corbyn wrote:
This seems obvious now, but I think the solution may be to add this logic to the EventObject interface:
Looks like implementation of visitor pattern (where visited object selectively accepts visitors)... maybe it's called something else, but it's certainly double dispatch here.
Re: event plugin system
Posted: Tue Mar 03, 2009 3:58 pm
by Chris Corbyn
Yeah it's certainly along those lines.
Re: event plugin system
Posted: Wed Mar 04, 2009 10:58 pm
by Christopher
Chris and Luke, how do you see an events system working with a standard controller based framework? Is this a completely independent system that would be accessed statically, or could an events system be integrated into the controllers in some way?
Re: event plugin system
Posted: Thu Mar 05, 2009 3:49 am
by Chris Corbyn
arborint wrote:Chris and Luke, how do you see an events system working with a standard controller based framework? Is this a completely independent system that would be accessed statically, or could an events system be integrated into the controllers in some way?
I've only ever done it with Swift Mailer which is kinda separate. I don't see why it couldn't work at a controller level provided your event dispatcher is globally accessible (via a registry?). It's basically a big machine for firing events around a stack of listeners so at its core it doesn't matter too much who or what decides to dispatch an event.
However, I'm not sure event dispatching is something that should be controller-level code in any case (unless it's an interface for something like request-filtering:
Code: Select all
$dispatcher->dispatchEvent(new RequestEvent($request));
////
interface RequestListener {
function requestReceived(RequestEvent $request);
}
I think there are better ways to do that sort of thing though (just use a filter chain).
Events are more the kind of thing that would happen for specific changes in the model, like Luke's user registration, or a forum thread being deleted/created.
Like I say, I've never done this in anything but a library though. We use observers on our domain object mappers for notifications about CRUD events so I guess this could be employed in place of that.
For me, a system like this is more about exposing a reasonably flexible interface for people to hook into an application as a whole, rather than a way to solve individual implementation-level problems such as those that occur during the flow of logic in a controller.
Re: event plugin system
Posted: Thu Mar 05, 2009 2:52 pm
by Luke
What I did was create a singleton object that manages the events. It is accessible in my controllers by calling $this->getEventManager(). There are still some kinks to work out, but I'll get there.
I'll post more when I get it figured out.