Creating Service Objects In The CFWheels ColdFusion Framework
At PAI, we use the CFWheels (aka, Wheels) framework for ColdFusion. Like most of the other CFML frameworks, it follows a Model-View-Controller (MVC) paradigm. But, unlike other frameworks, its concept of "Models" is a bit more constrained — in Wheels, a model refers to a data persistence object. It has the notion of a "tableless model"; but, a tableless model cuts against the grain of how Wheels wants to operate. As a newcomer to the Wheels framework, I'm itching to have something in between a Controller and a Model. Historically, this is something that I refer to as a "Service" object. And this is a quick look at how I'm exploring "Service" objects in Wheels.
Note: I'm writing this to work with the CFWheels 3.0 pre-release snapshot. I'm not familiar with how much this approach may or may not be compatible with earlier releases of the CFWheels framework.
Wheels doesn't have "Dependency Injection" (DI) like you see in FW/1 or ColdBox. But, it still has Inversion-of-Control (IoC). Meaning, there's still a layer of indirection between a ColdFusion template and the resources that it requires. This layer of indirection consists of the controller()
and model()
functions (not to mention the host of functions are automatically applied to each execution context).
The model()
function, in particular, accepts a "token" and returns a ColdFusion component instance. The code under the hood is a little complicated to follow (there are many layers of abstraction). But, if you simplify the control flow, the global model()
inversion-of-control function is essentially doing this:
- Look to see if the requested model is cached.
- If cached, return it.
- If not cached, enter a double-check lock.
- Once lock is obtained, double-check to see if model is cached.
- If cached, return it.
- If not cached, instantiate it.
- Mix-in any global functions that are required.
- Cache it.
- Return it.
Once you mentally parse all of the Wheels rigamarole, it's actually quite straightforward. To demonstrate, I'm going to create a globally available service()
function that performs the same type of inversion-of-control, only it's going to look in a different "models folder" (and not mix-in all of the Model-related functions, such as .findAll()
, .save()
, .exists()
, etc.).
First, we have to create the container for our cached service objects. Internally Wheels does this implicitly for the model()
function; but, since we're augmenting the runtime, we need to do it explicitly. For that, I'm adding this to my onApplicationStart.cfm
event handler template:
<cfscript>
// Instance cache for the global service() method.
application.serviceCache = {};
</cfscript>
Every time the CFWheels framework is initialized, it will call the onApplicationStart()
method in the internal Application.cfc
, which will turn around and call the above template, giving us a blank check made out to Cache (that's some US humor there).
Second, we need to define the service()
function in the /app/global/functions.cfm
template, which Wheels mixes-in at the bottom of its internal Global.cfc
ColdFusion component — the component that all controllers and models extend. Here's the truncated version that I have of this functions file:
<cfscript>
/**
* I get the service (from the "app.services" folder) with the given dot-name.
*
* @name I am the dot-local path to a component in the "/app/services" folder.
*/
public any function service( required string name ) {
if ( ! application.serviceCache.keyExists( name ) ) {
lock
name = "serviceCache.#name#"
type = "exclusive"
timeout = 30
{
if ( ! application.serviceCache.keyExists( name ) ) {
application.serviceCache[ name ] = application.wirebox
.getInstance( "app.services.#name#" )
;
}
}
}
return application.serviceCache[ name ];
}
</cfscript>
This service()
function is doing exactly what the model()
function is doing, only without all of the abstractions. It's entering the double-check lock; and, if the component isn't available, it requests that WireBox instantiated it out of the /app/services
folder.
I think that WireBox was added in the Wheels 3.0 rewrite. But, this is just an implementation detail. You could just as easily execute a createObject("app.services.#name#")
call to do the same thing without the WireBox mechanics.
And that's it: You have a globally available service()
function that manages "non-Model" model objects.
The only other thing I did, which isn't strictly necessary, is that I created a root Service.cfc
ColdFusion component that all of my services can extend. This is the same approach that Wheels recommends for controllers (with a root Controller.cfc
) and for models (with a root Model.cfc
). Here's my root Service.cfc
:
component extends = "wheels.Global" {
/**
* I initialize the service class.
*/
public void function init() {
// Ex, $integrateComponents( "/wheels/view" );
}
// ---
// PUBLIC METHODS.
// ---
/**
* I provide a callback hook for Wirebox to invoke after the dependency injection
* process has been completed.
*/
public function onDIcomplete() {
// ...
}
// ---
// PRIVATE METHODS.
// ---
/**
* I merge-in the public methods from the ColdFusion components in the given directory.
*/
private function $integrateComponents( required string directoryPath ) {
var dotPrefix = directoryPath
.trim()
.listToArray( "\/." )
.toList( "." )
;
var filenames = directoryList(
path = expandPath( directoryPath ),
recurse = false,
listInfo = "name",
filter = "*.cfc"
);
for ( var filename in filenames ) {
var componentName = filename.reReplaceNoCase( "\.cfc$", "" );
$integrateFunctions( createObject( "component", "#dotPrefix#.#componentName#" ) );
}
}
/**
* I merge-in the public methods from the given ColdFusion component.
*/
private void function $integrateFunctions( required any source ) {
for ( var method in getMetaData( source ).functions ) {
// We only want to pull-in public methods.
if ( method.access != "public" ) {
continue;
}
// We only want to pull-in non-conflicting methods.
if (
structKeyExists( variables, method.name ) ||
structKeyExists( this, method.name )
) {
continue;
}
variables[ method.name]
= this[ method.name ]
= source[ method.name ]
;
}
}
}
This Service.cfc
component doesn't actually do anything concrete other than extend wheels.Global
(which is where the globally-available functions get mixed-in). But, it does define the onDIComplete()
hook for WireBox; and, it documents how the $integrateComponents()
mechanics work if you wanted to integrate them (the internal CFWheels models and controllers do the same thing).
Now, if I wanted to use something like an ErrorLogger.cfc
for error management, I can just request it via the service()
function (assuming it resides in the /app/services
folder). Here's my onError.cfm
event template:
<cfscript>
service( "ErrorLogger" )
.logException( arguments.exception )
;
</cfscript>
<!--- .... render error template CFML .... --->
It took me a while to figure this out because the inversion-of-control in Wheels looks different than it does in other frameworks. But once I waded through the control flow for the core model()
and controller()
functions, the path forward became clear(er). That said, I'm still very much in the nascent stages of learning the "Wheels Way".
Want to use code from this post? Check out the license.
Reader Comments
@Ben
Love this. Firstly, I find a
service()
function to be incredibly useful within CFWheels and it fits nicely with it's conventions. I implemented my service layer using this 13-year-old gist by rip747 a long time ago, which instantiates every service on start up. I like that yours only caches a service once used. I'm not on CFW 3.0, but I think your approach would still work for me. I may give it a try. Thanks for sharing (as always)@Chris,
Very similar approaches - just a matter of timing mostly. I certainly have built my fair-share of applications that boot-up all the CFCs in the
onApplicationStart()
event, so that feels very familiar.I know that CFWheels 2.5 -> 3.x is going to be pretty big; but this is my first real Wheels experience, so I don't know how the difference are actually manifested.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →