Image Uploading in Model-Glue
Posted on Apr 21, 2006
Recently, I started using Model-Glue and even went as far as to unilaterally adopt it for a project I am working on at my job. So far the experience has been a positive one. One question that seems to come up frequently for Model-Glue newbies is how to handle image uploading in a form, and I wanted to cover how I chose to handle that. First, I want to thank Raymond Camden for his series, and Peter Farrell and Peter Bell for talking (IM-ing actually) me through some conceptual issues I was dealing with. Also, be gentle, I am very new to Model-Glue and OO, but I figured my experiences might help others who are also getting started (and feel free to correct me...I would love the feedback). That being said, back to the topic at hand.To understand why the image uploading question comes up in Model-Glue, you must first know that the framework moves all form and url variables into it's event scope. However, you cannot perform a cffile upload on anything but a form variable. You could put a cffile in your controller.cfc, but I quickly learned that putting logic in the controller was a mistake...first of all because on any reasonably sized application, your controller just starts to get unwieldy, but also it seems to be a mistake for portability reasons. So, while I could put the cffile there, it seemed like whatever service component I had handling the form processing should probably deal with this as well. For the record, my controller method might look something like this (note the specific form I was building handled uploading promos, second this is not necessarily a finished method):
<cffunction name="addPromo" access="public" returntype="void" output="false" hint="I add a new promo or edit an existing one"> <cfargument name="event" type="ModelGlue.Core.Event" required="true" /> <cfset var stPromoAdd = structNew() /> <cfset stPromoAdd.fileFieldName = arguments.event.getValue("fileFieldName") /> <P>
<cfset variables.promoService.addPromo(argumentCollection=stPromoAdd) /> </cffunction>
Notice, all this method does is create an argument collection and call the appropriate service, keeping my model perfectly dumb. Right now it is passing the name of the file field which, for the moment, is a hidden field in the form, though I don't know if this is the most elegant solution (but like I said, this is a work in progress).
From here the problem became that I didn't want my promoService to have to have any knowledge about my file system structure, not about how ot upload a file, since these things may change (and it didn't seem like something the service should know about anyway). So I created a file system component that is called to handle these things, which I instantiate in the promoService init method like so:
<cfset variables.fileSystemService = createObject("Component","admin.catapult.model.fileSystem").init("promo") />
The way I set up my filesystem component for this application, it knows a little bit about the way file uplaods are organized on the system. Therefore, I pass it the "type" of item that this represents, which it uses to place uploads in their appropriate place on the filesystem. So, now my addPromo method looks like this (note: I am leaving out all items except for the upload for the sake of simplicity):
<cffunction name="addPromo" access="public" output="false" returntype="void"> <cfargument name="fileFieldName" required="false" type="string" default="" /> <cfset var stUpload = "" /> <cfset var formFacade = createObject("component","admin.catapult.model.util.scopeFacade").init("form") /> <cfif len(arguments.fileFieldName) and formFacade.exists(arguments.fileFieldName) and len(formFacade.getValue(arguments.fileFieldName))> <cfset stUpload = fileSystemService.upload(arguments.fileFieldName) /> </cfif> </cffunction>
I use the facade component that Joe Rhinehart includes in the Model-Glue samples because I don't want to call the upload if the form field either doesn't exist or is empty (and I don't want to access the form scope directly...right?). If there is a file to upload I call my fileSystemService component to perform the upload for me. Here is the fileSystem component (this is a work in progress as well, as it only has a few methods at the moment):
<cfcomponent> <cffunction name="init" access="public" output="false" returntype="fileSystem"> <cfargument name="destination" type="string" required="false" default="" /> <!--- all file uploads from the admin go in common ---> <cfset variables.basePath = "/common/" /> <cfset setDestination(variables.basePath & arguments.destination) /> <cfreturn this /> </cffunction> <cffunction name="setDestination" access="public" output="false" returntype="void"> <cfargument name="destination" type="string" required="false" default="" /> <cfif len(arguments.destination)> <cfset variables.destination = arguments.destination /> </cfif> <!--- add the trailing slash if none exists ---> <cfif listLast(variables.destination,"") NEQ "/"> <cfset variables.destination = variables.destination & "/" /> </cfif> <cfif not directoryExists(expandPath(variables.destination))> <cfdirectory action="create" directory="#expandPath(variables.destination)#" /> </cfif> </cffunction> <cffunction name="getDestination" access="public" output="false" returntype="string"> <cfreturn variables.destination /> </cffunction> <cffunction name="upload" access="public" output="false" returntype="struct"> <cfargument name="formFieldName" required="true" type="string" /> <cfset var stReturn = structNew() /> <cfset var stFileUpload = "" /> <cfset var stMove = "" /> <cftry> <cffile action="upload" filefield="#arguments.formFieldName#" destination="#expandPath(variables.basePath)#" nameconflict="makeunique" result="stFileUpload" /> <cfset stMove = move(stFileUpload.serverFileName,stFileUpload.serverFileExt) /> <cfif not stMove.success> <cfthrow type="fileSystem.move"> </cfif> <cfset stReturn.success = true /> <cfset stReturn.sourceFile = stMove.sourceFile /> <cfset stReturn.destinationFile = stMove.destinationFile /> <cfcatch type="any"> <cfif fileExists(expandPath(variables.basePath) & stFileUpload.serverFileName & "." & stFileUpload.serverFileExt)> <cffile action="delete" file="#expandPath(variables.basePath)##stFileUpload.serverFileName#.#stFileUpload.serverFileExt#" /> </cfif> <cfset stReturn.success = false /> </cfcatch> </cftry> <cfreturn stReturn /> </cffunction> <cffunction name="move" access="public" output="false" returntype="struct"> <cfargument name="sourceFileName" required="true" type="string" /> <cfargument name="sourceFileExt" required="true" type="string" /> <cfargument name="fileName" required="false" type="string" default="#uniqueFileName()#" /> <cfset var stReturn = structNew() /> <cftry> <cffile action="move" source="#expandPath(variables.basePath)##arguments.sourceFileName#.#arguments.sourceFileExt#" destination="#expandPath(getDestination())##arguments.fileName#.#arguments.sourceFileExt#" /> <cfset stReturn.success = true /> <cfset stReturn.sourceFile = arguments.sourceFileName & "." & arguments.sourceFileExt /> <cfset stReturn.destinationFile = arguments.fileName & "." & arguments.sourceFileExt /> <cfcatch type="any"> <cfset stReturn.sourceFile = "" /> <cfset stReturn.destinationFile = "" /> <cfset stReturn.success = false /> </cfcatch> </cftry> <cfreturn stReturn /> </cffunction> <cffunction name="uniquefilename" access="public" output="false" returntype="string"> <cfargument name="prepend" required="false" type="string" default="" /> <!--- based upon cflib function: http://www.cflib.org/udf.cfm?ID=148 ---> <!--- note: I left prepend in for now, but it isn't being used at the moment ---> <cfset var fileName = "" /> <cfif len(arguments.prepend)> <cfset fileName = arguments.prepend & "_" /> </cfif> <cfset fileName = fileName & dateformat(now(),"mmddyy") & "_" & timeformat(now(),"HHmmss") > <cfreturn fileName /> </cffunction> <P>
</cfcomponent>
There is quite a bit going on here even though the number of methods are limited for now. First, when you initialize the component, it sets the destination, which also checks to see whether the directory exists and, if not, creates it - note: the create directory will eventually move to its own method. The upload method actually first uploads to my base path and then performs a move on the file. I do this so that I can rename the file according to my naming convention (which is just a date/timestamp for now - but that could change). Notice though that by using putting all this logic in my fileSystem component rather than in my model or my promo service, it can be reused by any other form that also needs to perform a file upload - which is after all the point to the framework right?.
Hopefully this will be helpful to people getting started with Model-Glue (like myself). If so, feel free to post a comment letting me know and I will continue to share these kinds of posts as I work through my Model-Glue project (which by the way, also uses Reactor extensively). Also, to the OO Gurus who once helped me out when I was feeling frustrated...feel free to push me in a direction whereby this could be further streamlined and improved :)
Comments
Posted By Peter J. Farrell / Posted on 04/24/2006 at 10:10 PM