Very simple, very ugly, CMS built with ColdFusion 9
Earlier on in the ColdFusion 9 beta, I worked on a simple CMS (Content Management System) that made use of ORM. I've messed with it every now and then over the past few months and spent some time today making it a bit nicer so I could share the code with others. I may, stress may turn this into a real project, but I have no real intention of trying to compete with Mura or Farcry. This was just for fun, and just to get some experience with ORM. Let me also add that as it's been worked on for a few months now, you may see some code that doesn't quite make sense. So for example, earlier on in the ColdFusion 9 alpha, there was no isNull. Today I replaced code like this:
2<cfif isDefined("section")>
with
2<cfif not isNull(section)>
You may see stuff like that in the code, so please keep in mind that this isn't "best practice" ColdFusion 9 code. With that out of the way, let me talk a bit about the architecture.
Simple CMS works with a simple model. The model is so simple I'm going to paste it all here. First is our template object:
2
3 property name="id" generator="native" sqltype="integer" fieldtype="id";
4 property name="name" ormtype="string";
5 property name="header" ormtype="text";
6 property name="footer" ormtype="text";
7
8}
Templates consist of an ID, a name, and a header and a footer. Next up is the section:
2
3 property name="id" generator="native" sqltype="integer" fieldtype="id";
4 property name="name" ormtype="string";
5 property name="sitedefault" ormtype="boolean";
6 property name="order" ormtype="integer";
7
8}
Sections consist of a name, a sitedefault property, and an order. The sitedefault property is simply a way to mark a section as the default section of a web site. Like a home page section for example. Order is used for ordering sections for display. More on that later. The last part of our model is the page:
2
3 property name="id" generator="native" sqltype="integer" fieldtype="id";
4 property name="title" ormtype="string";
5 property name="body" ormtype="text";
6
7 property name="section" fieldType="many-to-one" cfc="section" fkcolumn="sectionidfk";
8 property name="template" fieldType="many-to-one" cfc="template" fkcolumn="templateidfk";
9
10 property name="isHomePage" datatype="boolean";
11
12 public string function renderMe() {
13 return template.getHeader() & body & template.getFooter();
14 }
15
16}
Finally - a bit of complexity! Pages consist of a title, a body, and a related section and template. Lastly - a isHomePage property works much like the section siteDefault property. It is a way to say "if you request a section without a page, this is the one to load." Oh, and I've got a method to render the page. As you can see, it gets the template and wraps the body.
So how does the CMS work? There is one more CFC called cms. This component acts as a main controller for all CMS actions. When you come to the application with nothing in the URL (but the path to the application), it tries to find a default section and a page marked as a home page for that section. The application will nicely handle the lack of either of these values by showing a simple message. If you go to the application with a path in the URL: /cmsalpha/products/index.cfm, then it looks for a section named products and a home page object. Lastly, if you go to /cmsalpha/products/foo.cfm, it will look for a page named foo inside the products section.
The application makes use of onMissingMethod in Application.cfc to handle requests. Unfortunately, this means you can't do: /cmsalpha/products/. You must supply a full path like so: /cmsalpha/products/index.cfm. But that's a small trade off for a simple proof of concept. (You could always use a server side rewriter to handle this too.) Anyway, here is th ecode from Application.cfc:
2
3 try {
4 var page = application.cms.getPage(arguments.pageRequested);
5 } catch(any e) {
6 //If the error code is 1, it's reflects the lack of a default section, which we handle nicely
7 if(e.errorCode == 1) location("#application.cms.getCMSURL()#/notready.cfm");
8 //not safe to assume .., will fix later
9 if(e.errorCode == 2) location("#application.cms.getCMSURL()#/404.cfm?msg=#urlEncodedFormat(e.message)#");
10 writeDump(e);
11 abort;
12 }
13
14 application.cms.renderContent(page);
15
16 return true;
17}
Obviously there is an admin as well. The admin let's you edit pages, sections, and templates. What's cool is - if you try to create a page with no templates or sections in the database, it will notice this and stop you. The admin is currently unprotected. I've added it to my list of things to add later on (see final notes).
There is one really cool part to this (imho). When the application renders a page, it does it via the VFS:
2 var result = arguments.page.renderMe();
3 var vfile = hash(arguments.page.getSection().getName() & "/" & arguments.page.getTitle());
4 var vpath = expandPath("/vfs") & "/" & vfile;
5 fileWrite(vpath,result);
6 //before we run, copy some variables over so we can have dynamic templates
7 local.title = page.getTitle();
8 local.section = page.getSection().getName();
9 local.sectionlist = getSectionList();
10 local.cmsurl = getCMSURL();
11 writeLog(file="cms",text="local.cmsurl=#local.cmsurl#");
12 include "/vfs/#vfile#";
13}
What this means is that you can include code in your templates. For example, my main template footer has:
2<cfoutput>Copyright #year(now())#</cfoutput>
3</p>
And it works! Also - do you see all those local.* variables? I pass in a bunch of variables into the local scope so that both templates and pages can make use of the variables. So for example, check out the header:
2
3<head>
4<cfoutput><title>#local.section# / #local.title#</title></cfoutput>
5</head>
6
7<body bgcolor="green">
8
9<table width="80%" bgcolor="white">
10<tr>
11<td align="center">
12<cfloop index="l" list="#local.sectionlist#">
13<cfoutput><a href="#local.cmsurl#/#l#/index.cfm">#l#</a> <cfif l is not listLast(local.sectionList)>/</cfif></cfoutput>
14</cfloop>
15</td>
16</tr>
17<tr>
18<td>
19<cfoutput><h1>#local.section# / #local.title#</h1></cfoutput>
As you can see, I make use of the section name and page title in the title tag. Also note sectionlist. Remember when I said we had an order for sections? This comes into play here as it lets me spit out a simple ordered menu:

Of course, letting users write code like that in the admin is something of a security risk. You could use tokens instead (%title% would be a page title), but it's kinda cool how well it works.
Anyway, you can download the demo below. You will want to edit these lines in Application.cfc to meet your system requirements:
2this.ormsettings = {
3 dialect="MySQL",
4 dbcreate="update"
5};
You just need to change datasource and dialect. Oh, and I freaking love the fact that I didn't have to make a table once. Oh, and I freaking love that I added 'order' to section in the CFC, reloaded, and bam, the column was added for me. Me love me some ORM.
p.s. One more thing I'd like to do with this application later on. Hibernate (and CF9's use of it) allows you to run code on various events. In theory, I should be able to write code that says, "If I save a section and mark it as section default, update all other sections and set that value to false." I haven't played with events yet so that will be my next experiment.

http://labs.adobe.com/technologies/coldfusion9/
Click on the community tag, you will see links to docs.
I'm currently working on a CMS system that I hope to be great. I want to release it to RIAForge while its in development but I don't want it to be available once production is finished.
But great post, and love the new blog once again.
John: you can hold off uploading a file. I don't like that very much and recommend you don't hold off fir long, but u can. Pardon typos - on iPhone.
https://www.hibernate.org/410.html
Thanks for sharing your CMS, it's very helpful to see good CF-ORM example.
that integrates content management, campaign management, crm, API managements, rss syndication
and analytics. You are all welcome to take it for a test drive.
This is a really interesting exercise which has prompted a few ideas for some stuff I am working on.
I am having some trouble getting it to work as it should. In particular the code that is triggered to initialise the model (adding ?init=1 to the URL doesn't work for me). If I make changes to cms.cfc, it does not appear to update the cms object in memory.
I am using CF9 and IIS7. What am I doing wrong?
Kevin
So it certainly looks like if you rely completely on onMissingTemplate as you did it won't ever run onRequestStart. I guess the workaround is to add a call to onRequestStart in onMissingTemplate.
Or perhaps it needs to be an Admin only operation - with a link in the admin. That would work too. To be honest, that operation (the init like that) isn't really going to be used in production.