With any system there is a core that needs to be rock solid. When it comes to shopping carts I think this is especially true. So when designing a shopping cart I think it is important to start from the core, build it right, and then build out from there. With that being said I am working on an open source cart for the community (more on this later) and I would like to share some ideas on the core component of the application, the cart component.

I am a reader of Peter Bell' Blog and I think he has some really good articles on a wide range of topics, but for the purposes of this article he has some really good thoughts on Ecommerce. Peter explains that there are really 3 core features that he usually breaks his e-commerce applications into - a catalog, a cart and a checkout system. I happen to agree with him on this and would like to begin with what I think is the backbone, the cart. First we will go through and define our core functions which I believe at a minimum every system should have.

ShoppingCart.cfc Methods

  • add() - This method will allow us to add an item to the shopping cart. The first thing I always do is check to see if it exists in the cart, if it does I pass my arguments on to the update method.
  • update() - This method to me was a little trickier than I first thought and you will see what I mean when you dive into the code. You need to have an argument that tells us if we are updating this quantity, taking the current qty and adding to it or setting the qty to a specific number. The reason you come across this is because you have two ways to update a product, unknowingly and manually. When you add a product to the cart and circle back around and add that same item to the cart you are unknowingly updating that items qty, and in the end we now want 2 items in the cart. When you have 1 item in your cart and from the cart view change the qty from 1 to 5 you do not want to add 5 to the toal you want to manually change the qty to a specific number.
  • remove() - removes a specific product from the cart.
  • empty() - removes all products from your cart basically you are just clearing the array here.
  • list() - this helper function takes your array of structures and creates a nice query of the cart contents for you to use. This is especially important in the Shopping Cart view screen
  • getTotalProducts() - I like having 2 separate methods here, this one that gets us the number of products in the cart.
  • getTotalItems() - and 1 that gets us the total number of items. We could have 1 product in the cart with a qty of 3 and we would have 3 items.
  • getSubTotal() - This will just loop through our array and at the position in the product structure will add all of totals together.
  • getCartPosition() - This is a private method only to be used internally. This method will tell where in the cart a certain product is, this is an essential helper function when updating an item.
  • exists() - Another private method will basically tell us if the product in question already exists in the car or not.
  • getProductDetail() - This will get us details for a specific product. For the real world this should be a product bean that is passed in via Coldspring but for the purpose of this article I have it grabbing the information needed.

So now you have an idea of what I think is a standard set of methods for our cart. I would love to hear your ideas on additions or subtractions so please chime in. Ok, I guess everyone wants to see some code. Here is the ShoppingCart.cfc component. To use it you need a database table called product with a few basic columns - name,skew,description,price,saleprice. Other than that it should be really easy to setup.

ShoppingCart.cfc

view plain print about
1<cfcomponent name="ShoppingCart">
2
3    <cfset variables.dsn = "">
4    <cfset variables.myCart = "">
5    <cfset variables.subtotal = "">
6    <cfset variables.totalProducts = "">
7    <cfset variables.totalItems = "">
8
9    <cffunction name="init" access="public" returntype="ShoppingCart"
10        hint="I will set the applications datasource and initialize all relivant cart data.">

11        <cfargument name="dsn" type="string" required="true">
12        
13        <cfset variables.dsn = arguments.dsn>
14        <cfset variables.myCart = arrayNew(1)>
15        <cfset variables.subtotal = 0>
16        <cfset variables.totalProducts = 0>
17        <cfset variables.totalItems = 0>
18        
19        <cfreturn this>
20    </cffunction>
21
22    <cffunction name="add" access="public" returntype="void" hint="I will add a product to our shopping cart.">
23        <cfargument name="productId" type="numeric" required="true">
24        <cfargument name="qty" type="numeric" required="false" default="1">
25        <cfset var cartItem = structNew()>
26        <cfset var product = "">
27        
28        <!--- see if the item exists in our cart --->
29        <cfif exists(arguments.productId)>
30            <!--- we just need to run an update --->
31            <cfset update(arguments.productId,arguments.qty)>    
32        <cfelse>
33            <!--- populate a structure that will hold this items info --->
34            <cfset product = getProductDetail(arguments.productId)>
35            <cfset cartItem.productId = arguments.productId>
36            <cfset cartItem.qty = arguments.qty>
37            <cfset cartItem.name = product.name>
38            <cfset cartItem.skew = product.skew>
39            <cfset cartItem.price = product.price>
40            <cfset cartItem.salePrice = product.saleprice>
41            <!--- append to the array a new item containing our structure --->
42            <cfset arrayAppend(variables.myCart,cartItem)>
43        </cfif>
44        <cfreturn>
45    </cffunction>
46
47    <cffunction name="update" access="public" returntype="void"
48        hint="I will a products qty in the cart. This method should only be called from the add method.">

49        <cfargument name="productId" type="numeric" required="true">
50        <cfargument name="qty" type="numeric" required="true">
51        <cfargument name="addCurrentQty" type="boolean" default="true">
52        
53        <cfset var position = 0>
54        <cfset var currentQty = 0>
55        
56        <!--- this item should already exist in our cart but to be safe we will check --->
57        <cfif exists(arguments.productId)>
58            <cfif arguments.qty LT 0>
59                <!--- you can not add a negative number of products --->
60                <cfthrow message="The QTY argument passed to method update must be a positive number.">
61            <cfelseif arguments.qty EQ 0>
62                <!--- basically your saying remove the product from the cart --->
63                <cfset remove(arguments.productId)>
64            <cfelse>
65                <!--- get the position of the cart this item is in --->
66                <cfset position = getCartPosition(arguments.productId)>
67                <!---
68                    the reason you do this is for when people are changing the qty in the cart view
69                    they are changing it to a specific qty not adding to it
70                --->

71                <cfif arguments.addCurrentQty>
72                    <!--- get the current qty --->
73                    <cfset currentQty = myCart[position].qty>                    
74                </cfif>
75                <cfset myCart[position].qty = arguments.qty + currentQty>
76            </cfif>    
77        <cfelse>
78            <!--- this item doesnt exist? lets call the add method then --->
79            <cfset add(arguments.productId,arguments.qty)>
80        </cfif>
81    </cffunction>
82
83    <cffunction name="remove" access="public" returntype="void" hint="I will remove an item from cart.">
84        <cfargument name="productId" type="numeric" required="true">
85        <!--- make sure it exists in the cart before you try to remove it --->
86        <cfset var doesExist = exists(arguments.productId)>
87        <cfset var position = 0>
88        
89        <!--- it exists, get the position in the cart of its location --->
90        <cfif doesExist(arguments.productId)>
91            <cfset position = getCartPosition(arguments.productId)>
92            <!--- delete that position in the array --->
93            <cfset arrayDeleteAt(variables.myCart,position)>
94        </cfif>
95    </cffunction>
96    
97    <cffunction name="empty" access="public" returntype="void" hint="I will clear the array">
98        <cfset arrayClear(variables.mycart)>
99    </cffunction>
100    
101    <cffunction name="getProductDetail" access="public" returntype="struct">
102        <cfargument name="productId" type="numeric" required="true">
103        <cfset var qProducts = "">
104        
105        <cfquery name="qProducts" datasource="#variables.dsn#">
106        SELECT name, skew, description, price, salePrice
107        from Product
108        where productId = <cfqueryparam value="#arguments.productId#" cfsqltype="cf_sql_integer" />
109        </cfquery>
110        
111        <cfif qProducts.recordCount>
112            <cfreturn queryRowToStruct(qProducts)>
113        </cfif>
114    </cffunction>
115
116    <cffunction name="queryRowToStruct" access="private" output="false" returntype="struct">
117        <cfargument name="qry" type="query" required="true">
118        
119        <cfscript>
120            /**
121             * Makes a row of a query into a structure.
122             *
123             * @param query      The query to work with.
124             * @param row      Row number to check. Defaults to row 1.
125             * @return Returns a structure.
126             * @author Nathan Dintenfass (nathan@changemedia.com)
127             * @version 1, December 11, 2001
128             */

129            //by default, do this to the first row of the query
130
            var row = 1;
131            //a var for looping
132
            var ii = 1;
133            //the cols to loop over
134
            var cols = listToArray(qry.columnList);
135            //the struct to return
136
            var stReturn = structnew();
137            //if there is a second argument, use that for the row number
138
            if(arrayLen(arguments) GT 1)
139                row = arguments[2];
140            //loop over the cols and build the struct from the query row
141
            for(ii = 1; ii lte arraylen(cols); ii = ii + 1){
142                stReturn[cols[ii]] = qry[cols[ii]][row];
143            }        
144            //return the struct
145
            return stReturn;
146        
</cfscript>
147    </cffunction>
148    
149    <cffunction name="list" access="public" returntype="query"
150        hint="I am a helper function to create a nice query for you to use.">

151        <cfscript>
152        /**
153         * Converts an array of structures to a CF Query Object.
154         * 6-19-02: Minor revision by Rob Brooks-Bilson (rbils@amkor.com)
155         *
156         * Update to handle empty array passed in. Mod by Nathan Dintenfass. Also no longer using list func.
157         *
158         * @param Array      The array of structures to be converted to a query object. Assumes each array element contains structure with same (Required)
159         * @return Returns a query object.
160         * @author David Crawford (rbils@amkor.comdcrawford@acteksoft.com)
161         * @version 2, March 19, 2003
162         */

163            var colNames = "";
164            var theQuery = queryNew("");
165            var i=0;
166            var j=0;
167            //if there's nothing in the array, return the empty query
168
            if(NOT arrayLen(myCart))
169                return theQuery;
170            //get the column names into an array =
171
            colNames = structKeyArray(myCart[1]);
172            //build the query based on the colNames
173
            theQuery = queryNew(arrayToList(colNames));
174            //add the right number of rows to the query
175
            queryAddRow(theQuery, arrayLen(myCart));
176            //for each element in the array, loop through the columns, populating the query
177
            for(i=1; i LTE arrayLen(myCart); i=i+1){
178                for(j=1; j LTE arrayLen(colNames); j=j+1){
179                    querySetCell(theQuery, colNames[j], myCart[i][colNames[j]], i);
180                }
181            }
182            return theQuery;
183        
</cfscript>
184    </cffunction>
185    
186    <cffunction name="getTotalProducts" access="public" returntype="numeric"
187        hint="I return the total number of products in the cart">

188        <cfset var products = arrayLen(variables.myCart)>
189        <cfset variables.totalProducts = products>
190        <cfreturn variables.totalProducts>
191    </cffunction>
192        
193    <cffunction name="getTotalItems" access="public" returntype="numeric"
194        hint="I will return the number of total items in the cart products * qty.">

195        <cfset var items = 0>
196        <cfloop from="1" to="#arrayLen(variables.myCart)#" index="i">
197            <cfif isNumeric(variables.myCart[i].qty)>
198                <cfset items = items + variables.myCart[i].qty>
199            </cfif>
200        </cfloop>
201        <cfset variables.totalItems = items>
202        
203        <cfreturn variables.totalItems>
204    </cffunction>
205    
206    <cffunction name="getSubTotal" access="public" returntype="numeric"
207        hint="I will return the sub total of all the items in the cart.">

208        <cfset var cost = 0>
209        
210        <cfloop from="1" to="#arrayLen(variables.myCart)#" index="i">
211            <cfset cost = cost + (variables.myCart[i].price * variables.myCart[i].qty)>
212        </cfloop>
213        <cfset variables.subtotal = cost>
214        
215        <cfreturn variables.subtotal>
216    </cffunction>
217    
218    <cffunction name="getCartPosition" access="private" returntype="numeric">
219        <cfargument name="productId" type="numeric" required="true">
220        <cfset var currentPos = 0>
221        <!--- as long as there at least 1 item in the array --->
222        <cfif arrayLen(variables.myCart) GT 0>
223            <!--- loop the array and look for the productId --->
224            <cfloop from="1" to="#arrayLen(variables.myCart)#" index="x">
225                <cfif variables.myCart[x].productId EQ arguments.productId>
226                    <!--- we found what we came for --->
227                    <cfset currentPos = x>
228                    <!--- no need to stick around, get out of the loop --->
229                    <cfbreak>
230                </cfif>        
231            </cfloop>
232        </cfif>
233        <cfreturn currentPos>
234    </cffunction>
235    
236    <cffunction name="exists" access="private" returntype="boolean"
237        hint="pass a productId and I will tell you if this item is in the cart">

238        <cfargument name="productId" type="numeric" required="true">
239        <cfset var exists = false>
240        <!--- as long as there at least 1 item in the array --->
241        <cfif arrayLen(variables.myCart) GT 0>
242            <!--- loop the array and look for the productId --->
243            <cfloop from="1" to="#arrayLen(variables.myCart)#" index="x">
244                <cfif variables.myCart[x].productId EQ arguments.productId>
245                    <!--- we found what we came for --->
246                    <cfset exists = true>
247                    <!--- no need to stick around, get out of the loop --->
248                    <cfbreak>
249                </cfif>        
250            </cfloop>
251        </cfif>
252        <cfreturn exists>
253    </cffunction>
254    
255</cfcomponent>
Here is an example of my Application component and how i set up the cart.
view plain print about
1<cffunction name="onRequestStart" returnType="boolean">
2        <cfargument type="String" name="targetPage" required="true" />
3        
4        <cfif NOT structKeyExists(application,"beanFactory") OR structKeyExists(url,"reload")>
5            <cfset csprops = structNew()>
6            <cfset csprops.dsn = "cfinferno">
7            <cfset application.beanFactory = createObject("component","coldspring.beans.DefaultXmlBeanFactory").init(structNew(),csprops)/>    
8            <cfset application.beanFactory.loadBeansFromXmlFile("#expandPath('.')#\config\beans.xml",true)/>    
9        </cfif>
10    
11        <cfif NOT structKeyExists(session,"cart") OR structKeyExists(url,"reload")>
12            <cfset session.cart = application.beanFactory.getBean("ShoppingCart")>
13        </cfif>
14    
15        <cfreturn true>
16    </cffunction>

And Finally a quick way to use the cart. Here is my add,update and list method in use and the final result.
view plain print about
1<cfset session.cart.add(1)>
2<cfset session.cart.update(1,3)>
3<cfdump var="#session.cart.list()#">


You can download the Shopping Cart component using the download link below. As always I would love to hear anyones thoughts on this.