Ask Ben: Using ColdFusion Components As Return Types, Argument Types, And Property Types
Hi Ben! I have been reading your blog for about four years now (since I first played with Skin Spider), and I finally have a question to ask. I have finally started playing with Coldfusion 9, and I must say I'm impressed thus far. That said, I've stumbled upon my first problem that I haven't been able to find a solution to: How do you (or rather, can you) explicitly type return values for functions or inbound arguments to user defined components? I am one of those people who compulsively likely to explicitly type things and I haven't been able to find much about it on the web or in the CF docs. Any input would be fantastic!
I knew that this could be done to some extent; but, since I typically use "any" as my return type for non-built-in data type, I wasn't sure of the ins-and-outs and intricacies of such an approach. So, to answer your question, I had to do a little trial and error. I wanted to make sure that my test included multi-part paths, application-specific mappings, and, as you requested, return types and arguments types. And, in all honesty, I found some kind of cool stuff!
To test the full feature set of using ColdFusion components as data types, I needed to make sure that I had at least one situation where a path-mapping was used. As such, I created the following directory structure:
- ./model/Contact.cfc
- ./vo/ValueObject.cfc
- ./Application.cfc
- ./index.cfm
In the above pseudo file tree, the Contact.cfc and the ValueObject.cfc are located in sibling directories; as such, in order for the Contact.cfc to reference the ValueObject.cfc, it would have to do so through an application-specific mapping (NOTE: application-specific is not required, per say, but it's really the right way to do this). In the Application.cfc, I set up a mapping to both the "model" and "vo" directories:
Application.cfc
<cfcomponent
output="false"
hint="I provide application settings and event handlers.">
<!--- Define the application. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 0, 20 ) />
<!---
Get the root directory of the application. This will
be needed to define the subsequent app-specific mappings.
--->
<cfset this.rootDirectory = getDirectoryFromPath(
getCurrentTemplatePath()
) />
<!--- Define application-specific mappings. --->
<cfset this.mappings[ "/com" ] = (this.rootDirectory & "model/" ) />
<cfset this.mappings[ "/vo" ] = (this.rootDirectory & "vo/" ) />
<!--- Define page request settings. --->
<cfsetting
showdebugoutput="false"
/>
</cfcomponent>
In the above application-specific mappings, you will notice that I mapped "com" to the "model" directory; I did this only to make sure that no pathing shenanigans were taking place behind the scenes. By making sure that the mapping and the directory were not the same name, I could ensure that it was indeed my mapping that was being used to locate the given components.
With these mappings in place, I then created my Contact.cfc ColdFusion component within the model directory. I created the Contact.cfc using the new CFScript-based implementation in part because that is the format that the reader submitted to me; and, I did it in part because it has more options to test (in terms of where things can be defined within the component). In the following code, you will see references to ValueObject.cfc; this is a very simple get/set type component that I'll show you later on. For the moment, though, realize only that it (ValueObject.cfc) can only be referenced using the "vo" mapping:
Contact.cfc
// Import the "vo" name space.
// NOTE: The name space that we are importing is an application-
// specific mapping and that this name-space is used to define the
// return type of one of the functions below.
import "/vo.*";
// Define the component.
component
output="false"
accessors="true"
hint="I am a contact entity."
{
// Define properties.
property
name="name"
type="string"
default=""
validate="string"
validateParams="{ minLength = 1, maxLength = 30 }"
hint="I am the full name."
;
property
name="lastValueObject"
type="vo.ValueObject"
hint="I am the last requested value object."
;
/**
* Define constructor. Notice that the return type of the
* ColdFusion component uses BOTH the app-specific mapping
* to "com" as well as the multi-part, dot-delimitted path
* to THIS component.
*
* @access public
* @output false
* @hint I return an initialized component.
**/
com.Contact function init(
string name = ""
){
// Store default properties.
this.setName( arguments.name );
// Return this object reference. Remember, when used in
// conjunction with the NEW keyword, you must explicitly
// return the object reference.
return( this );
}
/**
* Notice that in this method, we are defining a local path
* to the component (no app-specific mappings) and that the
* returnType attribute is being defined in the comments.
*
* @access public
* @returnType Contact
* @output false
* @hint I return the current contact.
**/
function getContact(){
return( this );
}
/**
* Notice that in this method, the return type "ValueObject"
* is only valid becuase our IMPORT command above imported
* the app-specific mappings / namespace, "/vo". Notice also
* that the default argument value does NOT use the imported
* namespace, but rather, refers back to the multi-part,
* dot-delimtted mapping path.
*
* @access public
* @returnType ValueObject
* @output false
* @hint I return a value-object representation of this entity. If you want to, you can pass-in an existing valueObject instance.
*/
function getValueObject(
valueObject valueObject = new vo.ValueObject()
){
// Store local properties into the given value object.
arguments.valueObject.set( "name", this.getName() );
// Store the value object as a property.
this.setLastValueObject( arguments.valueObject );
// Return populated value object.
return( arguments.valueObject );
}
}
There's actually a good number of features being put to the test here.
Both the return type and argument types can use a multi-part, dot-delimited path. They can also both use name-only paths, so long as the name of the ColdFusion component is readily accessible (in a place where ColdFusion will search for it, such as the current directory).
The path for either the return type or argument type can reference application-specific mappings.
ColdFusion component property "type" values can also use components. This works with both multi-part and name-only paths (although I only demonstrate the multi-part case in the above code).
Using the IMPORT statement before the component definition acts like a package import in Java-style applications and can be used to help define both return type and argument types without referencing the full class name. (NOTE: Import does not need to precede class definition - it can be contained within class definition as well).
IMPORT statements within a ColdFusion component, even those that precede the component definition, can make use of application-specific mappings.
Return type (as well as Access for that matter), can be defined inline with the function definitions; or, it can be defined in the preceding comments. Both multi-part and name-only paths can be used in either place.
Not a bad number of tests for a fairly straight forward question, right? To make sure that this was all working as expected, I then set up a small demo page:
Index.cfm
<!---
Import the COM name space (NOTE: This actually maps to the
"model" directory thanks to our appliation-specific mappings).
--->
<cfimport path="com.*" />
<!--- Create a new Contact instance. --->
<cfset tricia = new Contact( "Tricia Smith" ) />
<!--- Rename contact (only to test accessors). --->
<cfset tricia.setName( "Tricia 'the hottie' Smith" ) />
<!---
Get the contact object (to test the return type is enforced
as a Contact instance).
--->
<cfset contact = tricia.getContact() />
<!---
Get the value object (to test that mapped-return type
based on the IMPORT command used inside the CFC).
--->
<cfset valueObject = contact.getValueObject() />
<cfoutput>
<!--- Output name contained within value object. --->
Name: #valueObject.get( "name" )#<br />
<!--- Output name contained within LAST value object. --->
Name: #tricia.getLastValueObject().get( "name" )#
</cfoutput>
This code simply makes use of the various methods to ensure that nothing would throw an error. And, while I can't easily demonstrate it in the code (see the video above), if I changed the data types, an error was thrown (demonstrating that it was not using "any" as a data type). When I run the code, I get the following page output:
Name: Tricia 'the hottie' Smith
Name: Tricia 'the hottie' Smith
As you can see, ColdFusion components can be successfully used as data types in the property types, return types, and argument types.
In the Contact.cfc code, I referenced the ValueObject.cfc; while this component is completely meta to the conversation, I will show it below in case you were curious as to what it was doing. There was very little thinking behind this component - I just needed something to test:
ValueObject.cfc
<cfcomponent
output="false"
hint="I am a very simple, light-weight value object for any component - I store properties in an expandable map.">
<cffunction
name="init"
access="public"
returntype="any"
output="false"
hint="I return an initialized value object.">
<!--- Set up default properties. --->
<cfset variables.propertyMap = {} />
<!--- Return this object reference. --->
<cfreturn this />
</cffunction>
<cffunction
name="get"
access="public"
returntype="any"
output="false"
hint="I return the given property, or property set.">
<!--- Define arguments. --->
<cfargument
name="name"
type="string"
required="false"
hint="I am the name of the property beging gotten (if omitted, fulle property set is returned)."
/>
<!--- Check to see if property name was included. --->
<cfif isNull( arguments.name )>
<!---
No property was requested; return entire
property map.
--->
<cfreturn variables.propertyMap />
<cfelse>
<!--- Return selected property. --->
<cfreturn variables.propertyMap[ arguments.name ] />
</cfif>
</cffunction>
<cffunction
name="set"
access="public"
returntype="any"
output="false"
hint="I set the given property.">
<!--- Define arguments. --->
<cfargument
name="name"
type="string"
required="true"
hint="I am the name of the property."
/>
<cfargument
name="value"
type="any"
required="true"
hint="I am the property value."
/>
<!--- Set the given property. --->
<cfset variables.propertyMap[ arguments.name ] = arguments.value />
<!--- Return this object reference for method chaining. --->
<cfreturn this />
</cffunction>
</cfcomponent>
Out of this entire experiment, the two features I found the most interesting were the fact that you could use IMPORT before the ColdFusion component definition to help define all aspects of a CFC-as-data-type approach; and, that the return type and access attributes of the function could be declared in the comments preceding the function definitions. In the end though, I just hope this helped point you in the right direction.
Want to use code from this post? Check out the license.
Reader Comments
Thanks Ben!
Wow, this answers my question and at least three others. I was not aware that you could use a Java-style IMPORT statement in my CF components for easy object reference. I'm always in favor of writing less code, and in some of my more complex applications I find myself with a sizable component directory tree. I was also unaware that you could define the attributes of your component's methods in preceding comment meta-data. VERY useful in multiple ways, as it simplifies the appearance of your method declarations for easy visual scanning and sort of forces a commenting convention in your components. It will help me force my team to better comment their components (of course, they should WANT to... lol.)
I greatly appreciate your prompt and thorough efforts in helping me to solve this problem!
@Nick,
Glad to help out! If you have any further questions, drop me a line.
Also, for type safe arrays, you can use:
path.to.MyCFC[] as your return type or parameter type. I don't think it actually checks each array index for that type (I think maybe just the first index).
@Devin,
I think I have heard that too - that it checks only the first index. I think I read a blog post or a tweet along those lines not too long ago, but can't remember where.
Something that I have noticed with the cf9 scripting interface is that you can no longer accept an argument with a defined type. This is something I do quite a bit with flex coding.
As an example I would do something like this:
public com.model.vo.Member function DoSomethingMemberIsh(
required com.model.vo.Member vo)
{
return vo;
}
Ben do you know if there is a work around for this, other than using the type of Any?
@Jeremy,
I am not sure what you are asking? You can certainly require function arguments to be of a certain type. In fact, that is part of what I am doing in this demo.
Am I misunderstanding what you are asking?
@Ben
Lets say I have these two files:
UserVO.cfc
UserService.cfc
UserVO.cfc:
component
{
property name="Firstname" type="string";
property name="Lastname" type="string";
public string function getFirstname()
{
return Firstname;
}
public void function setFirstname(required string Firstname)
{
Firstname=Firstname;
}
public string function getLastname()
{
return Lastname;
}
public void function setLastname(required string Lastname)
{
Lastname=Lastname;
}
}
UserService.cfc
component
{
public com.model.vo.UserVO function DoThis(required com.model.vo.UserVO vo)
{
return vo;
}
}
Having the argument vo type casted as com.model.vo.UserVO does not work with in the scripted cfc in cf9.
You will always get the error:
You cannot use a variable reference with "." operators in this context
@Jeremy,
Ah, I see what you're saying now. I had to run a quick test to double-check that and you are right. You cannot use multi-part paths in your argument types. You can, however, as a work around, import the name space:
import "com.model.vo.*";
... and then use a non-pathed class name:
public UserVO function DoThis(required UserVO vo)
I am personally a fan of full-path names as I find them to be self-documented; but, until they get this fixed, this would be an available work around.
Do you know if this is a known bug?
I sent something very similar over to Ray Camden on day two after the release of CF9. He did say it was a known bug.
As of yet, I have not been able to find a workaround that I liked. Importing the name space is nice, but not something I would like to do with an application as large as the one I am currently working on.
@Jeremy,
I can understand that. In general, I am not a fan of importing name spaces. When I do that, I find that I get confused as to where things are coming from. ... but then again, I also work so often with loosely typed languages that doing anything more than basic date type requirements is not something I am used to either.
@Jeremy,
Thanks again for the insight; I codified your issue in a subsequent blog post:
www.bennadel.com/blog/1798-Using-Multi-Part-Class-Paths-With-CFScript-Based-Argument-Types.htm
Hi,
I tried the same program you have mentioned.
It gives me an error.
11:22:14.014 - Template Exception - in : line -1
Invalid CFML construct found on line 2 at column 21.
The error its refering to is
<cfset tricia = new Contact( "Tricia Smith" )>
Line 2 Colomn 21 is Contact