To do this, I created an XML-driven EventManager to use as my application's global event handler. With my EventManager, I'm able to configure listeners to respond to events fired by my entities using the built in ORM event handlers. The events can be registered using regular expressions and all follow the pattern {EntityName}:{EventHandler}. For example, I might have User:postUpdate or Product:postDelete.
In my sample application, I've configured my EventManager to send an email before and after any entity is loaded, inserted, updated, or deleted. In other words, basically anytime something happens to an entity. I'm not sure how useful this would be, but it's just an example.
Here's the directory structure:
Application.cfc
component {
this.name = "Sample";
this.datasource = "Sample";
this.mappings["/sample"] = getDirectoryFromPath(getCurrentTemplatePath());
this.ormEnabled = true;
this.ormSettings.dbcreate = "update";
this.ormSettings.eventHandling = true;
this.ormSettings.eventHandler = "sample.com.EventHandler";
public void function onApplicationStart() {
application.beanFactory = createObject("component","coldspring.beans.DefaultXmlBeanFactory").init();
application.beanFactory.loadBeans("/sample/config/coldspring.xml");
}
}
EventHandler.cfc
component implements="cfide.orm.IEventHandler" {
public void function preLoad(any entity) {
handleEvent(entity,"preLoad");
}
public void function postLoad(any entity) {
handleEvent(entity,"postLoad");
}
public void function preInsert(any entity) {
handleEvent(entity,"preInsert");
}
public void function postInsert(any entity) {
handleEvent(entity,"postInsert");
}
public void function preUpdate(any entity, struct oldData) {
handleEvent(entity,"preUpdate");
}
public void function postUpdate(any entity) {
handleEvent(entity,"postUpdate");
}
public void function preDelete(any entity) {
handleEvent(entity,"preDelete");
}
public void function postDelete(any entity) {
handleEvent(entity,"postDelete");
}
private void function handleEvent(any entity, string handler) {
var collection = {};
collection.entity = ormGetSession().getEntityName(entity);
collection.id = entity.getID();
collection.handler = handler;
var eventManager = application.beanFactory.getBean("eventManager");
eventManager.dispatchEvent("#collection.entity#:#collection.handler#",collection);
}
}
EventManager.cfc
component accessors="true" {
property configPath;
public any function init() {
variables.events = {};
variables.loaded = false;
}
public void function setConfigPath(required string configPath) {
if(fileExists(configPath)) {
variables.configPath = configPath;
}
else {
variables.configPath = expandPath(configPath);
}
}
public void function dispatchEvent(required string event, struct data) {
if(!structKeyExists(arguments,"data")) {
arguments.data = {};
}
if(!variables.loaded) {
loadConfig();
variables.loaded = true;
}
local.listeners = getListeners(event);
for (var i=1;i <= arrayLen(local.listeners);i++) {
local.bean = application.beanFactory.getBean(local.listeners[i].bean);
evaluate("local.bean.#local.listeners[i].method#(argumentCollection=data)");
}
}
private array function getListeners(required string event) {
if(!structKeyExists(variables.events,event)) {
local.used = {};
local.listeners = [];
for (var i=1;i <= arrayLen(variables.config);i++) {
if(reFindNoCase(variables.config[i].name,event)) {
for (var j=1;j <= arrayLen(variables.config[i].listeners);j++) {
local.listener = variables.config[i].listeners[j];
if(!structKeyExists(local.used,local.listener.id)) {
arrayAppend(local.listeners,local.listener);
local.used[local.listener.id] = true;
}
}
}
}
variables.events[event] = local.listeners;
}
return variables.events[event];
}
private void function loadConfig() {
variables.config = [];
local.xml = xmlParse(fileRead(getConfigPath()));
for (var i=1;i <= arrayLen(local.xml.events.xmlChildren);i++) {
local.event = {};
local.event.name = local.xml.events.xmlChildren[i].xmlAttributes.name;
local.event.listeners = [];
for (var j=1;j <= arrayLen(local.xml.events.xmlChildren[i].xmlChildren);j++) {
local.listener = {};
local.listener.bean = local.xml.events.xmlChildren[i].xmlChildren[j].xmlAttributes.bean;
local.listener.method = local.xml.events.xmlChildren[i].xmlChildren[j].xmlAttributes.method;
local.listener.id = local.listener.bean & "." & local.listener.method;
arrayAppend(local.event.listeners,local.listener);
}
arrayAppend(variables.config,local.event);
}
}
}
NotficationService.cfc
component {
public void function sendNotification() {
savecontent variable="local.body" {
writeDump(arguments)
}
var notification = new Mail();
notification.setTo("joe@example.com");
notification.setFrom("joe@example.com");
notification.setSubject("Notification");
notification.setType("html");
notification.send(body=local.body);
}
}
coldspring.xml
<beans>
<bean id="eventManager" class="sample.com.EventManager">
<property name="configPath">
<value>/sample/config/events.xml</value>
</property>
</bean>
<bean id="notificationService" class="sample.com.NotificationService" />
</beans>
events.xml
<events>
<event name="[\w]+:(pre|post)(Load|Insert|Update|Delete)">
<listener bean="notificationService" method="sendNotification" />
</event>
</events>
I'm not sure if this is the best approach or how well this would scale, but it seems to work pretty well for now.
PS - note the use of savecontent inside script. And they said it was pointless... :)