I must admit to having been excited at the prospect of the Pet Market frameworks project when Simon proposed it to us at the Fusebox & Frameworks Conference in September. I once tried to do something similar by creating a small blog application using the three popular frameworks that I was aware of at the time (Fusebox 3-4 and Mach-II) and the onTap framework.
Unfortunately I did all the work myself and didn't have a great venue like the ColdFusion Developer's Journal to publish the results. In the end I think my efforts at a useful comparison of frameworks fell short of what I'd hoped. Naturally I'm excited to see Simon take the initiative to enlist the aid of various framework authors and publish the results in the journal. Although I personally lament the fact that the Pet Market application isn't perhaps an ideal medium to demonstrate the unique strengths or advantages of some frameworks (not only the onTap framework, although in my case display tools in particular spring to mind), I do believe the Pet Market application provides probably the best neutral ground for comparison.
My own conversion of the Pet Market application took a bit longer than I expected. At first I thought it was good that the Pet Market application was designed purely for demonstration purposes and wouldn't be used in a production environment.
On further reflection I wonder if perhaps the opposite is true - that the original application is worse for its attempts to educate programmers new to ColdFusion because anyone using it as a learning tool is apt to learn some bad habits. Here are a few examples of what I mean: no primary keys or foreign key constraints in the database containing many related tables (no enforcement of referential integrity); display code executed in a domain-model CFC; and serpentine multi-step form templates containing both form display and post logic in the same file. I'll omit my complaints about the way in which multiple historical postal addresses are stored. Having said all that, here's an application ripe for the kind of structure any good framework will provide.
A Statement of Benefits
Here's a brief list of the benefits I've found using the onTap framework to reinvent the Pet Market application in no particular order.
A Place for Everything and Everything in its Place
Because the onTap framework is designed to take advantage of directory structures, getting started with the migration of Pet Market to this framework is easy. First I created a /petmarket/ directory under my framework application root directory in which the Pet Market sub-application would execute. In this directory I put a copy of all the ColdFusion templates in the original application's /wwwroot/ directory. I could have put these templates in the framework root directory, although I didn't want to create a separate copy of the onTap framework core (3MB) for only the Pet Market application, so I created the subdirectory so it could execute as a sub-application under my existing framework installation. Then I copied the new /petmarket/ directory also into the framework /_components/ directory to mirror the root directory. Files in the root directory will be directly accessible to browsers, while files in the /_components/ directory won't (they're protected by an Application.cfm template). Once these files are copied, I replace the contents of each of the templates in the root /petmarket/ directory with a one-line cfinclude tag to drive the framework content:
<cfinclude template="#request.tapi.process()#">
This sort of minimalist index template should be familiar to anyone using Fusebox, Mach-II, Model-Glue, or the Hub. What's different in this case is that we have many Web-accessible templates instead of having all page requests routed through index.cfm.
To be fair the same can be done also with any of these other frameworks, although the other frameworks are rarely implemented this way because they don't inherently gain any noticeable benefit from using this structure and as such it's generally easier to simply route all requests through the index.cfm template. However, with the onTap framework the name of the base template is used as a part of the execution path to determine which templates are executed by the framework. So using cart.cfm has an inherently different result than using index.cfm as the base template. Of course there's nothing preventing you from routing all requests through index.cfm, although using this multi-template strategy let me migrate the application more quickly thanks to not needing to edit any of the links, form targets, or cflocation tags in the application. Had I used any of these other frameworks, which use form/url parameters and XML configuration files to direct their execution, they would have required that I edit every URL in the application to add the appropriate event parameter (FuseAction, event, etc.).
Although I chose a directory structure that let me ignore URLs in the application, I chose not to take the same approach with images. Although I could have simply left the Pet Market images in the /ontap/petmarket/images/ directory and ignored any image tags in the application HTML, the framework provides some added features with regard to images and for this reason I decided to put the images in the framework's designated image directories.
Yup, multiple image directories.
Although most HTML applications put all images in a single directory, the onTap framework provides a method of easily targeting methods contextually to provide simple branding and replacement of images by context. Interestingly enough, the Pet Market application provides a perfect example of why I included this feature in the framework in spite of the fact that the original Pet Market authors never imagined that I would write this framework revision: more evidence that this is a good feature to have.
When you view the Pet Market application in a browser, you'll notice that the layout presents several images of different animals at the top, linked to category pages for the various categories of pets, such as dogs, cats, and reptiles. As you browse through the application, however, you might notice that a different set of images is presented on the home page than on any other page in the application. In the original application, the location of this image was tightly coupled with the layout custom tag: the layout tag contained logic to determine if the current page was the home page and displayed a different set of images (including the size of the image as part of the name of the graphic file). I put copies of each of these smaller five images in the directory /ontap/_components/petmarket/_images/category/ for most pages in the application to use, renaming them to remove the image size from its file name. This makes the image name succinct and canonical and the name of the image file accurate, even if the size of the image changes later. Then I copied the five larger images to the directory /ontap/_components/petmarket/index/_images/category/ for the home page to use with the same succinct file names.
Throughout the HTML for the application I then replaced all image tag SRC attributes with the function request.tapi.getIMG() containing the name of the target image. This function checks a series of nested image directories and returns the URL to the first image it finds. So when the layout requests request.tapi.getIMG("category/dogs.gif") it will use the image located in /petmarket/index/_images/category/ on the home page and on every other page it will use the image located in /petmarket/_images/category/.
You might notice the string "/index/" in the path of the images used in the home page, and you'd be correct if you guessed this string matched the name of the base template (index.cfm). In this way, I can loosely couple the branding of images for different pages without needing to add any logic to the display or layout templates to handle the variance. This way I eliminated all image logic from the layout template.
Then there's the issue of layout in general. As a developer I tend to think about every application in terms of portability. Call me stubborn; I refuse to make assumptions about an application's operating environment. This is one of the many weaknesses of the original Pet Market application, which required that you either add a custom tag path in the ColdFusion administrator or put tags in the default custom tags directory, neither of which are solutions I'm willing to accept for an application I've written.
In the case of layout, the application uses a custom tag to create the bulk of the HTML on each page. The custom tag is used with an end-tag to wrap the content of each page. For my application, this custom tag wouldn't be necessary, although much of the same structure can be reused. I copied the /extensions/customtags/wrapper.cfm template from the original application to /_components/petmarket/_layout.cfm and changed the logic for the tag internally to use the variable variables.tap.layout (which has a value of "header" or "footer") instead of using the ColdFusion native variable thistag.executionmode. Once this was done I removed the custom tag references from all the other templates in the /_components/petmarket/ directory. I also removed the head, body. and HTML tags from the layout template, since these are generated by the framework, and moved the content of the embedded CSS style tags to the template /_components/petmarket/_htmlhead/100_petmarket.css (which is included by the framework automatically in the resultant HTML).
In all honesty this isn't the way I'd normally handle layout. There's nothing wrong with it per se, although the onTap framework includes an HTML library - a set of functions for generating modular displays in HTML format with much greater flexibility - and normally I'd use this library via an XHTML custom-tag to create and cache the layout and then insert the content for each page into the generated display structures. Although this technique is more extensible, I simply didn't have time to implement it for this article (and Simon requested that I use the original application's view layer).
The original Pet Market application also puts its ColdFusion Components (CFC) in a directory not below the web root, requiring the creation of a ColdFusion mapping to target these CFCs. The onTap framework has a central repository for CFCs and a custom function for targeting these components that allows the application to be moved to any environment and configured quickly and easily (no need to change the extends attributes of the various CFCs).
For this I copied the original application's /extensions/components/petmarket/ directory to /ontap/_components/_cfc/petmarket/ then located all references to the ColdFusion native CreateObject() function and replaced them with request.tapi.getObject() (the arguments did not change). I also would have changed any references to the cfobject tag or cfinvoke tags that might use cfc paths instead of instantiated objects (and replaced their paths with request.tapi.getCFC()), although these aren't used in the original Pet Market application, making such changes unnecessary.
Finally, the Pet Market application included an Application.cfm template in its root directory (above the web root) to execute code at the beginning of each request. Although the onTap framework has its own Application.cfc for handling the various application events (onApplicationStart, onApplicationEnd, onSessionStart, onSessionEnd, etc.), the structure of this template lets us easily migrate code from other applications used in their Application.cfc or Application.cfm templates by copying the code into an appropriate subdirectory. In the case of my Pet Market migration, I copied the contents of the original Pet Market Application.cfm template to the file /ontap/_components/petmarket/_application/100_petmarket.cfm. This template will execute at the beginning of each request when the base template is in the /ontap/petmarket/ directory.
By the time this template executes, however, the framework has already instantiated the application and so the cfapplication tag is no longer necessary and I deleted it. If I wanted to change the name of the application or its timeouts or session management settings, I'd make those changes by setting variables in the structure request.tap.cf.app (i.e., request.tap.cf.app.sessionmanagement = true) using code in the template /ontap/_components/_appsettings.cfm. (This template doesn't exist in the core components archive to safeguard against overwriting your project settings when upgrading to the latest framework core version.)
At this point, I could have left the files as they were and the application should work. I prefer, however, to have the /_components/ templates a little further segregated, so I renamed each of the templates in the /_components/petmarket/ directory to _process.cfm and put them in another subdirectory with the name of the original template, i.e., /cart.cfm is renamed to /cart/_process.cfm.
Now I have my desired file structure:
Object-Relational Mapping (ORM) and Data Access Objects (DAO)
ontap/
_components/
_cfc/
petmarket/
petmarket/
_application/
_htmlhead/
_images/
_layout.cfm
about/_process.cfm
cart/_process.cfm
index/
_images/
_process.cfm
etc/_process.cfm
petmarket/
about.cfm
cart.cfm
index.cfm
etc.cfm
I registered this structure in the request scope using the /ontap/_components/_appsettings.cfm template for general application configuration and then set about the task of replacing all the application's ad-hoc SQL queries with the onTap framework SQL-abstraction tools. By default these tools use the "primary" DSN (request.tap.dsn.primary) so if I want my SQL-abstraction code to access my new Pet Market DSN I have to include the DSN attribute or argument in each of the custom tags or function calls used to access this abstraction layer.
One way to reduce the amount of code needed to accomplish this task is to use a datasource.cfc provided by the framework and instantiate it with the name of the DSN I want it to use. Because I don't want to recreate this CFC on each request where I might need it, I create it in the application scope (application.petmerket.datasource) when the application starts by putting the object instantiation in the template /ontap/_components/_appstart/100_petmarket.cfm.
Once I started looking more deeply into the data access used in the Pet Market application, I realized that most of it could be replaced with standardized Object Relational Mapping (ORM) techniques. For this I used the onTap framework's dynamic inheritance tool in the soft constructor for most of the existing CFCs to extend the framework's Data Access Object (DAO) component (request.tapi.cfcExtend(this,"dao")) and eliminated a large amount of code from the CFCs that simply put data returned from ad hoc SQL queries into the "this" scope or a structure in the "this" scope.
Thanks to all of the code I could eliminate from these components, I reduced the CF/HTML/CSS code in the entire application from 85KB to 65KB. Although 20KB may not sound like a lot of code, it's roughly 25% of the entire application even after having added some of my own code (including a product-item DAO that wasn't included in the original application). That's a lot of keystrokes and time saved over a larger project.
The original Pet Market application included five CFCs: a _base.cfc that I eliminated (ontap.cfc provides an improved implementation of its only functional method), a shoppingcart.cfc for maintaining the current user's selected items and quantities and three components that are extended DAOs (category.cfc, product.cfc, and user.cfc). The original application stored product data in two tables (product and item), which prompted me to add the item.cfc for modeling data in the second table. The item.cfc also provided me with a good opportunity to highlight one of the advantages of the framework's dynamic accessors (getValue(propertyname)). In this case I could simplify the relationship between the product object and the product-item object by setting a property of "productname" in the item CFC's soft-constructor that ensures that the property is included in the structure returned from the getProperties() method and implemented the private get_productname method that instantiates the related product object and returns the value of its "name" property. So the item.cfc has a "productname" property that's drawn directly from the relevant related product object with no need for the accessing template to concern itself with the inner workings of these components or shape of the database schema.
Although the dynamic accessor feature let me eliminate many lines of code from the original CFCs, this altered structure forced me to check all of the display code to ensure that it accessed these objects using the appropriate methods (instead of arbitrarily exposed variables). For many of the display templates I opted not to replace the exposed variable references with dynamic accessor functions, chosing instead to append the desired variables to the attributes scope using the getProperties() method of the ontap.cfc and the ColdFusion native StructAppend() function. This strategy for decoupling variable use from both components and from the form and url scopes should be familiar to anyone who's used Fusebox in the past (and indeed I attempted to replace all form and url-scope variables in the application).
Show Me the Memory!
The original Pet Market application used many CFCs for different purposes, but it gained very little (if any) benefit from using CFCs. This is because the application created these objects for each request and the components didn't encapsulate their data. So no advantage could be gained from caching the data for categories or products even though this data is rarely (or never) changed; in fact, the application didn't even include any tools for updating product data. Since the onTap framework DAO objects are designed to make good use of caching techniques, I also implemented a factory object by extending the framework's factory.cfc. I instantiated the new petfactory.cfc and stored it in the variable application.petmarket.petfactory in the previously created template /ontap/_components/_appstart/100_petmarket.cfm that is executed when the application starts.
With this object in place I replaced all other instances of request.tapi.getObject() (previously CreateObject()) with calls to the methods of the application.petmarket.petfactory object (with the exception of user and shopping-cart objects instantiated at the beginning of a session). The petfactory object is now the only object that instantiates DAO components. As a result of this change, fewer queries are executed by the application, being limited only to search queries and queries executed when first instantiating a new DAO object. This caching makes the application significantly more scalable in handling large numbers of users and pet purchases. Since I created a factory object for the Pet Market application, I also took this opportunity to move the category-list and product-search queries to this factory object out of the category.cfc and product.cfc objects where they didn't belong in the first place.
A La Cart
I personally found Pet Market's implementation of a shopping-cart unacceptable for a number of reasons, both related to the model (data access and session management) and to the view (the checkout form is a horrid mess). I used several strategies to resolve what I saw as problems with the shopping cart.
First, I consolidated all of the code for the various checkout steps into the stepX.cfm templates in the /_components/petmarket/checkout/ directory so that each of these files contains all of the code for the relevant step of the checkout. The original version included only the form code in these files, while leaving code to display during subsequent steps casually misplaced in the checkout.cfm template. Then I removed the form-post code from the top of the checkout.cfm template and put it in several subdirectories of the /_components/petmarket/checkout/ directory. To target the code in these new directories I added a hidden form variable with the name "netaction" to each of the stepX.cfm form templates, indicating the relevant subdirectory. Now when each form is submitted only the relevant code for the submitted form is executed. Then I cleaned up /_components/petmarket/checkout/_process.cfm by putting the names of the steps in an array and used a loop and variables to generate the display of the list of steps and links to previous steps in the checkout process.
I must admit that the Pet Market shopping cart is an improvement over most shopping cart code I've seen because it smartly uses a query to hold the user's selected items (instead of the inappropriate multi-dimensional array used by most cart applications, which forces the programmer to use numeric indices where named keys would be more appropriate). Still, I think the shopping cart can definitely be improved.
Firstly, the display template /_shoppingcart.cfm fetches a query containing the names of all products in the database, which is then joined with the query containing the user's selected items. Although the first query is cached daily using the ColdFusion server's native query caching features, I find this technique to be unencapsulated and sloppy (the display code contains references to database schema) and I personally dislike the server's native query caching features. To alleviate these problems I implemented another of the framework's core CFCs - LinkedList.
To be honest I created the LinkedList.cfc as part of the framework core components specifically for handling memory-resident lists like shopping-carts. Although extensive, the transition from a ColdFusion query to a series of linked-list objects was reasonably simple thanks in part to the fact that several more complicated query tasks are encapsulated by the LinkedList.cfc object. When the cart is updated with quantities for one or more objects, checking to see if an item already exists in the cart is a simple function call (item.search("itemoid",arguments.itemoid)) instead of a query of queries. When an item is removed from the cart, a similar method (item.remove()) replaces another query of queries. Displaying the contents of the cart is a simple process of looping over the pre-populated items and displaying their names, quantities, and costs (while item.hasNext()), cleanly encapsulating the data access away from the display logic. And since the objects are pre-populated from the petfactory product objects prior to being cached in the session scope, this has the added benefit of ensuring that the names of products in the user's cart won't change before checkout is complete. Although the user.cfc still has a placeOrder method, the bulk of its work is now done by the appropriate shoppingcart.cfc (user.cfc is not an appropriate place for the bulk of this code).
The coup de grâce is a bit of incidental internationalization. That is to say, i18n implemented with no expenditure of effort. In the original Pet Market application, items in the cart display in the order in which they were added to the cart. I didn't like this, preferring that items in the cart be sorted alphabetically, so I implemented the LinkedList sort method (item.sort("productname")) each time an item is added to the cart (removing an item doesn't change the sort order and as such sorting is unnecessary). It merely happens that the sort method provided by the LinkedList.cfc uses Java international collations to provide non-lexigraphical sorting. That is to say, more than a simply case-insensitive sort, but rather the names of products are sorted using the dictionary and accenting rules of the user's preferred language.