The Shopping Cart Core
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
<cfset variables.dsn = "">
<cfset variables.myCart = "">
<cfset variables.subtotal = "">
<cfset variables.totalProducts = "">
<cfset variables.totalItems = "">
<cffunction name="init" access="public" returntype="ShoppingCart"
hint="I will set the applications datasource and initialize all relivant cart data.">
<cfargument name="dsn" type="string" required="true">
<cfset variables.dsn = arguments.dsn>
<cfset variables.myCart = arrayNew(1)>
<cfset variables.subtotal = 0>
<cfset variables.totalProducts = 0>
<cfset variables.totalItems = 0>
<cfreturn this>
</cffunction>
<cffunction name="add" access="public" returntype="void" hint="I will add a product to our shopping cart.">
<cfargument name="productId" type="numeric" required="true">
<cfargument name="qty" type="numeric" required="false" default="1">
<cfset var cartItem = structNew()>
<cfset var product = "">
<!--- see if the item exists in our cart --->
<cfif exists(arguments.productId)>
<!--- we just need to run an update --->
<cfset update(arguments.productId,arguments.qty)>
<cfelse>
<!--- populate a structure that will hold this items info --->
<cfset product = getProductDetail(arguments.productId)>
<cfset cartItem.productId = arguments.productId>
<cfset cartItem.qty = arguments.qty>
<cfset cartItem.name = product.name>
<cfset cartItem.skew = product.skew>
<cfset cartItem.price = product.price>
<cfset cartItem.salePrice = product.saleprice>
<!--- append to the array a new item containing our structure --->
<cfset arrayAppend(variables.myCart,cartItem)>
</cfif>
<cfreturn>
</cffunction>
<cffunction name="update" access="public" returntype="void"
hint="I will a products qty in the cart. This method should only be called from the add method.">
<cfargument name="productId" type="numeric" required="true">
<cfargument name="qty" type="numeric" required="true">
<cfargument name="addCurrentQty" type="boolean" default="true">
<cfset var position = 0>
<cfset var currentQty = 0>
<!--- this item should already exist in our cart but to be safe we will check --->
<cfif exists(arguments.productId)>
<cfif arguments.qty LT 0>
<!--- you can not add a negative number of products --->
<cfthrow message="The QTY argument passed to method update must be a positive number.">
<cfelseif arguments.qty EQ 0>
<!--- basically your saying remove the product from the cart --->
<cfset remove(arguments.productId)>
<cfelse>
<!--- get the position of the cart this item is in --->
<cfset position = getCartPosition(arguments.productId)>
<!---
the reason you do this is for when people are changing the qty in the cart view
they are changing it to a specific qty not adding to it
--->
<cfif arguments.addCurrentQty>
<!--- get the current qty --->
<cfset currentQty = myCart[position].qty>
</cfif>
<cfset myCart[position].qty = arguments.qty + currentQty>
</cfif>
<cfelse>
<!--- this item doesnt exist? lets call the add method then --->
<cfset add(arguments.productId,arguments.qty)>
</cfif>
</cffunction>
<cffunction name="remove" access="public" returntype="void" hint="I will remove an item from cart.">
<cfargument name="productId" type="numeric" required="true">
<!--- make sure it exists in the cart before you try to remove it --->
<cfset var doesExist = exists(arguments.productId)>
<cfset var position = 0>
<!--- it exists, get the position in the cart of its location --->
<cfif doesExist(arguments.productId)>
<cfset position = getCartPosition(arguments.productId)>
<!--- delete that position in the array --->
<cfset arrayDeleteAt(variables.myCart,position)>
</cfif>
</cffunction>
<cffunction name="empty" access="public" returntype="void" hint="I will clear the array">
<cfset arrayClear(variables.mycart)>
</cffunction>
<cffunction name="getProductDetail" access="public" returntype="struct">
<cfargument name="productId" type="numeric" required="true">
<cfset var qProducts = "">
<cfquery name="qProducts" datasource="#variables.dsn#">
SELECT name, skew, description, price, salePrice
from Product
where productId = <cfqueryparam value="#arguments.productId#" cfsqltype="cf_sql_integer" />
</cfquery>
<cfif qProducts.recordCount>
<cfreturn queryRowToStruct(qProducts)>
</cfif>
</cffunction>
<cffunction name="queryRowToStruct" access="private" output="false" returntype="struct">
<cfargument name="qry" type="query" required="true">
<cfscript>
/**
* Makes a row of a query into a structure.
*
* @param query The query to work with.
* @param row Row number to check. Defaults to row 1.
* @return Returns a structure.
* @author Nathan Dintenfass (nathan@changemedia.com)
* @version 1, December 11, 2001
*/
//by default, do this to the first row of the query var row = 1;
//a var for looping var ii = 1;
//the cols to loop over var cols = listToArray(qry.columnList);
//the struct to return var stReturn = structnew();
//if there is a second argument, use that for the row number if(arrayLen(arguments) GT 1)
row = arguments[2];
//loop over the cols and build the struct from the query row for(ii = 1; ii lte arraylen(cols); ii = ii + 1){
stReturn[cols[ii]] = qry[cols[ii]][row];
}
//return the struct return stReturn;
</cfscript>
</cffunction>
<cffunction name="list" access="public" returntype="query"
hint="I am a helper function to create a nice query for you to use.">
<cfscript>
/**
* Converts an array of structures to a CF Query Object.
* 6-19-02: Minor revision by Rob Brooks-Bilson (rbils@amkor.com)
*
* Update to handle empty array passed in. Mod by Nathan Dintenfass. Also no longer using list func.
*
* @param Array The array of structures to be converted to a query object. Assumes each array element contains structure with same (Required)
* @return Returns a query object.
* @author David Crawford (rbils@amkor.comdcrawford@acteksoft.com)
* @version 2, March 19, 2003
*/
var colNames = "";
var theQuery = queryNew("");
var i=0;
var j=0;
//if there's nothing in the array, return the empty query if(NOT arrayLen(myCart))
return theQuery;
//get the column names into an array = colNames = structKeyArray(myCart[1]);
//build the query based on the colNames theQuery = queryNew(arrayToList(colNames));
//add the right number of rows to the query queryAddRow(theQuery, arrayLen(myCart));
//for each element in the array, loop through the columns, populating the query for(i=1; i LTE arrayLen(myCart); i=i+1){
for(j=1; j LTE arrayLen(colNames); j=j+1){
querySetCell(theQuery, colNames[j], myCart[i][colNames[j]], i);
}
}
return theQuery;
</cfscript>
</cffunction>
<cffunction name="getTotalProducts" access="public" returntype="numeric"
hint="I return the total number of products in the cart">
<cfset var products = arrayLen(variables.myCart)>
<cfset variables.totalProducts = products>
<cfreturn variables.totalProducts>
</cffunction>
<cffunction name="getTotalItems" access="public" returntype="numeric"
hint="I will return the number of total items in the cart products * qty.">
<cfset var items = 0>
<cfloop from="1" to="#arrayLen(variables.myCart)#" index="i">
<cfif isNumeric(variables.myCart[i].qty)>
<cfset items = items + variables.myCart[i].qty>
</cfif>
</cfloop>
<cfset variables.totalItems = items>
<cfreturn variables.totalItems>
</cffunction>
<cffunction name="getSubTotal" access="public" returntype="numeric"
hint="I will return the sub total of all the items in the cart.">
<cfset var cost = 0>
<cfloop from="1" to="#arrayLen(variables.myCart)#" index="i">
<cfset cost = cost + (variables.myCart[i].price * variables.myCart[i].qty)>
</cfloop>
<cfset variables.subtotal = cost>
<cfreturn variables.subtotal>
</cffunction>
<cffunction name="getCartPosition" access="private" returntype="numeric">
<cfargument name="productId" type="numeric" required="true">
<cfset var currentPos = 0>
<!--- as long as there at least 1 item in the array --->
<cfif arrayLen(variables.myCart) GT 0>
<!--- loop the array and look for the productId --->
<cfloop from="1" to="#arrayLen(variables.myCart)#" index="x">
<cfif variables.myCart[x].productId EQ arguments.productId>
<!--- we found what we came for --->
<cfset currentPos = x>
<!--- no need to stick around, get out of the loop --->
<cfbreak>
</cfif>
</cfloop>
</cfif>
<cfreturn currentPos>
</cffunction>
<cffunction name="exists" access="private" returntype="boolean"
hint="pass a productId and I will tell you if this item is in the cart">
<cfargument name="productId" type="numeric" required="true">
<cfset var exists = false>
<!--- as long as there at least 1 item in the array --->
<cfif arrayLen(variables.myCart) GT 0>
<!--- loop the array and look for the productId --->
<cfloop from="1" to="#arrayLen(variables.myCart)#" index="x">
<cfif variables.myCart[x].productId EQ arguments.productId>
<!--- we found what we came for --->
<cfset exists = true>
<!--- no need to stick around, get out of the loop --->
<cfbreak>
</cfif>
</cfloop>
</cfif>
<cfreturn exists>
</cffunction>
</cfcomponent>
<cfargument type="String" name="targetPage" required="true" />
<cfif NOT structKeyExists(application,"beanFactory") OR structKeyExists(url,"reload")>
<cfset csprops = structNew()>
<cfset csprops.dsn = "cfinferno">
<cfset application.beanFactory = createObject("component","coldspring.beans.DefaultXmlBeanFactory").init(structNew(),csprops)/>
<cfset application.beanFactory.loadBeansFromXmlFile("#expandPath('.')#\config\beans.xml",true)/>
</cfif>
<cfif NOT structKeyExists(session,"cart") OR structKeyExists(url,"reload")>
<cfset session.cart = application.beanFactory.getBean("ShoppingCart")>
</cfif>
<cfreturn true>
</cffunction>
And Finally a quick way to use the cart. Here is my add,update and list method in use and the final result.
<cfset session.cart.update(1,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.
