I'm fairly new to using OO concepts such as DAO's, Gateways etc and I'm trying to work out the best way to implement an AJAX accessible CFC whilst trying not to repeat lot's of code.
I have the following DAO which holds CRUD methods for my DB table and takes the application DSN as an argument in it's constructor:
<cfcomponent name="property_imageDAO" displayname="property_imageDAO" output="false" hint="">
<!--- pseudo constructor --->
<cfscript>
variables.dsn = application.dsn;
</cfscript>
<!--- constructor --->
<cffunction name="init" access="public" output="false" returntype="any"
hint="Constructor for this CFC">
<!--- take DSN as argument --->
<cfargument name="dsn" type="string" required="true" hint="The datasource name" />
<!--- put dsn in variables scope so we can use it throughout the CFC --->
<cfset variables.dsn = arguments.dsn />
<!--- return this CFC --->
<cfreturn this />
</cffunction>
<!--- CRUD methods (create, read, update, delete) --->
<!--- CREATE: inserts a new property_image into the database --->
<cffunction name="createRecord" access="remote" output="true"
hint="Creates a new property_image record and returns a struct containing a boolean (success) indicating the success or
failure of the operation, an id (id), and a string (message) containing a message">
<!--- take property_image bean as argument --->
<cfargument name="property_image" type="any" required="true" />
<!--- initialize variables --->
<cfset var results = StructNew() />
<cfset var qInsertproperty_image = 0 />
<!--- defaults --->
<cfset results.success = true />
<cfset results.message = "The record was inserted successfully." />
<!--- insert the property_image --->
<cftry>
<cfquery name="qInsertproperty_image" datasource="#variables.dsn#">
INSERT INTO property_image (
name,
alt
)
VALUES (
<cfqueryparam value="#arguments.property_image.getname()#" cfsqltype="cf_sql_varchar" />,
<cfqueryparam value="#arguments.property_image.getalt()#" cfsqltype="cf_sql_varchar" />
)
</cfquery>
<cfcatch type="database">
<cfset results.success = false />
<cfset results.message = "Inserting the record failed. The error details if available are as follows: " & CFCATCH.Detail />
</cfcatch>
</cftry>
<!--- return the struct --->
<cfreturn StructCopy(results) />
</cffunction>
Should I add functionality to this DAO to make it AJAX accessible or should I create another DAO specifically for remote access?
Thanks
I think there's probably as many variations of a solution to this as there will be people to suggest one, but here's one take on it.
I'd not openthe DAO up to REMOTE access, I'd leave that as PACKAGE (and only be accessed by some business object in the same package). I'd also have some sort of facade sitting in front of that lot which handles the remote calls, as well as things like validating whether the call coming in remotely IS ALLOWED to be making the call. You don't want just anyone sticking stuff in your DB! The facade should deal with the auth side of things, then if all OK pass the call to the business object which then uses the DAO to access the DB.
I would not have that try/catch stuff in your DAO either. The best way to notify calling code that something went wrong is for an exception to be thrown. Then the calling code can decide what to do with it (whether to deal with it some way, ignore it, or re-bubble it to the sitewide error handling).
I suggest looking at ColdSpring and its ability to create remote proxies and its ability to secure them using AOP. This is a great way to expose only certain parts of a CFC to remote access and to control by whom they are accessed.
Both topics are covered in the ColdSpring Quick Start guide: http://www.coldspringframework.org/coldspring/examples/quickstart/
I also did a presentation on how to do this. You can see a recording here: http://textiles.online.ncsu.edu/online/Viewer/?peid=a4227aeb1ad84fa89eeb3817f075af5b1d
Related
I ran into an issue this morning after deploying of some files to a ColdFusion website/application.
I updated an existing CFC with some new code. The CFC has an init() method that returns the instantiated Object:
Original MyObject.cfc:
<cfscript>
VARIABLES.MyParam = "";
</cfscript>
<cffunction name="init" returntype="MyObject" output="false">
<cfargument name="MyParam" type="String" required="true" />
<cfscript>
VARIABLES.MyParam = ARGUMENTS.MyParam;
return THIS;
</cfscript>
</cffunction>
New MyObject.cfc:
<cfscript>
VARIABLES.MyParam = "";
</cfscript>
<cffunction name="init" returntype="MyObject" output="false">
<cfargument name="MyParam" type="String" required="true" />
<cfscript>
setMyParam(ARGUMENTS.MyParam);
return THIS;
</cfscript>
</cffunction>
<cffunction name="setMyParam" output="false" returntype="Void">
<cfargument name="MyParam" type="String" required="true" />
<cfset VARIABLES.MyParam = Trim(ARGUMENTS.MyParam) />
</cffunction>
<cffunction name="getMyParam" output="false" returntype="String">
<cfreturn VARIABLES.MyParam />
</cffunction>
Any time an Object that extended this CFC called init(), it was throwing an exception:
"The value returned from the init function is not of type MyObject."
This issue did not occur in any of the other environments in which this change was deployed - only in Production.
The only thing that fixed it was clearing the template cache in ColdFusion Administrator.
So, I'm either looking for a way to prevent this from happening in the future and/or a way to automatically clear the template cache when I deploy files.
FYI, I currently deploy files using Tortoise SVN.
In your init() (or more preferably, in another reload-style method), programmatically call the Admin API's clearTrustedCache() method:
<cfscript>
// Login is always required (if the administrator password
// is enabled in the ColdFusion Administrator).
// This example uses two lines of code.
adminObj = createObject("component","cfide.adminapi.administrator");
adminObj.login("admin");
// Instantiate the runtime object.
myObj = createObject("component","cfide.adminapi.runtime");
// clear cache
myObj.clearTrustedCache();
// Stop and restart trusted cache. However, only the clearTrustedCache function needs to be called.
myObj.setCacheProperty("TrustedCache", 0);
myObj.setCacheProperty("TrustedCache", 1);
</cfscript>
This functionality's been in place as far back as CF7 (Source). Note that you will need the CF Admin password for this.
I would also recommend clearing the component cache, if you have that option enabled in your admin:
myObj.clearComponentCache();
I have a filter setup that runs a function which checks if the user session is present on certain actions, like so;
<cffunction name="init">
<cfset filters(through="checkLogin", except="login,register,signin,create,home,profile") />
</cffunction>
The problem is, these are the action names...which conflict from other controllers I have.
For example, I have 2 controllers 'user' and 'link'. Each of these has an action called create, so that my URL's are like so:
/user/create/
/link/create/
How can the filter know with which controller to associate it with? Is there a way to prefix certain 'actions' in the 'except' clause with the controller name too?
For example, maybe something like:
<cffunction name="init">
<cfset filters(through="checkLogin", except="user/login,user/register,user/signin,link/create,main/home,user/profile") />
</cffunction>
I remember trying this but it didn't work and borked.
Hope you understand what I'm saying here. I don't want to have to name every action in separate controllers totally unique names.
Thanks,
Michael.
You can use basic inheritance to accomplish this:
<!--- controllers/Controller.cfc --->
<cffunction name="init">
<cfargument name="checkLoginExcept" type="string" required="false" default="">
<cfset filters(through="checkLogin", except=arguments.checkLoginExcept)>
</cffunction>
Then in any child controller (user, for example), you can specify which actions to exclude. This works well because the parent controller should really know nothing about its children. It implements what it cares about and nothing else.
<!--- controllers/User.cfc --->
<cffunction name="init">
<cfset super.init(checkLoginExcept="login,register,signIn,profile")>
</cffunction>
If another child always wants for checkLogin to run, then it shouldn't pass a value for checkLoginExcept:
<!--- controllers/Foo.cfc --->
<cffunction name="init">
<cfset super.init()>
</cffunction>
I'm new to wheels (and I'm sure I'll be posting here a lot) so bear with me.
I have two forms "register" and "login" under the controller of "user". So my URL's look like.
/user/register/
/user/login/
At the moment in my models folder I simply have user.cfc with validation for the "register" page inside the init method - this works just fine.
So essentially...my question is...regarding validation for my login form; do I have to always place validation into the init method or in a different one? If so, how do I do this? Each form of course has different fields...so I need to know some logic on detecting what form is currently in play.
Hope this makes sense. For reference, my user.cfc model currently looks like this:
<cfcomponent extends="Model" output="true">
<cffunction name="init">
<cfset validate( property='userName', method='validateAlphaNumeric') />
<cfset validatesPresenceOf( properties='userName') />
<cfset validatesUniquenessOf( properties='userName') />
<cfset validatesFormatOf( property='userEmail', type='email', message="Email address is not in a valid format.") />
<cfset validatesPresenceOf( properties='userEmail') />
<cfset validatesUniquenessOf( properties='userEmail') />
<cfset validatesPresenceOf( properties='userPassword') />
<cfset validatesConfirmationOf( property='userPassword') />
<cfset validatesLengthOf( property="userToken", allowBlank=true) />
</cffunction>
<cffunction name="validateAlphaNumeric" access="private">
<cfif REFind("[^A-Za-z0-9]", this.userName, 1)>
<cfset addError( property="userName", message="User name can only contain letters and numbers." ) />
</cfif>
</cffunction>
</cfcomponent>
Thanks,
Michael.
Michael,
You do need to put model validations in your init() method; Wheels requires that. However, I am not sure you would want or need to use model validation for the login page/call.
Unless I am missing something, you're not actually altering the model (i.e., creating a new or updating an existing user) when a user logs into a site. You are simply authenticating them (checking their username/password combo) against your DB values.
If it were me, I'd use client-side validation for login (fields are completed, etc.) and model validation for register.
HTH!
Craig
Go to this URL and scroll down to: Use "when", "condition", or "unless" to Limit the Scope of Validation
http://cfwheels.org/docs/1-1/chapter/object-validation
For your case, you could probably go with when="onCreate"
In my "link" model I have some basic validation, one of which is to check if the link the user is submitting is already in the database.
If the link IS already submitted to the database, I want to let them know that and forward them to the link that was previously submitted (to a URL basically).
How can I do this? My model looks like this so far:
<cfcomponent extends="Model" output="true">
<cffunction name="init">
<cfset validatesPresenceOf( property='linkURL') />
<cfset validatesFormatOf( property='linkURL', type='url', message="Your link isn't a valid URL.") />
<cfset validatesUniquenessOf( property='linkURL') />
</cffunction>
</cfcomponent>
Very basic. The validatesUniquenessOf() works great, but I'd like to do a bit more in my validation logic. If I was doing it without the framework...I'd of course do some standard logic, but I'd like to work the way wheels needs me to.
Thanks again CFWHEELS!
This falls out of the general use case for validatesUniquenessOf(), but there are ways around that using addError and errorsOn.
I would do this in the model:
<cfcomponent extends="Model">
<cffunction name="init">
<cfset validatesPresenceOf( property='linkURL') />
<cfset validatesFormatOf( property='linkURL', type='url', message="Your link isn't a valid URL.") />
<cfset validate("validateUniqueUrl") />
</cffunction>
<cffunction name="validateUniqueUrl" access="private">
<cfscript>
if (this.exists(where="linkURL='#this.linkURL#'")) {
this.addError(property="linkURL", name="linkExists", message="The link you entered already exists.");
}
</cfscript>
</cffunction>
</cfcomponent>
The reason I would do that is so that you'll have a named error to check for in the controller (called linkExists).
Then in your controller:
<cfcomponent extends="Controller">
<cffunction name="create">
<cfscript>
link = model("link").new(params.link);
local.linkExistsErrors = link.errorsOn(property="linkURL", name="linkExists");
if (link.save()) {
// Whatever you want to do on success
}
else if (ArrayLen(local.linkExistsErrors)) {
flashInsert(error=local.linkExistsErrors[1].message);
Location(url=link.linkURL, addToken=false); // Need to use Location or cflocation for hard-coded URLs
}
else {
// Whatever you want to do on other types of validation failures
}
</cfscript>
</cffunction>
</cfcomponent>
API Resources
errorsOn: http://cfwheels.org/docs/function/errorson
validate: http://cfwheels.org/docs/function/validate
addError: http://cfwheels.org/docs/function/adderror
Could you not supply the submitted url as a link in the message attribute of validatesUniquenessOf()? This way the user will get the error and will be able to follow the link in the message. Otherwise, I think you will need to use cflocation to send the user to the linUrl value if the validatesUniquenessOf() functions returns false.
I have tried multiple tutorials on this topic from Forta.com and yet run into the same error:
"Error invoking CFC/....(file path)../wgn.cfc: Internal Server Error [Enable
debugging by adding 'cfdebug to your URL parameters to see more info]"
I am working on my local machine and testing as localhost. Running WinXP Pro with sp3. Using Coldfusion's web server.
Both my .cfm and .cfc are in the same folder under the the webroot. In my case:
c:\ColdFusion9\wwwroot\bridges(.cfm and .cfc here)
So, they are in a "bridges" folder under wwwroot.
The code should generate some autosuggest functionality when the user types in the input box. Instead, it just spits back the above error.
This is my cfc named wgn.cfc:
<cfcomponent output="false">
<cfset THIS.dsn="bridges">
<!--- Lookup used for auto suggest --->
<cffunction name="getWGN" access="remote" returntype="array">
<cfargument name="search" type="any" required="false" default="">
<!--- Define variables --->
<cfset var data="">
<cfset var result=ArrayNew(1)>
<!--- Do search --->
<cfquery datasource="#THIS.dsn#" name="data">
SELECT tblIDs.ID
FROM tblIDs
WHERE (tblIDs.IDType = 'xxx') AND (tblIDs.ID Like ('#ARGUMENTS.search#%'));
</cfquery>
<!--- Build result array --->
<cfloop query="data">
<cfset ArrayAppend(result, searchIDs)>
</cfloop>
<!--- And return it --->
<cfreturn result>
</cffunction>
</cfcomponent>
And this is the relevant part of the form from my .cfm page:
<cfform .....>
<cfinput name="searchIDs" type="text" autosuggest="cfc:wgn.getWGN({cfautosuggestvalue})">
//......more to form, obviously
</cfform>
UPDATE
Solution:
change
<cfset ArrayAppend(result, searchIDs)>
to
<cfset ArrayAppend(result, ID)>