Wednesday, November 11, 2009

ORM Event Handling in ColdFusion 9

I've been playing around with ORM event handling in ColdFusion 9. For my application, I want to be able to fire events during various points in the lifecyle of my persistent entities. However, I don't want to have to clutter up my business logic with tasks such as logging, synchronizing integration data, or pushing changes out to Flex. I want to take a more AOP-style approach.

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... :)

3 comments:

  1. Hi dude,
    i read your blog,This is a wonderful blog.I was able to get the
    information that I had been looking for. Thanks once again.

    ColdFusion Downloads

    ReplyDelete
  2. Useful information like this one must be kept and maintained so I will put this one on my bookmark list! Thanks for this wonderful post and hoping to post more of this!

    Online Reputation Management Company & Best PPC Service Provider in India

    ReplyDelete