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:
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:
1 <cfset section=EntityLoad('section', url.delete, true)>
2 <cfif isDefined("section")>
with
2 <cfif isDefined("section")>
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:
2 <cfif not isNull(section)>
1 component 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:
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 }
1 component 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:
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 }
1 component 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:
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 }
1 public 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:
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 }
1 public 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:
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 }
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:
2 <cfoutput>Copyright #year(now())#</cfoutput>
3 </p>
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:
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>
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:
1 this.datasource="cms1";
2 this.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.
2 this.ormsettings = {
3 dialect="MySQL",
4 dbcreate="update"
5 };
Comment 1 written by Sean Coyne on 25 July 2009, at 5:21 PM
Comment 2 written by Raymond Camden on 25 July 2009, at 8:17 PM
Comment 3 written by Misty on 25 July 2009, at 10:09 PM
Comment 4 written by Raymond Camden on 25 July 2009, at 10:12 PM
http://labs.adobe.com/technologies/coldfusion9/
Click on the community tag, you will see links to docs.
Comment 5 written by Johan on 26 July 2009, at 11:26 PM
Comment 6 written by Jody Fitzpatrick on 27 July 2009, at 8:04 AM
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.
Comment 7 written by Raymond Camden on 27 July 2009, at 8:12 AM
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.
Comment 8 written by Art Holland on 27 July 2009, at 11:22 AM
https://www.hibernate.org/410.html
Thanks for sharing your CMS, it's very helpful to see good CF-ORM example.
Comment 9 written by Raymond Camden on 28 July 2009, at 11:08 AM
Comment 10 written by Sahr Johnny on 31 July 2009, at 4:53 AM
that integrates content management, campaign management, crm, API managements, rss syndication
and analytics. You are all welcome to take it for a test drive.
Comment 11 written by Johan on 19 November 2009, at 4:31 PM
Comment 12 written by Raymond Camden on 19 November 2009, at 4:33 PM
[Add Comment] [Subscribe to Comments]