A Better CFParam Tag With A Catch Attribute (Proof Of Concept)
I love the ColdFusion CFParam tag. I think it's quite awesome and makes my programming life 10 times easier. That being said, I do have one criticism and suggestion that I think would make the CFParam tag even that much better. One thing that frustrates me a lot about the CFParam tag is that if the value being param'd does not match the allowed type, ColdFusion errors out. Now, don't get me wrong - I think that is the right thing to do. But, I hate having to deal with it in this manner:
<!--- Try to param this numeric type. ---> <cftry> <cfparam name="intID" type="numeric" default="0" /> <!--- If the variable does not hold a numeric type (and errors out), catch the error and set the value explicitly. ---> <cfcatch> <cfset intID = 0 /> </cfcatch> </cftry>
This is a numeric type example (which is like 95% of all type validation that means anything to me), but this could just as easily be applied to most any type. My suggestion is to circumvent all that CFTry / CFCatch nonsense by adding a simple "CATCH" attribute to the CFParam tag. This attribute value would be used if the CFParam fails. This would basically take the place of the CFCatch tags above:
<!--- Param numeric value. ---> <cfparam name="intID" type="numeric" default="0" catch="0" />
To demonstrate, I have put together a ColdFusion custom tag that accomplishes just this (as a proof of concept). It should have the same functionality of the standard ColdFusion CFParam tag but adds the CATCH attribute:
<!--- Kill extra output. ---> <cfsilent> <!--- The name of the variable. ---> <cfparam name="ATTRIBUTES.Name" type="string" /> <!--- The data type of the variable. ---> <cfparam name="ATTRIBUTES.Type" type="string" default="string" /> <!--- The default value of the variable. ---> <cfparam name="ATTRIBUTES.default" type="any" /> <!--- Regex pattern. ---> <cfparam name="ATTRIBUTES.Pattern" type="string" default="" /> <!--- Min for range validation. ---> <cfparam name="ATTRIBUTES.Min" type="numeric" default="0" /> <!--- Max for range validation. ---> <cfparam name="ATTRIBUTES.Max" type="numeric" default="0" /> <!--- Try to param the value. ---> <cftry> <!--- When paraming, remember to param the value from the CALLERs scope. This tag will not automatically have any hooks to that scope (unless set explicitly). ---> <cfparam name="CALLER.#ATTRIBUTES.Name#" type="#ATTRIBUTES.Type#" default="#ATTRIBUTES.Default#" min="#ATTRIBUTES.Min#" max="#ATTRIBUTES.Max#" pattern="#ATTRIBUTES.Pattern#" /> <!--- Catch an errors that occur for paraming. ---> <cfcatch> <!--- The param did not work. Check to see if there is a "catch" value that we should use. ---> <cfif StructKeyExists( ATTRIBUTES, "Catch" )> <!--- There is a catch value, but we have to param that to make sure it is of the correct type as well. ---> <cfparam name="ATTRIBUTES.Catch" type="#ATTRIBUTES.Type#" default="#ATTRIBUTES.Catch#" min="#ATTRIBUTES.Min#" max="#ATTRIBUTES.Max#" pattern="#ATTRIBUTES.Pattern#" /> <!--- If we have gotten this far, then our catch value can be used as the default value for our variable. Remember to set the value in the CALLERs scope. ---> <cfset "CALLER.#ATTRIBUTES.Name#" = ATTRIBUTES.Catch /> <cfelse> <!--- No catch value was provided, so rethrow error back to the calling page. ---> <cfrethrow /> </cfif> </cfcatch> </cftry> <!--- We only want to execute this tag once (with or without an end tag). Exit out. ---> <cfexit method="EXITTAG" /> </cfsilent>
Now, to demonstrate the cool powers of this CATCH attribute (this demo assumes that my ColdFusion custom tag is names "param.cfm"):
<!--- Set defualt ID that we know will fail paraming. ---> <cfset intID = "a" /> <!--- Param the value. ---> <cf_param name="intID" type="numeric" default="0" catch="5" /> <!--- Output. ---> ID: #intID# <!--- Set a value that we know will not fail. ---> <cfset intID = 13 /> <!--- Param the value. ---> <cf_param name="intID" type="numeric" default="0" catch="5" /> <!--- Output. ---> ID: #intID#
Notice that in the first Param case, the current value of intID, which is "a," will NOT validate as numeric. This validation throws an error which is caught internally by the CF_Param tag. That tag, then applies the CATCH value of "5" to the intID variable. Notice in the Param tag code that the Catch value is also validated in the same way.
Then, on the second go-round, the intID value is valid and the CATCH attribute is ignored.
Am I only the only one who thinks this would rock the pants of the ColdFusion CFParam tag? Notice how easy it would be to validate and set values on a per-variable basis. Probably not for Scorpio, but I have my fingers crossed for ColdFusion MX 9.
Want to use code from this post? Check out the license.
I think you need this http://www.adobe.com/cfusion/mmform/index.cfm?name=wishform
Why not just:
<cfif not isDefined(paramName) and not isValid(paramName,validationType)>
<cfset paramName = defaultValue />
Oops, isValid() has its args the other way round (type first, then value).
And it should be 'or' not 'and'.
Sheesh, I must need more coffee!
Thanks, just submitted.
I don't do that because you are using:
1. One CFIF statment.
2. Two logical NOTs.
3. One logical AND (..OR).
4. Two method calls.
5. Two instances of the value paramName.
... and if I had a "catch" attribute, all I would need is:
1. One additional attribute ( priceless ).
Am I crazy? To me this seems brilliant!
Now, how does your methodology compare to my CFTry / CFCatch methodology? There is probably a bit more overhead to generated the Exception object - but that is just a guess. But, to be honest, when it comes to making sure a value exists, I am generally not concerned about performance - I don't have a million validations on a page (20 would be a LOT)... I am talking more aboue convenience.
...because throwing an exception is a very expensive operation that can be avoided by using a conditional?
I personally hardly ever use <cfparam>... not so much, because it is lacking a feature, but, because I got used to IsDefined().
Plus, isDefined() executes much faster than <cfparam>. I did some tests a couple of years ago and on my machines IsDefined() would be about 100 times faster than <cfparam>.
On the other hand, the user would only notice the difference if there were literally 1000s of <cfparams> (The user won't notice 100 ms difference in page loading.) So I guess it all comes down to personal coding preference. ;-)
Chris, as far as I'm aware structKeyExists() executes faster than isDefined().
This is because isDefined() searches each scope in turn, but with structKeyExists() you specify the scope (i.e. the structure) to search.
e.g. <cfif not structKeyExists(url,"foo")>...</cfif>
of course, searching scopes takes time, but, nothing stops you from specifying a scope with isDefined(), too. ;-)
<cfif NOT IsDefined("url.foo")>
Actually, with the exception of the variables scope I personally scope all variables. (I'm just too lazy for scoping local variables. ;-P)
Chris, I could be wrong but I think that isDefined("url.foo") will first search the variables scope for "variables.url.foo"
I wasn't aware that IsDefined() behaved like that, thanks for pointing that out!
I got curious and just for the fun of it wrote a small test to compare the two functions. I found that when searching the variables scope, IsDefined() is usually faster, when searching other scopes, StructKeyExists() is faster most of the time.
But, as I said before, the difference is so small that I'd always prefer readability over a couple of ms. It was like 0.2 secs difference max for 10000 function calls... now how realistic a scenario is that? ;-))
And although <cfparam> is mucgh slower, even that difference will not be noticed by any user in real world examples.
But I have to admit... sometimes it IS fun to play around like this. ;-P
So maybe I am using CFParam wrong? I think it's an amazing tag, but maybe its not all its cracked up to be in my mind. I usually use it like this:
<!--- Top of a FORM based page. --->
<cfparam name="FORM.first_name" type="string" default="" />
<cfparam name="FORM.last_name" type="string" default="" />
<cfparam name="FORM.phone" type="string" default="" />
<cfparam name="FORM.fax" type="string" default="" />
<cfparam name="FORM.email" type="string" default="" />
Are you guys looking at this and shaking your heads? Would you replace all this with IsDefined() and conditional statements? This just seems to small and easy to read.
Another more significant benefit of isDefined + isValid over try/catch:
(Can't remember if this is the cfparam/default or cfargument/default), but I'm sure I read somewhere that the default value is evaluated even when its not actually used. So if you had:
<cfparam name="bob" value="#readNext('Bob')#"/>
then the function executes even if bob is actually defined, and I think Sean's suggestion would allow this to be bypassed, which is what I think would often be required?
But... would there be any benefit in doing both Sean & Ben's stuff together?
ie: first the lightweight conditional, and then the try-catch as a backup.
This is assuming that
1) the try-catch is only heavy/expensive if an exception is generated.
2) there might be something that could throw an error with the isDefined/isValid stuff.
I can't think of any reason for it, but it can be nice to have a safety net, right?
"<cfparam name="bob" value="#readNext('Bob')#"/>"
I think you might have meant the "default" attribute:
<cfparam name="bob" default="#readNext('Bob')#"/>
... That gets executed no matter what.
I don't know if one could say "using it wrong".
Come to think of it, I use <cfparam> on form pages only, when I want to prefill a form control:
<cfparam name="form.foo" default=""/>
<input type="text" name="foo" value="#form.foo#"/>
My apps are usually CFC based, Fusebox + CFCs that is, not so much OO. So I pass the complete attributes struct to a method and inside that method I check with isDefined()
<invoke object="fooBar" methodcall="saveObject(argumentcollection=attributes)" returnvariable="error"/>
<cfset var whatever=""/>
<cfif NOT IsDefined("arguments.foo")>
... do something
Actually I think that StructKeyExists() would be better here, not because of performance, but, because I pass a struct into the method and want to know if a specific key exists... so StructKeyExists() seems not only more granular but also more consistent.
I'm with you, Ben. I find it much more readable to "group" my parameters at the top of the page. Sort of takes the place of explicitly declaring stuff, I suppose.
When I know something is (or might be) coming in, I like to state that at the top of my processing. Partly for error trapping/initialization and partly for my own awareness. Keeps me from forgetting stuff and protects me from myself. Having isDefined() calls scattered throughout the code isn't very clean to me.
I tend not to use the type functionality, though, since I have to catch errors anyway, I'd rather just catch the error when I try to do something with it. For the most part, I don't think parameter type validation errors should be fatal unless they create a runtime error that is, in fact, fatal. For most of my work, I prefer to let errors just alter my course rather than halt processing entirely.
Screw what everyone else is saying, I think it's brilliant :)
Ha ha, finally a voice of reason ;)
<Ben>I think you might have meant the "default" attribute:</Ben>
Ah crap! I always do that. :(
I think it would be nifty, too. But I never put try/catch around cfparams so it wouldn't be as time-saving for me. It would allow allow you to replace something like this:
<cfif not (isDefined("name") and reFindNoCase("^pattern$", name))>
<cfset name = default />
<cfparam name="name" type="regex" pattern="pattern" default="default" catch="default" />
Naturally you use the pattern attribute cause you are a reg-ex badass :) Yeah, it seems like hardly anyone "validation" features of the CFParam tag. Looking back, I can't remember how / why I started to use it that way. I must have learned it somehow. I wonder if it's cause I really got into CF hardcore after the tag was introduced and my mind wasn't clouded with IsDefined() and what not. (not to say clouded in a bad way, more just as a non-influential way).
"Yeah, it seems like hardly anyone "validation" features of the CFParam tag."
Personally I've never used the validation features of the cfparam tag or the isValid function (other than type validation like for query, array, or integer) because the documentation doesn't show the actual rules behind of the more vague validation types. For example, cfparam/isValid can supposedly validate an email address. What the hell!? Why would I trust the rules for a valid email address that I can't see?
I'm sure the ColdFusion engineers realize that people have come up with literally thousands of regexes to validate email addresses, with varying degrees of accuracy. If I'm going to use a regex to validate an email address (or other things they have on the list like "telephone"), I want to see the rules behind it to ensure it actually meets my needs/expectations.
In order to really validate an email address according to the official spec, you'd have to use a regex like the one shown at http://www.regular-expressions.info/email.html under the heading "The Official Standard: RFC 822", which is over 6300 characters long, and it matches so much crazy stuff that it's completely impractical in the real world anyway.
I really only use the numeric type validation as the only validation. All other validation is done outside of the CFParam tag. As far as the IsValid() is concerned, I used to write my own regex for it, but it required an additional UDF - IsValidEmail(). I understand what you are saying about not seeing what is going on underneath, but I like the idea of being able to leverage the built-in functionality as much as possible. If for no other reason, it makes the code smaller (I have to scope all my UDFs).
One piece of good news is that IsValid( "email" ) stops more emails that would otherwise cause CFMail to break:
Just curious. *Why* would you want to default a value if it fails validation? Going to the extreme here, but if I was filling out a "Donate to Ben" form and I put "Foo" in the amount box that you happened to be using a CFPARAM for that required it to be numeric, I would hope you would show me an error vs. putting a CATCH amount of 500 in for me. The point is what was the users intent. If I don't pass anything and you use a default value, that's one thing, but if the user actually supplies a value and you cannot interpret what their input means, then you should ask for new input.
That is a good question. The default value used after the validation exception would still not be a valid value for form processing. The difference is that by using the catch, I know that it will at least be a valid type.
Take this example, which is where I would use this most of the time: User has a drop down box for status and let's say these are the values:
0 - "Select a Status"
1 - "Active"
2 - "Inactive"
3 - "Pipeline"
Now, this "Status" is a required field such that the user will get a form validation message if they try to submit with 0 (zero) value selected. Now, 99.9999% of the type there will never be a data TYPE issue since its a drop down menu. But, let's say someone is being malicious and TRIES to break the form by setting the value at client-run-time to "" or a SQL injection statement or something.
At that point, the CFparam tag would fail:
<cfparam name="FORM.status_id" type="numeric" default="0" />
.. since the type is no longer numeric. Then the CATCH attribute could re-default it to zero.
At that point, the form still has to go through validation where we will check for a valid Status id.
So you see, the idea here is that we more assertive TYPE checking on the form value. This is NOT TO BE CONFUSED with data validation, which is checking the valid value (not types).
It's just a very convenient way of what other people are doing with multiple lines of code (IMO).
Ben, I'm TOTALLY with you on this.
Using cfparam is both elegant (to look at) and also nasty (to handle errors).
Using multiple isValid and isDefined cfif staements is not my idea of fun. An additional attribute would be a welcome addition.