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' SmithName: 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.

Reader Comments

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!

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.

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.

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.