In my previous post in this series, I made the case for why I believe that service layer objects are a common need in an application's architecture. I also pointed out the fact that CFWheels does not provide for such animals within its framework, and that I had overcome this obstacle using two different approaches. What follows are the details of my first approach and a simple yet complete working sample app for your dissection pleasure.
MANUALLY IMPLEMENTING SERVICE LAYER OBJECTS IN CFWHEELS
So, what we need to be able to do, ideally, is have and use CFCs in our controllers that are NOT directly associated with a database table. The CFWheels approach requires all models to extend the Model CFC. If we omit that extension, and then attempt to retrieve our service layer object using the "model()" method call, we receive an error. If we DO extend the Model CFC on a service layer object and attempt to retrieve it using the "model()" method call, we receive an error. What to do, what to do!
In a nutshell, we have to instantiate and retrieve our service layer objects ourselves. This could simply be done using a line in a controller that performs a "createObject" call, and that would give us our object. Ah, but one of the key elements that a CFWheels developer needs is still missing, because although I can indeed create an instance of a service layer object this way, my service layer object will be completely ignorant of the CFWheels environment. It won't be able to do one of the things it needs to, and that is to ITSELF utilize the CFWheels "model()" and "get()" calls! The remedy for this turned out to be simple, but took a LOT of digging to discover: include the appropriate files from the CFWheels framework in my service layer objects.
Personally, I opted not to show a demo of implementing SLOs (I'm gonna use this acronym from here out) the way described above, because it just wasn't beautiful nor did it provide for an easy way of re-use in multitple controllers. What I decided I wanted, then, was a method all my own that I could use within controllers in order to retrieve my SLOs. Since I retrieve models using 'model("somemodel")', I thought it appropriate to retrieve my SLOs using 'service("mySLO")'. Additionally, I didn't want my SLOs living in the same folder as my models, so I create a folder just for them called "services". This folder contains a core SLO base object, exactly the same way that the "models" folder contains a core Model object. All of my SLOs extend this core component, and thusly inherit the required CFWheels functionality they need to do their jobs.
Steps I took/Modifications I Made
1. Created a "services" folder off the root;
2. Created a core "Service.cfc" that all SLOs must extend;
3. Modified my core "Controller.cfc to include two new methods;
That's it! Let's peek at these items in more detail.
In step 2, it was convenient (and followed the same approach as the rest of CFWheels) to create a core component to be extended. The entire contents of this component is as follows:
<!---
This CFC provides access to the wheels core functions needed by our service layer objects.
--->
<cfcomponent output="false">
<cfinclude template="../wheels/global/functions.cfm">
</cfcomponent>
In step 3, I added two methods to my core Controller.cfc. One of these methods, you may have supposed, is called "service". The other provides a single place where the developer can declare and create all of their SLOs at once. That method is called "initServices", and it looks like this:
<cffunction name="initServices" returntype="void" hint="I initialize the services objects for this app">
<!--- create readable alias keys that the dev will use to get these objects --->
<cfset application.$_ServiceObjects = {
importService = createObject("services.importUsers").init(
dsn = get("DataSourceName")
),
sessionStorage = createObject("component","services.wormhole").init()
} />
</cffunction>
The "service" method looks like this:
<cffunction name="service" returntype="any" hint="I am the method used to access any service layer object from within any controller">
<cfargument name="service" type="string" required="true" />
<cfset var retval = "" />
<cfif structkeyExists(application.$_ServiceObjects,arguments.service)>
<cfset retval = application.$_ServiceObjects[arguments.service] />
</cfif>
<cfreturn retval />
</cffunction>
One more tiny little thing, within the Controller.cfc's Init method, I added a call to InitServices to kick it off:
That is IT. In this example, what I am able to do now that I could NOT do before in a CFWheels controller (without bloating my controller, anyway) is this:
<cffunction name="importUsers">
<cfset var objImporter = service("importService") />
<cfset result = objImporter.importData(params.dataIn) />
<cfset flashInsert(msg=result) />
<cfset redirectTo(action="index") />
</cffunction>
THAT, my friends, is how thin a controller method SHOULD be, WHENEVER possible!
Okay, I won't blab on about this approach. I think it's slick, it's fairly easy to maintain, and I think once you take the time to poke through and run the sample user data importer app linked in this post, that you'll agree that you really have been missing Service Layer Objects too, you just didn't call it by that name.
Doug out :0)
Next up in the series: Implementing Service Layer Objects in CFWheels using the awesome DI framework WIREBOX and the plugin I wrote for it! :)
--------------------------------
Sample App Zip File (be sure to read the READMEDUDE.txt in the zip; you'll have to create one table and edit the datasource setting to get the app to work for you.)
You are not logged in, so your subscription status for this entry is unknown. You can login or register here.
awesome post. by following your article I was able to get a service layer into cfwheels and play around with it. the problem is though that it's a little complicated to test since you have to create a controller object in order to test. I took the method you created and plae them in the following file:
service -> events/functions.cfm
initServices -> events/onapplicationstart.cfm
I also alter the initServices to automatically create object for each component in the services directory. this allows you to create services and by reloading your app, they are automatically available.
also by moving the service() method into the functions.cfm, they are available through out the entire wheels ecosystem which makes testing much easier. I created a gist for easier copy and pasting:
https://gist.github.com/2853902
I got everything all setup and noticed that I am unable to access the params scope from the services. Anyway to do this without having to pass it in every time?
Thanks!
-Tim
One of the basic tenets of Object Oriented Programming is encapsulation; the idea that any given object should be as autonomous and stand-alone as possible, and should be as disconnected from its environment as it can be. It should never directly access scopes and values that have not been explicitly provided to it when it was called upon. Think of it in this scenario, that the service layer object you are writing should be able to be easily copied and dropped into a Fusebox app, or a Model Glue app, or a Coldbox app, and still work just as wonderfully as it does in your Wheels app. The moment you chain that service layer object to your Wheels app, it becomes a one hit wonder, a one trick pony, a bicycle with square wheels that only works great in this one very specific instance. So, although we could alter the situation so that service layer objects in wheels have direct access to params, we absolutely should not. Heck, even Wheels' own MODEL objects don't even have direct access! You have to pass in param values to a model call, right? Think of your service layer objects in the same way.
Hope that makes sense. True OOP religion does not allow us to do what you're talking about doing. :)
That said, you may want to toss it out to the Wheels list for a more well rounded philisophical discussion.
