Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Garrett Johnson
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Garrett Johnson

Getting Flattened Component Metadata In ColdFusion

By
Published in

Over the years, I've learned to favor composition over inheritance in my ColdFusion programming. To power this Inversion of Control (IoC) pattern, I use a simple component that provides a dependency injection (DI) container. This component has worked well until I recently tried to use inheritance in part of my ColdFusion data modeling. It turns out that ColdFusion's getMetadata() built-in function (BIF) returns data in a hierarchical structure that doesn't play nicely with my simplified injector mechanics.

To demonstrate, here are three ColdFusion components that create an inheritance chain. First, Alpha.cfc. This component has a unique property, a unique method, and a generic method that will be overridden by concrete components:

component {

	property name="alpha" default="alphaValue";

	public void function alphaFunc() { /* .. */ }
	public void function genericFunc() { /* .. */ }

}

Then, Bravo.cfc, which extends Alpha.cfc and overrides the "generic" method:

component extends = "Alpha" {

	property name="bravo" default="bravoValue";

	public void function bravoFunc() { /* .. */ }
	public void function genericFunc() { /* .. */ }

}

Then, Charlie.cfc, which extends Bravo.cfc and also overrides the "generic" method:

component extends = "Bravo" {

	property name="charlie" default="charlieValue";

	public void function charlieFunc() { /* .. */ }
	public void function genericFunc() { /* .. */ }

}

If we instantiate the Charlie.cfc and get its metadata:

writeDump( getMetadata( new Charlie() ) );

... we'll see that it gets reported as a chain of parent-child relationships indicated by the extends key:

With this nested metadata structure, the properties and functions aren't reported as aggregate collections. Instead, each layer within the inheritance chain has its own local properties and functions specific to that physical CFC file.

On its own, this nested structure isn't an issue. However, if we're using the metadata to drive meta-programming in ColdFusion, reading the component metadata becomes more complicated. To get the aggregate picture of what properties and functions a component contains, we need to walk up the inheritance chain looking at each file's local properties and functions collection.

To demonstrate, I've created a getFlatMetadata() user-defined function (UDF) that returns a properties and functions collection that each contain the aggregate of all properties and functions, respectively, in the inheritance chain. Since the chain traversal starts at the "most concrete" metadata and gradually walks up towards increasingly "more abstract" metadata, I only collect values that haven't already been defined in a lower part of inheritance chain.

<cfscript>

	writeDump( getFlatMetadata( new Charlie() ) );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I return a flattened version of the given component metadata. The result includes
	* only "properties" and "functions", but injects an "CFC:SOURCE" property in each
	* entry that contains information about the originating component.
	*/
	public struct function getFlatMetadata( required any input ) {

		// In order to make the chain visiting easier, let's create a fake "root" that
		// uses the input as a "parent". This way, the first step in our do-while loop can
		// always be an ".extends" navigation.
		var target = {
			extends: getMetadata( input )
		};
		// Stylistic choice, I'm using ordered structs here so that the "more concrete"
		// entries will be higher-up in the final collections. And, the "more abstract"
		// entries will be lower-down in the final collections.
		var propertyIndex = [:];
		var functionIndex = [:];
		// This is the property that will contain metadata about which component in the
		// chain contained the given property or method.
		var sourceName = "CFC:SOURCE";

		do {

			target = target.extends;

			// Component metadata structures appear to get cached internally to the
			// ColdFusion server. As such, we want to take special care not to directly
			// mutate any of the data returned from the getMetadata() function. This is
			// why we're performing a shallow-copy of the various parts.
			var source = structCopy( target );
			source.delete( "extends" );
			source.delete( "properties" );
			source.delete( "functions" );

			for ( var entry in target.properties ) {

				// Since we're walking up the component chain from more concrete to more
				// abstract, only include properties that haven't already been defined at
				// a more concrete level. 
				if ( ! propertyIndex.keyExists( entry.name ) ) {

					propertyIndex[ entry.name ] = structCopy( entry );
					propertyIndex[ entry.name ][ sourceName ] = source;

				}

			}

			for ( var entry in target.functions ) {

				// Since we're walking up the component chain from more concrete to more
				// abstract, only include functions that haven't already been defined at
				// a more concrete level. 
				if ( ! functionIndex.keyExists( entry.name ) ) {

					functionIndex[ entry.name ] = structCopy( entry );
					functionIndex[ entry.name ][ sourceName ] = source;

				}

			}

		} while ( target.keyExists( "extends" ) );

		return {
			properties: structValueArray( propertyIndex ),
			functions: structValueArray( functionIndex )
		};

	}


	/**
	* I return an array of the values in the given struct.
	* 
	* Note: this function exists natively in Lucee CFML, but not in Adobe ColdFusion.
	*/
	public array function structValueArray( required struct input ) {

		return input
			.keyArray()
			.map(
				( key ) => {

					return input[ key ];

				}
			)
		;

	}

</cfscript>

Note that I'm using structCopy() to create shallow copies of the component metadata structures during the traversal. ColdFusion appears to cache metadata under the hood; so, I like to create a copy such that any changes I make aren't accidentally persisted (and which might impact other meta-programming workflows).

If we run this ColdFusion code, we see a flattened output:

As you can see, all of the properties and functions in the inheritance chain have been rolled up into flat collections; but, only in cases where the given property or function would have been inherited (ie, not overridden) by a more concrete component.

I rarely use meta-programming in ColdFusion. But, in scenarios that involve dependency injection and memory leak detection, this kind of detail becomes relevant.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel