Custom Functions in ColdFusion (Part I)

 
Aug 28, 2000
by Michael Dinowitz

Let's be honest here. ColdFusion is a great development tool and language with a lot of capability, but it's missing an important piece: the ability to write custom functions. I'm not talking about custom tags or extending ColdFusion's abilities with outside code. I'm referring to a function that can be used in a tag, can be used in other functions and can be outputted to a page. I want to be able to do IsEmail(email) and have it return a yes or no. ColdFusion can't do this.

Actually, ColdFusion can do this, but it's very ugly. Writing custom functions involves hacking the way ColdFusion evaluates variables. To do this, you need an intimate understanding of how ColdFusion evaluates variables, functions and expressions as well as a twisted imagination. Once you have this, hacking custom functions will be relatively easy.

Let's start with a single assumption. We want to take an email address and evaluate if it is 'proper.' That is, we want to see if it is formatted correctly with the proper extensions and lack of 'wrong' characters. After hours of fighting with Regular Expressions (or about two minutes looking through CF-Talk) you have your code.

<CFIF Not
REFindNoCase('^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[:alnum:]][-a-zA-
Z0-9%\>.]*\.[[:alpha:]]{2,}$',email)>
   <CFABORT showerror="Your email address is not formatted properly">
</CFIF>

Now what you really want to do is take all of that and make it into some sort of module. You don't want it to be a custom tag because a custom tag can't be used inside a function or inside a CF tag body. Additionally, you don't want to have to keep referring to that complex Regular Expression for each email check. The solution is to write the RegEx to a server variable in such a way that it can be called later along with some arbitary data--in other words, as a custom function.

Let's start by setting up our function generator. This is the code that will 'pre-load' our function library into server memory for use. It's set up with the proper locking and checks so that it will use as little page resources as possible.

<CFLOCK scope="SERVER" type="READONLY" timeout="5">
<!--- Only set server variables if they do not already exist --->
<CFIF Not IsDefined('Server.Custom')>
  <!--- ALways exclusive lock when setting memory variables --->
  <CFLOCK SCOPE="SERVER" TYPE="EXCLUSIVE" TIMEOUT="3">
    <CFSET Server.Custom=1>

    <!--- Return 0 if email is false or 1 if it is a proper
address --->
    <CFSET
Server.IsEmail="REFindNoCase('^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[
:alnum:]][-a-zA-Z0-9%\>.]*\.[[:alpha:]]{2,}$','">
    <CFSET Server.IsEmailEnd="')">

    <!--- Returns 0 if number is not an US number, 1
otherwise --->
    <CFSET
Server.IsPhone="REFindNoCase('^([0-9]{3}[-_./]?)?[0-9]{3}[-_. /]?[0-9]{4}',
'">
    <CFSET Server.IsPhoneEnd="')">

    <!--- returns a decimal number that can be used in place of
an IP for site lookups --->
    <CFSET
Server.DotLessIP="(ListGetAt(SetVariable('dotlessipworkvar', '">
    <CFSET Server.DotLessIPEnd="'),
1,'.')*16777216)+(ListGetAt(dotlessipworkvar,
2,'.')*65536)+(ListGetAt(dotlessipworkvar,
3,'.')*256)+(ListGetAt(dotlessipworkvar, 4, '.'))">
  </CFLOCK>
</CFIF>
</CFLOCK>

  1. Function 1: Check if an email address is properly formatted. Returns Boolean.
  2. Function 2: Check if a phone number is properly formatted for American use. Returns Boolean.
  3. Function 3: Turn an IP address into its decimal equivilent. Returns Number.
Now to show you how it's actually done. As you may have noticed, each custom function has a beginning and an ending variable. This is because a basic custom function is actually taking a standard ColdFusion function and adding pieces to it on the fly. Once all the pieces are together, it is evaluated by the evaluate() function. This treats the constructed function as a whole and gets the final value, which is returned as the custom function value.

Lets return to our email address checking function and walk it through from a full function to a custom function. The importing thing here is the logic behind what we're doing. Once you have that, you can build custom functions on your own. Lets assume for this example a variable called email with a value of "mdinowitz@houseoffusion.com".

REFindNoCase('^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[:alnum:]][-a-zA-
Z0-9%\>.]*\.[[:alpha:]]{2,}$',email)
This is the regular expression we want to make into a custom function

Evaluate("REFindNoCase('^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[:alnum
:]][-a-zA-Z0-9%\>.]*\.[[:alpha:]]{2,}$',email)")
Start by turning the entire function into a string. The Evalaute() function will turn the string into a function and then evaluate it.
Evaluate("REFindNoCase('^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[:alnum
:]][-a-zA-Z0-9%\>.]*\.[[:alpha:]]{2,}$','"&email&"')")
Knowing this, we can 'slice' the string up into smaller pieces and put them together inside the evaluate() function using simple concatenation.Pay attention to the order of operation within the Evaluate() function AND to the quotes used. Rather than have the email inside quotes, we use a small trick. At the end of the first part of the regex, we have an additional single quote that seems to be hanging. The same is at the beginning of the last segment of the RegEx. The email is totally without quotes and according to the order of operation inside functions, will be evaluated first. After it is evaluated, you'll have a text value that will be concatenated into the regex to look like this:

REFindNoCase('^([[:alnum:]][-a-zA-Z0-9_%\.]*)?[[:alnum:]]@[[:alnum:]][-a-zA-
Z0-9%\>.]*\.[[:alpha:]]{2,}$','mdinowitz@houseoffusion.com')
Now take this to the last step. You have a string containing part of a regular expression. Another string containing the other part of the same expression and all it's expecting is a variable. If we load the two parts into variables as we did above, then we can call the whole thing like so:
Evaluate(Server.IsEMail&email&Server.IsEmailEnd)
This is the full custom function. The start of a RegEx, the variable to be manipulated and the end of the regex. Looks a little ugly, but works suprisingly well.

There's actually a lot more you can do beyond single variable functions. The third function in the library above actually uses a single variable multiple times. I'll wait a bit to explain that, as this technique may take some time to sink in. Within the next week or so I'll post how to use a single variable multiple times in a custom function and how to use multiple variables. Be warned. These are all hacks that work and work well, but are ugly. If you use a custom function, document it. Someone who dosn't know how they work will have an epileptic seizure trying to understand what's going on without it.

A final note. Many functions such as RegEx return numbers. These can be treated as if they give a yes/no result. If you want to return an actual yes or no response, try wrapping the entire custom function call inside a YesNoFormat() function.

<cfset email="mdinowitz@houseoffusion.com">
<cfset IP="207.31.122.140">

<CFOUTPUT>
  #YesNoFormat(Evaluate(Server.IsEMail&email&Server.IsEmailEnd))#
  #YesNoFormat(Evaluate(Server.Isphone&email&Server.IsPhoneEnd))#
  #YesNoFormat(Evaluate(Server.Isphone&'951-3235'&Server.IsPhoneEnd))#
  #Evaluate(Server.DotLessIP&IP&Server.DotLessIPEnd)#
</CFOUTPUT>

Stay tuned for Part II, where Michael discusses more advanced types of custom functions.


Privacy | FAQ | Site Map | About | Guidelines | Contact | Advertising | What is ColdFusion?
House of Fusion | ColdFusion Jobs | Blog of Fusion | AHP Hosting