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
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>
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.
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.

#1 by Matthias on 12/15/06 - 8:47 AM
I also like your idea of having two methods to retrieve things in the cart. But it seems a little too abstract for me. What could be a useful real-world purpose of getTotalProducts?
#2 by Dan on 12/15/06 - 9:32 AM
#3 by Peter Bell on 12/15/06 - 10:05 AM
Great article (and thanks for the plug!). I absolutely get the total products versus items. Only thing you might consider is composing a shopping cart of items so instead of Cart.List() you might Cart.get("ItemIBO") to get an IBO or your collection mechanism of choice that you can then loop through. Just a thought for down the line as it allows you to have getters and setters for the items which can be required to encapsulate complexity in more sophisticated carts. I'm also not 100% convinced about the need for getCartPosition(), but let me work up my new commerce code and post it and we can compare notes!
Some of the other things I've had to write in the past and you may find yourself adding down the line:
- Attribute support. Product can come in different colors or sizes, but still same product ID. Need to describe a cart item by both its ID AND the concatenation of its attribute values as if you add a blue sweater to the cart it shouldn't just increment the number of gray sweaters in the cart.
- Multi-add. The ability to add n-products to the cart in one go.
- Attribute based pricing. Different attributes often affect the pricing of an item.
- Packages. Add a package and it expands into n-items in your cart.
Then there is discounting, gift certificates and "enter shipping method and zip to get ship pricing" which allows people to see estimated total cost before checking out.
Again, great posting, love it!
#4 by Dan on 12/15/06 - 10:20 AM
Thanks Again!
#5 by Dan Wilson on 12/15/06 - 1:28 PM
Nice job on your code.... Looking over a few things I noticed skew. I believe you might be referring to the SKU ( Stock Keeping Unit ).
If so, changing it now might be a bit easier than later on once the code is on thousands of shopping carts...
http://en.wikipedia.org/wiki/Stock_Keeping_Unit
dw
#6 by Sophek Tounn on 12/15/06 - 1:32 PM
Why should ruby on rails have all the cools products?
Thanks
Sophek
#7 by Dan on 12/15/06 - 1:45 PM
@Sophek - As soon as I can get something together I'll post it on riaforge and google. I would love any help or contributions anyone is willing to give!
#8 by Geoff on 12/15/06 - 2:51 PM
I'd add a stock column to your qProducts query - so you know if there's a stock problem when someone adds something to their cart - you can throw a "Sorry, we only have X in stock!" message.
Also, I'm looking forward to see how you cope with locking - especially when you pass your session.cart to your checkout.cfc. Checkout.cfc will have some sort of webservice call, which potentially could take a while to complete, meanwhile the user has given up, clicked elsewhere and changed their cart contents...
#9 by Dan on 12/15/06 - 3:10 PM
#10 by Dan on 12/15/06 - 6:45 PM
#11 by Geoff on 12/15/06 - 7:51 PM
Your session.cart shouldn't know or care how to directly query the catalogue database ... I reckon your "qProducts" query should really be a method of catalog.cfc
Cheers, Geoff
#12 by Kola on 1/30/07 - 12:12 PM
Hi, just one small thing to add but I think it will help. It may be an idea if you started with a number of textual use cases - i.e. document the most common scenarios. I think this will be a huge benefit when designing the system and will help drive the design. When you have questions and it looks like you have a number of competing design decisions instead of saying "we may need an emptyCart method" for example you can look at the use cases and see where (if) it is required. It may be better to look at all the use cases and use those to help determine whether behaviour makes sense in the context of a particular use case. Of course if you're doing this already ignore me :)
#13 by Nick Tong on 3/9/07 - 1:14 PM
#14 by Nate on 5/15/07 - 6:00 PM