CF9 ORM and Apache's mod_rewrite

Here’s another problem I’ve run into with ColdFusion’s ORM (there seem to be a few of these recently…).

I’ve only just started running a CF dev environment under Apache on my Mac – I was previously running IIS under VMWare. The problem comes when using Apache’s mod_rewrite for URL rewriting.

I set up a simple test scenario with the following Application.cfc:

component output="false"
{
	this.name = "ormtest";
	this.sessionManagement = true;
	this.sessiontimeout = CreateTimeSpan(0,5,0,0);
	this.ormEnabled = true;
	this.datasource = "TEST";
	this.ormSettings = {
		cfclocation = "/model"
	};	
}

The model directory contains a single ORM mapping bean, user.cfc.

This runs fine and initialises without a problem. But then I add the following mod_rewrite rules into .htaccess:

RewriteEngine on
RewriteBase /

RewriteCond %{REQUEST_URI} !/(assets|index.cfm) [NC] 
RewriteRule ^(.*)$ /index.cfm/$1 [NC,L]

This handles rewriting of SES URLs for FW/1 pages – essentially, anything that’s not in the assets folder gets index.cfm/ prepended to it behind the scenes. However, this causes the following error when loading the application:

Could not find the ColdFusion component or interface user.

It turns out that the reference to /model as the ORM’s cfclocation is also being rewritten; what is more, adding model into the RewriteCond of paths to ignore doesn’t solve the problem.

After a bit of searching, I discovered Richard Herbert had experienced what seemed to be the same problem. A quick tweet later, and he told me of the solution he’d found.

The solution is simple: create a mapping to the directory containing the ORM CFCs. So, in my example, I added the following before the ORM settings:

this.mappings = {'/model' = '/Volumes/Dev/Web/sites/ormtest/model'};

…and everything now works as it should. (Note: be sure to change the mapping to reflect the structure of your live server!)

Out of interest, I tried the same test under IIS running ISAPI_Rewrite – and it didn’t share the problem. So I assume that mod_rewrite runs at a lower level in the system than ISAPI_Rewrite: mod_rewrite will rewrite your internal CF file calls. So that’s something to be wary of…

7 comments Posted on 29 August, 2010, in ColdFusion, ISAPI_Rewrite, mod_rewrite, ORM

Custom buildURL() method for FW/1

UPDATED: The functionality has now been rolled into the framework as of version 1.2, so the hack is unnecessary. The methodology for overriding a framework method still applies, but let me reiterate: be very careful what you change, and look out for any code changes to the overridden method in future releases!

Original post continues below…


My (first) FW/1 application consists of two subsystems: public and admin. The public subsystem is the default, so if I invoke a URL action without a subsystem specified explicitly, it will use ‘public’.

Of course, if you use the buildURL() method to create your links (which you should be doing!) it will always prepend your action with public: – which I find a little untidy, especially when using SES URLs.

It’s very easy to customise FW/1 to “fix” this. Simply import a customisation file at the top of your Application.cfc file – alongside all the application settings – and put the following code in that file:

<cffunction name="buildURL_custom" access="public" output="false">
	<cfargument name="action" type="string" />
	<cfargument name="path" type="string" default="#variables.framework.baseURL#" />
	<cfargument name="queryString" type="string" default="" />

	<cfset var initialDelim = '?' />
	<cfset var varDelim = '&' />
	<cfset var equalDelim = '=' />
	<cfset var basePath = '' />
	<cfset var extraArgs = '' />
	<cfset var queryPart = '' />
	<cfset var anchor = '' />
	<cfset var ses = false />
	<cfset var omitIndex = false />

	<cfif arguments.path eq "useCgiScriptName">
		<cfset arguments.path = CGI.SCRIPT_NAME />
		<cfif variables.framework.SESOmitIndex>
			<cfset arguments.path = getDirectoryFromPath( arguments.path ) />
			<cfset omitIndex = true />
		</cfif>
	</cfif>
	
	<cfif find( '?', arguments.action ) and arguments.queryString is ''>
		<!--- shorthand for action/queryString pairing --->
		<cfset arguments.queryString = REReplace( arguments.action, '[^\?]*\?', '') />
		<cfset arguments.action = REReplace( arguments.action, '([^\?\##]*).*', '\1') />
	</cfif>

	<cfif find( '?', arguments.path ) gt 0>
		<cfif right( arguments.path, 1 ) eq '?' or right( arguments.path, 1 ) eq '&'>
			<cfset initialDelim = '' />
		<cfelse>
			<cfset initialDelim = '&' />
		</cfif>
	<cfelseif structKeyExists( request, 'generateSES' ) and request.generateSES>
		<cfif omitIndex>
			<cfset initialDelim = '' />
		<cfelse>
			<cfset initialDelim = '/' />
		</cfif>
		<cfset varDelim = '/' />
		<cfset equalDelim = '/' />
		<cfset ses = true />
	</cfif>

	<!--- Start customisation. Don't display subsystem if in default subsystem --->
	<cfif ses>
		<cfif getSubsystem( arguments.action ) eq getConfig().defaultsubsystem>
			<cfset basePath = arguments.path & initialDelim & replace( getSectionAndItem( arguments.action ), '.', '/' ) />
		<cfelse>
			<cfset basePath = arguments.path & initialDelim & replace( getFullyQualifiedAction( arguments.action ), '.', '/' ) />
		</cfif>	
	<cfelse>
		<cfif getSubsystem( arguments.action ) eq getConfig().defaultsubsystem>
			<cfset basePath = arguments.path & initialDelim & variables.framework.action & equalDelim & getSectionAndItem(arguments.action) />
		<cfelse>
			<cfset basePath = arguments.path & initialDelim & variables.framework.action & equalDelim & getFullyQualifiedAction(arguments.action) />
		</cfif>		
	</cfif>
	<!--- End customisation --->
	
	<cfif len( arguments.queryString )>
		<cfset extraArgs = REReplace( arguments.queryString, '([^\?\##]*).*', '\1') />
		<cfif find( '?', arguments.queryString )>
			<cfset queryPart = REReplace( arguments.queryString, '[^\?]*\?([^\##]*).*', '\1') />
		</cfif>
		<cfif find( '##', arguments.queryString )>
			<cfset anchor = REReplace( arguments.queryString, '[^\##]*\##(.*)', '\1' ) />
		</cfif>
		<cfif ses>
			<cfset extraArgs = listChangeDelims( extraArgs, '/', '&=' ) />
		</cfif>
		<cfif extraArgs is not ''>
			<cfset basePath = basePath & varDelim & extraArgs />
		</cfif>
		<cfif queryPart is not ''>
			<cfif ses>
				<cfset basePath = basePath & '?' & queryPart />
			<cfelse>
				<cfset basePath = basePath & '&' & queryPart />
			</cfif>
		</cfif>
		<cfif anchor is not ''>
			<cfset basePath = basePath & '##' & anchor />
		</cfif>
	</cfif>
	
	<cfreturn basePath />

</cffunction>

<cfset variables.buildURL = variables.buildURL_custom />

The code is just a copy-and-paste of the buildURL() method from the org.corfield.framework cfc. The changes I’ve made are in the commented area in the middle, and just tell it that if the requested action is in the default subsystem, then only return the section and item in the URL; otherwise use the fully qualified action.

Note that I’ve renamed the method to buildURL_custom, and then used a cfset after the end of the function to replace the pre-defined method, as you are not allowed to define the same method twice.

You can use this method to override other FW/1 methods, but I’d advise a touch of caution: your changes might have knock-on effects! Also, keep an eye on any future updates of the framework to see if you need to apply code changes to your custom methods.

2 comments Posted on 26 August, 2010, in ColdFusion, FW/1

Using include within cfscript

I’ve only recently started using cfscript to code my components, and today ran into this little problem…

I have an Application.cfc, written in cfscript, and I want it to include a settings file which contains the different settings between development and production servers (so I can update the Application.cfc file without having to worry about altering settings before copying it to the live server).

It looks something like this (simplified, of course!):

component output="false"
{
	this.name = "myApp";
	this.sessionManagement = true;
	this.ormEnabled = true;
	this.datasource = "myDatasource";
	include "/config/serversettings.cfm";
}

And the settings file it included was this:

// Server settings for dev server
this.ormSettings.logSQL = true;

I couldn’t work out why the logging setting was not being seen. By trial and error with other script-based includes, I discovered the answer:

I’d assumed that, like a normal cfinclude, it would just take the contents of the included file and placed them in place of the include call – so, because I was already within cfscript, it would see the code in the included file as just another piece of cfscript to run.

This doesn’t seem to be the case. By changing the included file to this:

<cfscript>
// Server settings for dev server
this.ormSettings.logSQL = true;
</cfscript>

…it worked perfectly. It would appear that, even if you’re calling an include from within a cfscript block, it doesn’t see what’s included as cfscript unless you explicitly define it as such.

I suppose it makes sense – otherwise you’d never be able to include traditional CFML tags from within cfscript – but it caught me out; I hope this is helpful to anyone else who is caught out by it!

1 comment Posted on 22 August, 2010, in ColdFusion, Quick Tips

CF9 ORM relationships - has<property>() oddity

In my CF9 ORM application, I have a magazine object. Each magazine has a single genre (e.g. Craft, Sports, etc.) – so I have a many-to-one relationship set up on the magazine.cfc:

component output="false" persistent="true"
{
	// identifier
	property name="magazineid" fieldtype="id" setter="false" generator="identity";
	
	// properties
	property name="title";
	property name="genre" cfc="genre" fieldtype="many-to-one" fkcolumn="frn_genreid";
}

The genre.cfc looks like this:

component output="false" persistent="true"
{
	// identifier
	property name="genreid" fieldtype="id" setter="false" generator="identity";
	
	// properties
	property name="genre";
	property name="magazines" cfc="magazine" fieldtype="one-to-many" fkcolumn="frn_genreid";
}

It all works nicely, until I want to try removing the genre from the magazine.

If I create a new magazine entity, the genre property contains an empty string (confirmed by dumping the object). And the hasGenre() method returns false.

If I load an existing magazine entity, the genre property contains a genre object, and hasGenre() returns true.

So to try to remove the genre, I use:

magazine.setGenre( '' );

to set the genre property to an empty string, just like in the new entity – but hasGenre() still returns true – why?

If I instead use:

magazine.setGenre( {} );

to set the genre property to an empty struct, hasGenre() now returns false, and everything works OK.

But what is the difference between the empty string that I set manually, and the empty string that’s present when a new entity is created? And why do they return different results for hasGenre()?

4 comments Posted on 18 August, 2010, in ColdFusion, ORM

FW/1's populate() method uses named arguments

Just a quick tip for anyone using FW/1’s populate() method to populate their objects.

I ran into a problem where my auto-generated setters were being called correctly by the populate() method, but my explicit setters were not. And because FW/1 ignores any error generated here, I couldn’t work out why this was…

Traditionally, I’ve always written my setters as follows:

<cffunction name="setFirstName">
	<cfargument name="value" type="string" required="true" />
	<cfset variables.firstname = arguments.value />
</cffunction>

…and it would work fine, since it was always being called as setFirstName("Seb"). It’s a bit of laziness really, when writing setters by hand – it means there’s two fewer changes to make when copying and pasting.

But a quick look at FW/1’s code shows us this:

<cfinvoke component="#arguments.cfc#" method="#key#" argumentCollection="#args#" />

So when it finds a value for rc.firstname, FW/1 is essentially calling:

setFirstName( firstname = rc.firstname )

- which will of course fail if my setter is expecting an argument of “value”.

The solution is this: when creating a setter, always name the argument to match the value being set. So my setter above should instead read:

<cffunction name="setFirstName">
	<cfargument name="firstname" type="string" required="true" />
	<cfset variables.firstname = arguments.firstname />
</cffunction>
2 comments Posted on 15 August, 2010, in ColdFusion, FW/1