MillionMunkeys.net

 

PiMunkey Documentation ->

A Pi Component is a ColdFusion object the conforms to the Property-Invocation ("Pi") programming standard. Property-Invocation is the next evolution beyond "Event-Driven Architecture", the programming philosophy behind most of the major ColdFusion frameworks. Event-driven is a very powerful approach, but the problem is that if you forget an event in your design, you might paint yourself into a corner. If you forget to leave a plugin point, you may be stuck with a major rewrite to add it later.

Property-Invocation instead builds on the "Java Bean" idea of using a "getter" and "setter" for every property, and leverages that to provide an infinite number of plugin points. Every property is set using the "set" function and retrieved using the "get" function. Then every function is either a "listener" that gets fired when a property is set or a "filter" that gets fired when a property is retrieved.

Structs vs. Arrays

Collections of data in ColdFusion are commonly stored in either structures or arrays. With Pi Programming, both are handled by the same object.

Arrays

Ordered arrays are not accessed by named attributes, but by their index within the object. You can use the "add", "insertAt", and "remove" functions to add and remove properties based on their location in the object, rather than their name.

Example:

<cfset var site = newMunkey() />
<cfset var page1 = newMunkey(title="Page1") />
<cfset var page2 = newMunkey(title="Page2") />
<cfset var index = site.add(page2) />
<cfset site.insertAt(index,page1) />
<cfset site.remove(2) />

Result:

The above example creates two pages and adds them to a Pi Array called "site". The second page is added first by appending it to the end of the array. The first page, in this example, is then inserted before the second page, putting the pages in the correct order. As a last step, the second page is then removed.

Structs

Named properties are added by their IDs, and can be accessed in the same way. They can also be added one at a time or in batches.

Example:

<cfset page = newMunkey() />
<cfset page.set("id","page1") />
<cfset page.set(title="First Page", template="firstpage.cfm", group="products") />
<cfset page.remove("group") />
<cfreturn page.get("title") />

Result:

The above example sets four properties, "id", "title", "template", and "group", then removes the "group" property and returns the "title" property.

Listeners vs. Filters

Listeners

Listeners are fired whenever the set() function is called. They can be either "global listeners" or "property listeners". Global Listeners are fired whenever anything changes on a Pi Component. They are most commonly used with Pi Components acting as Arrays, but can also be used Pi Components acting as structs. Property Listeners, in contrast, are only fired when one particular property changes on the object. The possibilities are almost limitless on how to use a listener. You can use them to update other values when a key value changes, or to override or rollback a change when certain conditions occur. When using "global listeners" on a Pi Array, you can ensure that certain actions happen, regardless of where in your code an item is added to the Array. Some other coder on your team could create an entirely new page that adds objects to your Pi Component, and you don't have to worry about them remembering to also include your special processing code. It will automatically execute.

For more details and examples, see the sections below on "addListener" and "removeListener".

Filters

Filters are fired whenever the get() function is called. As with listeners, they can be either "global filters" or "property filters". Global Filters intercept any request made on a Pi Component. Property filters intercept requests for only one particular property on a Pi Component. You can use filters mask or hide information, or to add additional information based upon the requestor. You can also use them to create "virtual properties," i.e. add a filter for a property that doesn't actually exists. If someone requests that property, your filter method can instead calculate an answer and return it to the requestor. And as with listeners, you setup your filters on your objects and then it doesn't matter who uses your objects. Someone else on your team could create new code that requests a property or an object, and they won't have to remember to do any special processing or conditional code. They just request the property and use the result.

For more details and examples, see the sections below on "addFilter" and "removeFilter".

Unanticipated Extendability

The extra benefit of listeners and filters is the unanticipated tangle. Perhaps you're two weeks from delivery and the client says that they forgot that they need an audit trail. Anytime anything changes on a request, they want to log it to a file. There's no telling how many places in your code might edit the object. With Pi Components, though, it's no problem. Just add a global listener to your collection, which listens to anything object added to it and writes to the log file whenever any property on any item is modified.

This is the tragic flaw of "event-driven" methodology. If you had never added an "edit" event to your objects, you would still have to go through every place in your code where the object is edited and fire this new "edit" event. With Property-Invocation, everything is a potential plugin point, with no additional work and little to no additional overhead in execution time.

Methods

  • add: Appends values to the end of a PiComponent when used as an Array.

    Usage:

    add (any item1 [, any item2, ... any itemN]) : Pass one or more items to be added to the end of the array. The items may be of any data type. To add items to the beginning or in the middle of an array, use the "insertAt" function.

    Example:

    code:
    <cfset numbers = newMunkey(1,3,5,7) />
    <cfset numbers.add(2,4,6) />

    result: 1,3,5,7,2,4,6

    NOTE: The add() function will not accept named attributes.

  • addFilter: Adds a filter function to either the entire object as a whole, or a specific property within the object. NOTE: You may add a filter to a property even if the property doesn't exist yet. It will still be fired whenever that property is requested from the Pi Component, and regardless of whether the property actually exists or not.

    Usage:

    1. addFilter (Object object, string method) : Use two arguments to add a "global filter" that fires whenever any item is requested from the Pi Component. This format is particularly useful when using Pi Components as arrays.
      • Object object : The first argument must be a valid ColdFusion object. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do.
      • string method : The second argument must be the name of a method within the object specified in the first argument.
    2. addFilter (string property, Object object, string method) : Use three arguments to add a "property filter" that fires whenever the specified property is requested from the Pi Component.
      • string property : The first argument is the name of a property within the Pi Component. NOTE: You may add a filter to a property even if the property doesn't exist yet. It will still be fired whenever that property is requested from the Pi Component, and regardless of whether the property actually exists or not.
      • Object object : The second argument must be a valid ColdFusion object. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do.
      • string method : The third argument must be the name of a method within the object specified in the second argument.

    Filter Method Arguments:

    1. object: A reference to the Pi Component.
    2. property: The name of the property being requested. NOTE: This argument is always passed to the filter, regardless of whether the filter is a "global filter" or a "property filter".
    3. value: The value of the property being returned. If the property does not exist, this value will be the empty string.

    Filter Method Result:

    If you do not use a <cfreturn> tag within your filter method, then the "value" argument passed to your method will be passed back to the calling template unmodified. If you do use a <cfreturn> tag within your filter method, however, you will override the value being passed into your filter method. This will not change the value of the item in the Pi Component, but it will change the value returned to the calling template.

    CAUTION: If you edit the value of the property in the Pi Component, but do not return the new value with a <cfreturn> tag, then the calling template will still receive the original value.

    Examples:

    In this first example we're going to add a global filter to an array of requests in a workflow. Anytime a request is retrieved we want to assign that request to the user so that other people don't try to claim it.

    Array Filter Setup Code:
    <cfset requestManager.addFilter(AccessController, "setOwnerId") />

    Global Filter Method:
    <cfcomponent hint="Manages Access and Permissions to Objects">
         ...
         <cffunction name="setOwnerId">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="value" />
              <cfset var object = Arguments.value />
              <cfset object.set("ownerId", GetAuthUser()) />
         </cffunction>
         ...
    </cfcomponent>

    In this second example we're going to add a filter to non-existant "user_status" property. Whenever someone requests this property, we're going to instead show them the user's first name, last name, and the number of requests that they are currently processing.

    Property Filter Setup Code:
    <cfset this.addFilter("user_status", this, "getCurrentStatus") />

    Property Filter Method:
    <cfcomponent hint="User Object">
         ...
         <cffunction name="getCurrentStatus">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="value" />
              <cfreturn "#this.get('first_name')# #this.get('last_name')# (#this.get('myRequests').getLength()#)" />
         </cffunction>
         ...
    </cfcomponent>

  • addListener: Adds a listener function to either the entire object as a whole, or a specific property within the object. NOTE: You may add a listener to a property even if the property doesn't exist yet. It will be fired when the property is added to the object.

    Usage:

    1. addListener (Object object, string method [, boolean applyToExisting]) : Use two arguments (with an optional boolean third argument) to add a "global listener" that fires whenever any property is modified on the Pi Component. This format is particularly useful when using Pi Components as arrays.
      • Object object : The first argument must be a valid ColdFusion object. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do.
      • string method : The second argument must be the name of a method within the object specified in the first argument.
      • boolean applyToExisting : [default = true] The optional third argument determines whether the method should be immediately fired after being added. If the Pi Component is not empty, the listener function will be fired once for each item in the object.
    2. addListener (string property, Object object, string method [, boolean applyToExisting]) : Use three arguments (with an optional boolean fourth argument) to add a "property listener" that fires whenever the specified property is modified on the Pi Component.
      • string property : The first argument is the name of a property within the Pi Component. NOTE: You may add a listener to a property even if the property doesn't exist yet. It will be fired when the property is added to the component.
      • Object object : The second argument must be a valid ColdFusion object. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do.
      • string method : The third argument must be the name of a method within the object specified in the second argument.
      • boolean applyToExisting : [default: true] The optional fourth argument determines whether the method should be immediately fired after being added. It does not matter if the property exists or not, the method will always be executed.

    Listener Method Arguments:

    1. object: A reference to the object being modified.
    2. property: The name of the property being modified. NOTE: This argument is always passed to the listener, regardless of whether the listener is a "global listener" or a "property listener".
    3. oldValue: The value of the property before it was modified. If the property did not previously exist, this value will be the empty string.
    4. newValue: The value of the property after it was modified. If the property was removed, the value will be the empty string.

    Listener Method Result:

    If you do not use a <cfreturn> tag within your listener method, then the "newValue" argument passed to your method will be stored in the Pi Component. If you do use a <cfreturn> tag within your listener method, however, you will override the value being saved.

    NOTE: The value of the property has already been set by the time it reaches your listener method. If you directly edit the value of the property in the Pi Component, but do not return a value with a <cfreturn> tag, there's no need to worry. Your edit will NOT be overwritten after exiting your method.

    Examples:

    In this first example we're going to add a global listener to an array of pages. Anytime a new page is added, we want to dynamically generate a unique URL for the page based on its page ID. We're also going to run this method against any pages already in the array.

    Array Listener Setup Code:
    <cfset pages.addListener(TrafficMunkey, "addURLVariable", true) />

    Global Listener Method:
    <cfcomponent>
         ...
         <cffunction name="addURLVariable">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfset var newPage = Arguments.newValue />
              <cfif not newPage.exists("url") and newPage.exists("pageId")>
                   <cfset newPage.set("url", "#this.get('baseURL')#?#this.get('urlVariable')#=#newPage.get('pageId')#") />
              </cfif>
         </cffunction>
         ...
    </cfcomponent>

    In this second example we're going to add a listener to a single page object. If its page ID is ever changed, we want to update its URL with a new unique URL, based on the new page ID. We also want remove its old ID from the page lookup list, and register it under its new ID. However, if the ID was deleted, then we don't want to register this page anymore, just remove it from the page lookup entirely.

    Property Listener Setup Code:
    <cfset page.addListener("pageId", TrafficMunkey, "addURLVariable", true) />

    Property Listener Method:
    <cfcomponent>
         ...
         <cffunction name="addURLVariable">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfset var pageLookup = this.get("pageLookup") />
              <cfset pageLookup.remove(Arguments.oldValue) />
              <cfif Arguments.newValue neq "">
                   <cfset Arguments.object.set("url", "#this.get('baseURL')#?#this.get('urlVariable')#=#Arguments.newValue#") />
                   <cfset pageLookup.set(Arguments.newValue, Arguments.object) />
              <cfelse>
                   <cfset Arguments.object.remove("url") />
              </cfif>
         </cffunction>
         ...
    </cfcomponent>

    In this third example we're going to add a global listener to a single page object. If anything changes, we want to mark it so we can save it to the database later.

    Global Listener Setup Code:
    <cfset page.addListener(DataAccessLayer, "isDirty", false) />

    Global Listener Method:
    <cfcomponent hint="Data Access Layer">
         ...
         <cffunction name="isDirty">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfset Arguments.object.set("isDirty", true) />
         </cffunction>
         ...
    </cfcomponent>

  • exists: Returns a boolean value indicating whether or not the specified property exists within the object.

    Usage:

    1. exists (integer index) : Indicates whether or not an item exists at the specified position in the object.
    2. exists (string propertyName) : Indicates whether or not there is an item associated with the given name.

    Examples:

    <cfif object.exists(1)>
         <cfset object.insertAt(1, newItem) />
    <cfelse>
         <cfset object.add(newItem) />
    </cfif>

    <cfif not object.exists("url") and object.exists("id")>
         <cfset object.set("url", "#this.get('baseURL')#?#this.get('urlVariable')#=#object.get('id')#") />
    </cfif>

  • get: Returns either the value of a property, the item at the specified index, or the empty string if nothing found.

    Usage:

    1. get (string propertyName) : Pass a string to the "get" function and it will return the item associated with that property name. If no item exists, it will return the empty string.
    2. get (integer index) : Pass an integer to the "get" function and it will return the item at that position in the object. If no item exists, it will return the empty string.

    Example:

    <cfloop from="1" to="#object.getLength()#" index="i">
         <cfset nextItem = object.get(i) />
         <cfset titleList = ListAppend(titleList, nextItem.get("title")) />
    </cfloop>

  • getIndex: Returns the index of the property's numeric position within the object. Returns zero if the property is not found.

    Usage:

    getIndex (string propertyName)

    Example:

    code: <cfset index = object.getIndex("title") />
    result: 2

  • getLength: Returns the total number of properties of this object. This function is most useful when the PiComponent is being used as an Array, but it may also be used with named properties as well.

    Example:

    <cfloop from="1" to="#object.getLength()#" index="i">
         <cfset titleList = ListAppend(titleList, object.get(i).get("title")) />
    </cfloop>

  • getProperty: Returns the name of the property at the given index. Returns the empty string if there is no property at the given index.

    Usage:

    getProperty (integer index)

    Example:

    code: <cfset prop = object.getProperty(1) />
    result: "id"

  • getPropertyList: Returns a list of all of the properties of this object.

    Usage:

    1. getPropertyList ( ) : Returns a list of all the unique property names within the object.
    2. getPropertyList (string propertyName) : If the PiComponent is being used as an Array of other PiComponents, or as an array of Structs, you can optionally pass the name of a property to build a list of the values of that property for each item in the Array.

    Examples:

    code: <cfset propertyList = object.getPropertyList() />
    result: "id,title,template"

    code: <cfset titleList = pages.getPropertyList("title") />
    result: "Creating Your Application.cfc,Creating a Site Template,Adding Breadcrumbs"

  • getUUID: Returns a ColdFusion UUID to uniquely identify the PiComponent. It's perfect to checking if two variables are equal to each other.

    Example:

    <cfif object1.getUUID() eq object2.getUUID()>
         <!--- Same Object! --->
    </cfif>

  • insertAt:

    Usage:

    insertAt (integer index, any item1 [, any item2, ... any itemN]) : The first argument is always a ColdFusion-type numeric index, i.e. starting at 1 and going up to the length of the array. The items passed will be inserted into the array before the specified index position. The index is then followed by one or more items to be added to the array at the desired position. The items may be of any data type. To add items to the end of an array, use the "add" function.

    Example:

    code:
    <cfset numbers = newMunkey(1,3,5,7) />
    <cfset numbers.insertAt(2,2) />
    <cfset numbers.insertAt(4,4,6) />

    result: 1,2,3,4,6,5,7 (Note that the numbers 6 and 5 are reversed, because 4 and 6 were both inserted before 5.)

    NOTE: The insertAt() function will not accept named attributes.

  • newMunkey: A shortcut function for any child of the PiComponent that returns a new instance of the base PiComponent class, and not the child class. To get a new instance of the child class use the 'new' function. WARNING: This function should never be overridden by a child class!

    Usage:

    1. newMunkey ( ) : Call the function without arguments to get an empty PiComponent.
    2. newMunkey (propertyName1 = any item1 [, propertyName2 = any item2, ... propertyNameN = any itemN]) : If you use the named attribute syntax in ColdFusion, the returned object will be initialized with the arguments passed. Each argument name will be used as the name of the property, and the value of the argument will be used as the associated item.
    3. newMunkey (ArgumentCollection = Struct collection) : If you use the ArgumentCollection syntax in ColdFusion, the returned object will be initialized with the values of passed structure. The name of each member of the passed structure will be used as the name of the property, and the value of each member of the structure will be used as the associated item.

    Examples:

    Each of the following examples will produce the same object structure:

    <cfset object = newMunkey() />
    <cfset object.set(id="page1", title="Page One", template="first.cfm") />

    <cfset object = newMunkey(id="page1", title="Page One", template="first.cfm") />

    <cfset args["id"] = "page1" />
    <cfset args["title"] = "Page One" />
    <cfset args["template"] = "first.cfm" />
    <cfset object = newMunkey(ArgumentCollection=args) />

    Caller Page Code: <my:customTag id="page1", title="Page One", template="first.cfm" />
    Custom Tag Code: <cfset object = newMunkey(ArgumentCollection=Attributes) />

  • remove: Removes the specified property from the object.

    Usage:

    1. remove (integer index) : Removes the item at the specified position in the object.
    2. remove(string propertyName) : Removes the item with the associated name.

    Examples:

    code:
    <cfset numbers = newMunkey(1,3,5,7) />
    <cfset numbers.remove(4) />
    <cfset numbers.remove(2) />

    result: 1,5

    code:
    <cfset page = newMunkey(id="page1", title="Page One", template="first.cfm", group="Lesson 1") />
    <cfset page.remove("group") />
    <cfset properties = page.getPropertyList() />

    result: The "properties" variable contains "id,title,template" because "group" is now gone.

  • removeFilter: Removes a filter function from the Pi Component. NOTE: If the filter is not actually listening to this Pi Component, then this function call will just be ignored.

    Usage:

    1. removeFilter (Object object, string method) : Use two arguments to remove a "global filter".
      • Object object : The first argument must be a valid ColdFusion object that is listening to this Pi Component. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do. NOTE: If the filter is not actually listening to this Pi Component, then removeFilter will just be ignored.
      • string method : The second argument must be the name of a method within the object specified in the first argument.
    2. removeFilter (string property, Object object, string method) : Use three arguments to remove a "property filter".
      • string property : The first argument is the name of a property within the Pi Component.
      • Object object : The second argument must be a valid ColdFusion object that is listening to this Pi Component. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do. NOTE: If the filter is not actually listening to this Pi Component, then removeFilter will just be ignored.
      • string method : The third argument must be the name of a method within the object specified in the second argument.

    Examples:

    <cfset requestManager.removeFilter(AccessController, "setOwnerId") />

    <cfset this.removeFilter("user_status", this, "getCurrentStatus") />

  • removeListener: Removes a listener function from the Pi Component. NOTE: If the listener is not actually listening to this Pi Component, then this function call will just be ignored.

    Usage:

    1. removeListener (Object object, string method) : Use two arguments to remove a "global listener".
      • Object object : The first argument must be a valid ColdFusion object that is listening to this Pi Component. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do. NOTE: If the listener is not actually listening to this Pi Component, then removeListener will just be ignored.
      • string method : The second argument must be the name of a method within the object specified in the first argument.
    2. removeListener (string property, Object object, string method) : Use three arguments to remove a "property listener".
      • string property : The first argument is the name of a property within the Pi Component.
      • Object object : The second argument must be a valid ColdFusion object that is listening to this Pi Component. It can be a Pi Component itself, but it does not need to be. Any ColdFusion object will do. NOTE: If the listener is not actually listening to this Pi Component, then removeListener will just be ignored.
      • string method : The third argument must be the name of a method within the object specified in the second argument.

    Examples:

    These first two calls will clean up the listeners added during the examples in the addListener section of this documentation.

    <cfset pages.removeListener(TrafficMunkey, "addURLVariable") />

    <cfset page.removeListener("url", TrafficMunkey, "addURLVariable") />

    This next example assumes that IDs will not change, and so once it's done it's job it will remove itself. In other words, this method will only ever be executed once per object.

    Property Listener Setup Code:
    <cfset page.addListener("pageId", TrafficMunkey, "addURLVariable", true) />

    Property Listener Method:
    <cfcomponent>
         ...
         <cffunction name="addURLVariable">
              <cfargument name="object" />
              <cfargument name="property" />
              <cfargument name="oldValue" />
              <cfargument name="newValue" />
              <cfif Arguments.newValue neq "">
                   <cfset Arguments.object.set("url", "#this.get('baseURL')#?#this.get('urlVariable')#=#Arguments.newValue#") />
                   <cfset Arguments.object.removeListener("pageId", this, "addURLVariable") />
              </cfif>
         </cffunction>
         ...
    </cfcomponent>

  • set: A function for direct assignment of properties, singly or in bulk.

    Usage:

    1. set (string propertyName, any item) : If you use unnamed arguments, the first argument is the unique name of the property within the object, and the second argument is what to store there. The second argument can be of any data type you want.
    2. set (propertyName1 = any item1 [, propertyName2 = any item2, ... propertyNameN = any itemN]) : If you use the named attribute syntax in ColdFusion, each argument name will be used as the name of the property, and the value of the argument will be used as the associated item.
    3. set (ArgumentCollection = Struct collection) : If you use the ArgumentCollection syntax in ColdFusion, the name of each member of the passed structure will be used as the name of the property, and the value of each member of the structure will be used as the associated item.

    Examples:

    Each of the following examples will produce the same object structure:

    <cfset object.set("id", "page1") />
    <cfset object.set("title", "Page One") />
    <cfset object.set("template", "first.cfm") />

    <cfset object.set(id="page1", title="Page One", template="first.cfm") />

    <cfset args["id"] = "page1" />
    <cfset args["title"] = "Page One" />
    <cfset args["template"] = "first.cfm" />
    <cfset object.set(ArgumentCollection=args) />

    Caller Page Code: <my:customTag id="page1", title="Page One", template="first.cfm" />
    Custom Tag Code: <cfset object.set(ArgumentCollection=Attributes) />

 

 

Follow millionmunkeys on Twitter

Copyright ©2010, MillionMunkeys® LLC, All Rights Reserved