Skip to main content
Ben Nadel at RIA Unleashed (Nov. 2010) with: Erica Lebrun
Ben Nadel at RIA Unleashed (Nov. 2010) with: Erica Lebrun

Customizing The Router In The CFWheels ColdFusion Framework

By
Published in Comments (3)

Like all of the ColdFusion frameworks, CFWheels provides a router that declaratively maps URLs onto Controller actions (the "C" in "MVC"). However, the convenience methods that Wheels provides out-of-the-box don't necessarily align with how I want to build a ColdFusion application. And so, I needed a way to customize the router (aka, the "mapper"). To do this, I created a proxy component that manipulates the mapper state internally and exposes a more Ben-friendly API.

This need to customize the router all started with the way in which I process form submissions. By default, Wheels really wants forms to be rendered and processed by two different controller actions. For example, rendering a new user form might declaratively map to one action (.new):

GET /users/newUsers::new()

... and then submitting that form might declaratively map to a different action (.create):

POST /users/newUsers::create()

I'm sure there's an academic reason for this control flow (Wheels is certainly not the only framework that does this); but, it's just not how my brain works. Instead, I want both the GET and the POST to be handled by the same controller and rendered by the same view.

Ideally, I'd have a convenience method on the router called .postback(), which routes both GET and POST methods to the same controller. But, it's not entirely clear from the Wheels documentation how I might go about doing this. After a few different explorations, here's the approach that I came up with.

The global Wheels mapper() function uses a fluent API. That is, you can continue to chain .-methods on it, and it maintains state and route scoping internally. This is very nice to consume; but, not so easy to modify. To bridge the gap, I created a Mapper.cfc ColdFusion component which invokes this fluent mapper() API under the hood, but exposes it's own set of proxy methods.

In the Mapper.cfc ColdFusion component, I define all of the same methods that the Wheels routing provides; and, by default, each of these methods turns around and invokes the underlying mapper() state method using a private method, proxyMapperMethod():

Note: My Mapper.cfc extends the wheels.Global component in order to have access to the mapper() method.

component
extends = "wheels.Global"
hint = "I provide a proxy for the underlying Wheels mapper() to allow for customized behavior."
{
/**
* I initialize the mapper wrapper.
*/
public void function init() {
variables.__wheels_mapper__ = mapper();
}
// .... truncated .... //
/**
* I proxy the given method on the underlying Wheels mapper.
*/
private any function proxyMapperMethod(
required string methodName,
required any methodArguments
) {
variables.__wheels_mapper__ = invoke( variables.__wheels_mapper__, methodName, methodArguments );
// Always return THIS reference so we return the proxy and not the underlying
// Wheels mapper instance.
return this;
}
}

If you look at the proxyMapperMethod() code, you'll see that it provides it's fluent API — after it moves the mapper() state forward (internally), it returns the this reference to the proxy component. All of the public methods on this component extend that fluent API by propagating the return value:

component
extends = "wheels.Global"
hint = "I provide a proxy for the underlying Wheels mapper() to allow for customized behavior."
{
/**
* I initialize the mapper wrapper.
*/
public void function init() {
variables.__wheels_mapper__ = mapper();
}
// ---
// PUBLIC METHODS.
// ---
/**
* I proxy the method invocation for collection() on the underlying mapper.
*/
public any function collection() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for constraints() on the underlying mapper.
*/
public any function constraints() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for delete() on the underlying mapper.
*/
public any function delete() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for end() on the underlying mapper.
*/
public any function end() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for get() on the underlying mapper.
*/
public any function get() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for member() on the underlying mapper.
*/
public any function member() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for namespace() on the underlying mapper.
*/
public any function namespace() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for package() on the underlying mapper.
*/
public any function package() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for patch() on the underlying mapper.
*/
public any function patch() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for post() on the underlying mapper.
*/
public any function post() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for put() on the underlying mapper.
*/
public any function put() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for resource() on the underlying mapper.
*/
public any function resource() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for resources() on the underlying mapper.
*/
public any function resources() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for root() on the underlying mapper.
*/
public any function root() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I proxy the method invocation for scope() on the underlying mapper.
*/
public any function scope() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
/**
* I return the underlying CFWheels mapper (that we're proxying).
*/
public any function wheelsMapper() {
return __wheels_mapper__;
}
/**
* I proxy the method invocation for wildcard() on the underlying mapper.
*/
public any function wildcard() {
return proxyMapperMethod( getFunctionCalledName(), arguments );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I proxy the given method on the underlying Wheels mapper.
*/
private any function proxyMapperMethod(
required string methodName,
required any methodArguments
) {
variables.__wheels_mapper__ = invoke( variables.__wheels_mapper__, methodName, methodArguments );
// Always return THIS reference so we return the proxy and not the underlying
// Wheels mapper instance.
return this;
}
}
view raw Mapper.cfc hosted with ❤ by GitHub

At this point, the Mapper.cfc component does nothing more than the Wheels mapper() — it provides the same fluent API. But, the mechanics are different. Since my Mapper.cfc is a ColdFusion component, I can now sub-class the Mapper.cfc definition, add new methods, and even augment the behavior of existing methods by leveraging the super call-path.

For example, here's my sub-classed component, Routes.cfc, which extends Mapper.cfc and adds my postback() method:

component
extends = "Mapper"
hint = "I provide custom behaviors for the Wheels mapper proxy."
{
/**
* I provide a convenience method for defining routes that can post back to themselves.
*/
public any function postback() {
return this
.get( argumentCollection = arguments )
.post( argumentCollection = arguments )
;
}
}
view raw Routes.cfc hosted with ❤ by GitHub

As you can see, the postback() method isn't doing anything more complex than calling .get() and .post() with the same set of arguments. But that's just it — it's a convenience method.

And, this Routes.cfc component now opens the door to all kinds of extension. For example, if I wanted to omit the route slug for all .show actions, I could just sub-class the .get() behavior and explicitly define an empty pattern:

component
extends = "Mapper"
hint = "I provide custom behaviors for the Wheels mapper proxy."
{
public any function get(
string name,
string pattern
) {
// Don't require the detail pages to have a URL slug.
if ( ( arguments?.name == "show" ) && isNull( arguments.pattern ) ) {
arguments.pattern = "";
}
return super.get( argumentCollection = arguments );
}
}

This is where the magic of the CFC-based mechanics come into play - I can still leverage the power of the core .get() method by calling super.get() after I've augmented the arguments collection.

Using this Routes.cfc instance is exactly the same as using the Wheels mapper() function. The only difference lies in how the fluent API call chain is started:

<cfscript>
new Routes()
.resource( name = "users", nested = true )
.postback( name = "show" )
.end()
.end();
</cfscript>
view raw routes.cfm hosted with ❤ by GitHub

CFWheels has some cool stuff; but, it also has some stuff that doesn't quick click with how I think about the world. But, the nice thins is that Wheels — and ColdFusion — make it relatively easy for me to apply my way of thinking through extension points.

Want to use code from this post? Check out the license.

Reader Comments

16,061 Comments

@Chris,

Yes, this was all in the config directory, right next to the routes.cfm -- I'm not sure if that's where this stuff always goes, but that's how we have it organized at work. At this point, I still have no idea what is native "Wheels" vs. what we've done at PAI.

282 Comments

@Ben Nadel,

Very cool. I'm going to need to deep dive into this after my trip. I believe (most) everything native wheels is in the /wheels directory besides configs etc. Upgrading is just replace that directory.

Post A Comment — I'd Love To Hear From You!

Markdown formatting: Basic formatting is supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.
Cancel
I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel