The Shopping Cart Core

Word Count: 215

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

<cfcomponent name="ShoppingCart">

   <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>
Here is an example of my Application component and how i set up the cart.
<cffunction name="onRequestStart" returnType="boolean">
      <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.add(1)>
<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.

Comments

#1 Posted By: Matthias Posted On: 12/15/06 8:47 AM
Nice clean layout of the cart methods.
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 Posted By: Dan Posted On: 12/15/06 9:32 AM |
Author Comment
@Matthis - Thanks for your comments. I have seen carts that show the number of products in your cart, carts that show the number of items, and carts that have show both. I just wanted to give people the option!
#3 Posted By: Peter Bell Posted On: 12/15/06 10:05 AM
Hi Dan,

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 Posted By: Dan Posted On: 12/15/06 10:20 AM |
Author Comment
@Peter - Thanks for the comments. I can totally agree on the cart.get() and why it would make sense. This is just the beginning and its ope for improvement so your notes are great. As far as the attribute support etc I will need to work on that. I think this is the base and i need to come up with a way of extending that based on if the client is going to use "feature xyz".
Thanks Again!
#5 Posted By: Dan Wilson Posted On: 12/15/06 1:28 PM
Dan,

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 Posted By: Sophek Tounn Posted On: 12/15/06 1:32 PM
I started writing a shopping cart cfc a while back but go too busy. I really like to contribue with just ajax stuff via jsmx and the cool UI effects via jquery and interface plugin.

Why should ruby on rails have all the cools products?

Thanks
Sophek
#7 Posted By: Dan Posted On: 12/15/06 1:45 PM |
Author Comment
@Dan - Typo, thanks for the correction and for the compliments!
@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 Posted By: Geoff Posted On: 12/15/06 2:51 PM
Looks interesting Dan

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 Posted By: Dan Posted On: 12/15/06 3:10 PM |
Author Comment
I tend to think in feature sets and to me Inventory is a feature because some customers would use it while others would not. I think Inventory should be a separate component of the cart. Again this is a very basic start, looks for more to come and thank you so much for you comments / input!
#10 Posted By: Dan Posted On: 12/15/06 6:45 PM |
Author Comment
I was giving the cart some more thought on my way home from work and came to the following conclusion. If we are taking Peter's recommendations for (and I am) splitting the cart into 3 main core features - the catalog, cart, and checkout then the inventory component and anything associated would be a a part of the catalog. The reason you would do this is during display of times you would want to tell the user right then and there that there are no items available. This would then make much more sense tying directly into our Product Component.
#11 Posted By: Geoff Posted On: 12/15/06 7:51 PM
Yes...

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 Posted By: Kola Posted On: 1/30/07 12:12 PM
Dan

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 Posted By: Nick Tong Posted On: 3/9/07 1:14 PM
Nice article Dan, well written and some interesting comments.
#14 Posted By: Nate Posted On: 5/15/07 6:00 PM
I'd love to see any jquery or ajax user interface enhancements to go along with this great Coldfusion server side code of yours. Does anyone have some code to allow the user to make updates to the cart without a page refresh?


Post Your Comment

Leave this field empty







Show Captcha

If you subscribe, any new posts to this thread will be sent to your email address.

Copyright © 2007 Dan Vega | BlogCFC was created by Raymond Camden. This blog is running version 5.8.001.