Today's been an interesting day for me.
I found myself rereading this old blog entry from Jason Delmore from last year. It was about why Adobe doesn't give ColdFusion away for free like PHP or Ruby, which of course people still debate today although there's not really any reason to debate it anymore with there now being three separate free CFML engines in various stages of development (Railo, Open BlueDragon and SmithProject) and an upcoming CFML language standard committee.
Whatever your opinion on the idea of standards or for that matter committees, you can't argue that 2008 was anything but an eventful year for the ColdFusion community. Some of the news is just caching up to us like the announcement of the beta for the new Bolt IDE for ColdFusion. The community spoke and Adobe listened. Announcements regarding the features of CF9/Centaur (such as Hibernate ORM) are of course similarly exciting. And then there have been all the wow events in the community like Railo announcing their becoming part of the JBoss project and becoming free/open-source and Kristen Schofield announcing the free educational licensing for ColdFusion and releasing the evangelism kit that finally arms us with some great information for evangelizing the platform, allowing you to "be your own Ben Forta" as she described in her Max presentation. :)
These are all great things for the ColdFusion community, they will be great things for the CF Open Source community generally and personally I know they will be great things for the onTap framework community specifically as we grow rapidly over the next year or two.
ColdFusion is a bit unusual in the way it does things. Although it's not without its challenges as is the case with any language, CF has often been the first in new areas. Notably, CF was the first server of its kind to connect the web to databases seamlessly and easily over ten years ago when Allaire released the very first version. The onTap framework today is in some ways following in that pioneering spirit and right now I'd like to show you a few specific features that make it truly unique in today's CF open source community.
I had the pleasure recently of helping Eric Jones with a project he's working on called BeALight.com. They're working on a new member-driven application and with all his other commitments Eric didn't have time to put together the Proof of Concept (POC) even in spite of the fact that as applications go it wasn't very complicated. One of the big challenges with building applications in today's environment is that there are all these ancillary things that need to get done. You have to create a user-management system, you have to create a security framework, you have to create a data-access layer for talking to the database. All these things add up and at the end of the day, he just didn't have time to do it all over again for Be A Light at the moment. (And yes, a lot of shops have developed a "baseline" in their favorite framework to work with to speed up that process, but he didn't have one handy.)
So he posted an unusual request to the CF-Talk mailing list. Is there anyone out there who'd like to build the POC for this application gratis? That is... for free... unless of course you know how to pay your bills with kudos. ;)
If it were me, it would have to be something I really believe in for me to get the guts to post that kind of request. And I think it's pretty obvious that this is the case with Eric, that Be A Light is something he's passionate about, otherwise he'd have just let it slide. (There may be money for this project in the future, but there isn't any currently, so he's not getting paid right now either.)
If you read my last blog entry you know that something I'm passionate about is helping others. While I may not have the kind of religious convictions that Eric has (I'm Unitarian and I think he's Protestant, possibly evangelical), I'm grateful for the opportunity to help someone else in need. And while I enjoy being philanthropic, I also try to be shrewd, so you can bet that I planned to write this blog entry from day one. ;) So the Be A Light POC is actually helping me fulfill several of my goals. I'm helping people and at the same time I'm getting a great opportunity to evangelize the framework platform.
The onTap framework or one of its plugins already handle a number of the things described in the functional spec. A lot of the requested profile information for example name, bio and gender were already implemented in the Members onTap plugin. And so by installing that plugin from the web service, I was able to get much of the way to a working prototype in the first five minutes of development.
Some of the things described in the functional spec however were new and so I had to find a way to integrate those new ideas into the framework. They wanted to handle password retrieval with a secret-question/secret-answer pair (which you may have seen in some other applications), and they wanted several additional fields that would appear on the profile page.
Something I've tried to mention often in the community is the notion that in order to "future-proof" our code, we need to find a way to create applications that can be customized without editing the original application code. Why is this important? Well although I'm sure Eric will maintain Be A Light for a long time, I'm also willing to bet that there will be more future releases of the Members onTap plugin. If I had simply gone into the member form and started adding input fields at the bottom, that would be the "lazy way" of customizing his application. I would be trading the expediency of getting it done quickly and easily in exchange for ... leaving Eric TRAPPED IN TIME! Muahahahaha!!!!
Yes I'm being a little melodramatic to make my point. :) If I were to take the lazy way of customizing the application, I would be editing a file from the Members onTap application, and that would mean that the moment I change even one line of code for a new version anywhere in the plugin, Eric has a problem. Editing that template in his copy makes him out of sync with the plugin core, just like happens when you branch a project in your version-control application. So just like branching, we want to keep that to a minimum. Luckily for me the framework and its plugins save me a lot of time in other areas, which frees up a lot of extra time for these customizations.
Here I'll be showing you how I added Eric's customizations to the Members onTap plugin without changing a single line of the plugin code.
I started with the model. The plugin already has a "member" object with an associated profile view. So I should be able to use the existing form and the existing profile view from the plugin, I don't have to create new ones. The form and the view both make use of the same member.cfc object from the model, so that's where I started my integration.
In order to add our new properties to the member object without modifying the member.cfc directly, I need to use an object-oriented technique. Tada! Inheritance to the rescue. Inheritance lets us use the existing class properties and behavior and add our new features in addition. So I need to extend the member.cfc to create a custom component for our Be A Light project. So there's my first goal.
One of the things that helped me to achieve this first goal is the onTap framework's new IoC manager which manages multiple IoC factories for different plugins. So in this case the Members onTap plugin uses a simplified IoC factory class that's part of the onTap framework core. When you install the plugin, it creates a config file in /_tap/_config/ioc/membersontap.cfc. This is where the IoC container for the member plugin is declared. It looks like this:
<!--- requires the datafaucet container - this line may not be necessary,
but it shouldn't hurt anything, so I'll put it in here anyway --->
<cfset loadAfter("datafaucet") />
<cffunction name="configure" access="public" output="false" returntype="void">
<cfset newContainer("MembersOnTap").init("plugins.membersontap.iocfactory") />
Pretty simple isn't it? Components in the /_config/ioc/ directory are loaded when the application loads and they configure the IoC containers for individual plugins. So all the business objects for the Member plugin will be in the "MembersOnTap" container created there. The newContainer function creates a new container and allows me to initialize it with the path to my IoC factory which in this case is "plugins.membersontap.iocfactory". Then once the application is running any time I need a member object, I can get it from the MembersOnTap container... That's the simple answer - it actually involves one more object and that's the MemberFactory which is responsible for caching member objects. However, I get the MemberFactory from the IoC container, so ultimately all my business objects for the plugin are coming from that container.
In order for me to swap out the existing member.cfc with our new custom member component, I have to first swap out the MemberFactory object and to do that I have to first swap out the IoC container. Is it sounding complicated? It's not. In fact, by design it's VERY SIMPLE. :)
- New IoC Container
- New IoC Factory
- New Member Factory
- New Member CFC
I also know from the spec that the Be A Light project will need some other new business objects to represent messages between users and a few other things. So my plan here is to actually make the Be A Light IoC factory an extension of the MembersOnTap IoC factory and then use that as a substitute. This is beautiful encapsulation, because the rest of our application will have no idea that anything has changed. :)
I start by creating the config CFC for the Be A Light project in the same directory, /_tap/_config/ioc/bealight.cfc. That component looks like this:
<!--- we're going to override the members onTap IoC Factory with the Be A Light factory,
so we need to attach it after the plugin loads --->
<cfset loadAfter("membersontap") />
<cffunction name="configure" access="public" output="false" returntype="void">
<cfset var container = newContainer("BeALight").init("cfc.bealight.iocfactory") />
<!--- now we can attach our new Be A Light container also as the Members onTap container,
so this container will occupy 2 name-spaces in the manager, "bealight" and "membersontap"
-- "MembersOnTap" has effectively become an alias for "BeALight". --->
<cfset addContainer("MembersOnTap",container) />
You might notice that this config CFC looks rather similar to the one for the MembersOnTap plugin. At the top I tell it that it needs to load after the MembersOnTap config loads, so that when I call addContainer() at the bottom of my configure() method, it will override the original Members onTap IoC container with our new Be A Light container.
That's step 1 (IoC Container) done.
Then I need to create our Be A Light IoC factory. That file is in /_tap/_cfc/bealight/iocfactory.cfc and looks like this:
hint="defines the object model for Be A Light as an extension of the Members onTap plugin to allow some custom member profile information">
// we're overriding the member plugin member factory so that we can // seamlessly integrate the Be A Light profile properties with the member object // for that we need to override the member factory and the member gateway define("memberFactory","cfc.bealight.memberfactory");
define("guideGateway","cfc.bealight.guidegateway"); // this gateway is specifically for searching guides and will exclude seekers from the results define("conversationGateway","cfc.bealight.conversationgateway"); // fetches the subjects of conversations for a given member define("messageGateway","cfc.bealight.messagegateway"); // fetches the messages associated with a given conversation
define(beanName="conversation",beanClass="cfc.bealight.conversation",transient=true); // contains messages and sends emails define(beanName="message",beanClass="cfc.bealight.message",transient=true); // contains message content and nothing else </cfscript>
Pretty simple isn't it? At the top you can see that it extends the component "plugins.membersontap.iocfactory". If you remember from the first code sample, that's the path to the original IoC factory from the Members onTap plugin. So all we're doing here is extending that factory and we're declaring a few new business objects as well as overwriting the memberFactory and memberGateway objects from the member plugin with our new custom versions. I won't show code for the gateway object, but I assure you, it's as simple as these other files. :)
The IoC factory is so easy to tweak this way in part because it doesn't use an XML configuration file like ColdSpring. If it had used XML this would be a lot harder because I couldn't just extend the factory and overwrite those declarations. ColdSpring is also an option (as is LightWire) and there's a built-in ColdSpring adapter in the framework if you need or prefer it, but it wasn't needed for the member plugin so this keeps things simple. :)
That's step 2 (IoC Factory) done.
So with our new IoC factory for managing our business objects, we need our new custom memberFactory.cfc and member.cfc components. Remember that a custom member object is our ultimate goal, so that we can add the new properties needed to support the Be A Light project. For that we now create our custom memberFactory.cfc which you can see declared in the last code sample above as "cfc.bealight.memberfactory" and of course is located in /_tap/_cfc/bealight/memberfactory.cfc. It looks like this:
extends="cfc.membersontap.memberfactory" hint="provides object caching for member objects - overrides the default member factory from the Members onTap plugin">
<!--- this is all we need to do to override the default member class with our new class for members --->
<cfset classPath.member = "bealight.profile" />
Could it get much simpler? :) If I didn't have the foresight to set that as a private variable within the CFC this might have been a little more complicated, but I anticipated that some people might want to customize the member object, so this file is a cake walk. :)
That's step 3 (Member Factory) done.
In the last step we've declared where our new member object is located in "bealight.profile" we need to create our new member object. Normally this would be "cfc.bealight.profile", but the memberFactory was written to assume the "cfc" directory, so this file will also be in /_tap/_cfc/bealight/profile.cfc and looks like this:
<cfproperty name="profileJob" type="string" required="false" length="1000*" />
<cfproperty name="profileSchool" type="string" required="false" length="1000*" />
<cfproperty name="profilePast" type="string" required="false" length="1000*" />
<cfproperty name="profileWalk" type="string" required="false" length="1000*" />
<cfproperty name="profileHobbies" type="string" required="false" length="1000*" />
<cfproperty name="profileActivities" type="string" required="false" length="1000*" />
<cfproperty name="profileGroups" type="string" required="false" length="1000*" />
<cfproperty name="secretQuestion" tyupe="string" required="true" length="250*" />
<cfproperty name="secretAnswer" type="string" required="true" length="250*" />
<!--- all the properties declared here will be added in a 2nd table that's added to the tap_member table data,
making the merger between the Be A Light Profile and the default Member Object pretty seamless --->
<cfset addTable("bal_profile") />
<cffunction name="getLanguageTable" access="private" output="false" returntype="string">
<!--- in retrospect, the member object could have handled this function better --->
Viola! We have our custom member object. I was originally thinking that it might automatically install the new "bal_profile" table from this new CFC but that wasn't the case. (That's an area where I can improve DataFaucet later.) So I did have to create the new database table myself. However at this point member data for the Be A Light project will actually be stored in two separate tables, the tap_member table which was installed by the Member plugin and the bal_profile table we just created to hold these extra values. This contains (but does not eliminate) the threat of future name-space collisions.
The only snag here actually was that function at the bottom, the getLanguageTable() function that returns the name of the table where a member's spoken languages are stored. I discovered after I started testing that it couldn't find the table because the returned table name was wrong due to the customizations. It could have been better written probably, but for now all we need to resolve the issue is to override the function with the proper table name. A future version of the Members onTap plugin will of course remove this minor issue. :)
That's step 4 (member.cfc) the final step in our first leg of customization.
Okay, so the business objects are taken care of, we're half-way done. The remaining tasks are to modify our views and controllers so that the member form and the profile page will show our new properties. Again, the goal here is for every file we use to make these customizations to be what? NEW files. We're not allowed to edit any existing files. We can look, but we can't touch, because touching them is a cardinal sin! :) We do this to keep Eric from becoming TRAPPED IN TIME! Muahahahaha!
Fortunately the framework's templating system and XSL skins provide us with a simple way of accomplishing this task. We'll start with the form. We'll need to modify our controller to inject our XSL into the templating system. So we look in the Member plugin and we see that the member form is in /_tap/membersontap/member/form/. As with most of my applications, the bulk of the controller work is done in the /_local subdirectory. Here we find 100_skin.cfm and 200_form.cfm. This gives us a natural location to add our XSL for be a light between the skin declaration and the inclusion of the form. So we create a file named 150_bealight.cfm that looks like this:
member form tweaks for Be A Light are in the be a light skin directory
-- this provides support for the added member profile settings including secret question
<cf_translate file="#expandpath('/inc/bealight/memberform.txt')#" overwrite="false" />
<cfset arrayPrepend(request.tap.getHTML().skin.memberform,"bealight/memberform.xsl") />
Here we're prepending the "memberform" skin (which is an array of paths to XSL files) and we're adding our new XSL template for the Be A Light project. Above that you can see the CF_TRANSLATE custom tag, which we're using to read a new resource bundle containing the labels for the new input elements we're adding to the form. The resource bundle is the simpler of these two files - it's just there to hold localized strings so that the member plugin can be translated into different languages as needed. The bundle file which is located at /_tap/_includes/bealight/memberform.txt looks like this:
That's pretty normal for a resource bundle. The only thing that may stick out here is that I've prefixed all the variable names for my localized struings with %tap_. That's only necessary here because it's a naming convention I've used in the Member plugin to help prevent potential name-space collisions (and to make it obvious if I forgot to translate something).
The XSL sheet is a little more involved. I would still describe this as relatively easy, although I admit that many ColdFusion developers may not be as familiar with XSLT. Anyway, the XSL file is located at /_tap/_includes/skin/bealight/memberform.xsl and looks like this:
<xsl:output method="xml" indent="no" omit-xml-declaration="yes" />
<!-- copy everything in the source XML -->
<xsl:copy-of select="@*" />
<!-- add the secret question and secret answer inputs to the login tab -->
<xsl:copy-of select="@*" />
<input type="text" name="secretquestion" tap:required="true" />
<input type="text" name="secretanswer" tap:required="true" />
<!-- add the new profile input elements to the identity tab -->
<xsl:copy-of select="@*" />
<textarea name="profilejob" cols="50" rows="8" />
<textarea name="profileschool" cols="50" rows="8" />
<textarea name="profilepast" cols="50" rows="8" />
<textarea name="profilewalk" cols="50" rows="8" />
<textarea name="profilehobbies" cols="50" rows="8" />
<textarea name="profileactivities" cols="50" rows="8" />
<textarea name="profilegroups" cols="50" rows="8" />
You may notice that we're creating input elements and textareas here but we're not giving them any values. That's fine, the framework will actually populate all the form fields for us, so we don't have to worry about adding values to our input elements. Also since we added our new properties to the existing member objects, the framework will populate the values for us automatically from our custom member object.
Even on the back-end when the form is submitted it will also automatically update our new table that holds our custom properties because we customized the business object for members. Did you notice how many lines of SQL we edited to do that? Pretty close to zero wasn't it? In fact, it is zero. :)
So at this point, with just these three files shown above, the member form is fully customized! I'm going to skip showing the code for customizing the profile display or the sign-up form in part because this article is already pretty long, but also because the code is very similar to what I've already shown here. There are a couple more controller templates, a couple more XSL templates and one more resource bundle. But as is often the case with frameworks, the structure is basically the same.
So we achieved our goals. We added several properties to a custom member object, we modified the form so that users can edit those new properties and we internationalized the view. Most of the work was basically done for us by using the cfcomponent tag's extends attribute to inherit properties and behavior from components that already existed. Most importantly while doing all this we future-proofed the Be A Light application by ensuring that we created our customizations entirely in new files. As far as I know, the onTap framework is currently the only framework for ColdFusion that makes this kind of future-proofed customization possible.
There is still work to be done on this project and I know that I'll be involved in the continued development, maintenance and support. I'm really happy with how well the integration has turned out and I know I'll be proud of this site. In addition to what I feel is a pretty slick integration, this project was also done on a pretty short timeline. I had a little over a week to get it done and managed to get most of the items in the functional spec implemented. I must admit I ran behind because I added a little much of my own extra code for managing phone numbers, but that's really about me, not about the framework. There's an old saying "good, fast and inexpensive: pick two". This project showed that with the right libraries, it is possible to do all three. ;)
I'm looking forward to hearing your thoughts!