Twitter: raymondcamden


Address: Lafayette, LA, USA

Very simple, very ugly, CMS built with ColdFusion 9

07-25-2009 10,033 views ColdFusion 22 Comments

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:

view plain print about
1<cfset section=EntityLoad('section', url.delete, true)>
2<cfif isDefined("section")>

with

view plain print about
1<cfset section=EntityLoad('section', url.delete, true)>
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:

view plain print about
1component persistent="true" {
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:

view plain print about
1component persistent="true" {
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:

view plain print about
1component persistent="true" {
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:

view plain print about
1public boolean function onMissingTemplate(string pageRequested) {
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:

view plain print about
1public string function renderContent(page) {
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:

view plain print about
1<p align="center">
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:

view plain print about
1<html>
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:

view plain print about
1this.datasource="cms1";
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.

Download attached file

22 Comments

  • Commented on 07-25-2009 at 5:21 PM
    It would be cool to see how you could use hibernates event model and centaur's new cache functions to cache pages the when changes are made in the admin, replace the cached value with the newly edited page. Very cool stuff.
  • Commented on 07-25-2009 at 8:17 PM
    Yep, I could envision a 'cached' value on both pages and sections allowing you to cache at either level.
  • Misty #
    Commented on 07-25-2009 at 10:09 PM
    Hi Ray! Where i can see what are the new tags which are introduced in CF9. Also one Request can u tell me any link on website where i can see how i can run Coldfusion 8 & Railo 3 on Same Machine
  • Commented on 07-25-2009 at 10:12 PM
    If you go to the Labs page:

    http://labs.adobe.com/technologies/coldfusion9/

    Click on the community tag, you will see links to docs.
  • Commented on 07-26-2009 at 11:26 PM
    Interesting! I tested using onMissingTemplate() and found it quite slow so would not recommend it for a solution that takes lots of traffic.
  • Jody Fitzpatrick #
    Commented on 07-27-2009 at 8:04 AM
    Hey Ray

    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.
  • Commented on 07-27-2009 at 8:12 AM
    Johan: I have not seen that myself. Can you share sone tests?

    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.
  • Commented on 07-27-2009 at 11:22 AM
    Ray, do you know if Adobe has/will enable one to create a Solr collection from the Hibernate ORM? It appears to have been done by the Hibernate community:
    https://www.hibernate.org/410.html

    Thanks for sharing your CMS, it's very helpful to see good CF-ORM example.
  • Commented on 07-28-2009 at 11:08 AM
    Forgive typos, on iPhone. I am not aware of any specific support for this. I do know that you can get access to the XML file that configures your app's use of hibernate, but it isn't an area I've researched.
  • Commented on 07-31-2009 at 4:53 AM
    Interesting Ray. We just went beta with a new Coldfusion-based CMS called I.C.E.
    that integrates content management, campaign management, crm, API managements, rss syndication
    and analytics. You are all welcome to take it for a test drive.
  • Commented on 11-19-2009 at 4:31 PM
    Sorry I missed your reply Ray - testing/slow onMissingTemplate() - it was sometime back when I tested using onMissingTemplate() with my CMS as a way to handle different URL formats. Requesting the same "real" url via my current method (index.cfm/xxx) and onMissingTemplate() using the MSFT web stress tool resulted is significantly slower response times. As a result I did not bother to investigate further. For me it also just feels wrong using onMissingTemplate() as a url rewriter.
  • Commented on 11-19-2009 at 4:33 PM
    Interesting. You can get away with it by using IIS/Apache side rewriting to always go to dyn.cfm?path=WHATYOURREQ here. Then your CF onMissingHandler isn't run - but a normal CFM.
  • Commented on 06-30-2010 at 5:51 AM
    Ray,

    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
  • Commented on 06-30-2010 at 6:28 AM
    Well, let's start simple. Does the hook in onRequestStart run? You should end up on a page with reloaded=true in the URL.
  • Commented on 06-30-2010 at 7:57 AM
    I doesn't. Which is the main reason why I know its not working.
  • Commented on 06-30-2010 at 8:02 AM
    Ok, so let's put some logging in there. Add a few writelogs to onRequestStart to see if you can find why it isn't running.
  • Commented on 06-30-2010 at 9:23 AM
    This is wierd. It never runs onRequestStart! Is it because this thing uses onMissingMethod ?
  • Commented on 06-30-2010 at 9:24 AM
    Inconceivable! ;) Seriously though - it should be automatic. Dumb question - but you are on CF9 right?
  • Commented on 06-30-2010 at 9:26 AM
    Yes if I add the missing template it run onRequestStart! Is that a bug in CF?
  • Commented on 06-30-2010 at 9:29 AM
    Wait a tick - when you added ?init=1 to the URL - were you also requesting a file that did not exist? like foobar.cfm?
  • Commented on 06-30-2010 at 9:32 AM
    Yes, because that is the way your app (above) works. It uses the path to work out what to get from the DB.

    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.
  • Commented on 06-30-2010 at 9:36 AM
    Well hells bells. I had assumed I had at least an index.cfm. :) I wouldn't add a call to onRequestStart - I'd just move the logic into onMissingTemplate, since that is run 100% of the time then.

    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.

Post Reply

Please refrain from posting large blocks of code as a comment. Use Pastebin or Gists instead. Text wrapped in asterisks (*) will be bold and text wrapped in underscores (_) will be italicized.

Leave this field empty