Ask Ben: Creating A Simple Wish List / Shopping Cart
Someone wanted to create a simple Wish List on their site. This wish list was a single-session "shopping cart" that was not used for checkout. It was used only to keep track of items and to help print out a list that could be brought into a store and given to a store clerk.
For this solution, I have created a ColdFusion component, Cart.cfc, that keeps track of its Items using an internal query. This cart does not have ALL the needed features, but I think it is a pretty good base from which to start. Currently, it only allows you to add items and clear the cart. It does not provide any way to update the quantities directly or delete a given item from the list. However, these can be added with some fairly simple methods that use the Item's PKEY value.
As you can see from the code, there are a few core query columns for the Items query that cannot be altered:
PKEY: The unique ID used for each row which will be used to reference and update a particular row.
NAME: Name of the item in the cart.
SKU: Internal SKU value for inventory.
PRICE: Base cost of the item.
QUANTITY: The number of items that match a particular row of item criteria.
SUB_TOTAL: The cost of the row based on the item price and its quantity.
These are part of the Cart by default. In addition to these columns, you can add custom properties during cart creation (the Init() method call). These properties are added as VARCHAR columns to the internal Items query.
Here is an example of how the Cart.cfc can be used to keep track of items and then to print a list:
<!--- | |
Create an instance of the cart. When we initialize | |
the cart, we are going to add three additional | |
item properties. | |
---> | |
<cfset objCart = CreateObject( "component", "Cart" ).Init( | |
"color, size, sex" | |
) /> | |
<!--- | |
Get an empty-value new item struct from the cart | |
itself. We will populate this struct and add it back | |
into the cart. | |
---> | |
<cfset objItem = objCart.GetNewItem() /> | |
<!--- Now, let's set the value of this new item struct. ---> | |
<cfset objItem.Name = "Girl's Baby Doll T-Shirt" /> | |
<cfset objItem.Price = 15.95 /> | |
<cfset objItem.Quantity = 1 /> | |
<cfset objItem.Color = "Pink" /> | |
<cfset objItem.Size = "M" /> | |
<cfset objItem.Sex = "Female" /> | |
<!--- Add the item back into the cart. ---> | |
<cfset objCart.AddItem( objItem ) /> | |
<!--- | |
Get an empty-value new item struct from the cart | |
itself. We will populate this struct and add it back | |
into the cart. | |
---> | |
<cfset objItem = objCart.GetNewItem() /> | |
<!--- Now, let's set the value of this new item struct. ---> | |
<cfset objItem.Name = "Men's Boxer Briefs" /> | |
<cfset objItem.Price = 7.95 /> | |
<cfset objItem.Quantity = 3 /> | |
<cfset objItem.Color = "Black" /> | |
<cfset objItem.Size = "M" /> | |
<cfset objItem.Sex = "Male" /> | |
<!--- Add the item back into the cart. ---> | |
<cfset objCart.AddItem( objItem ) /> | |
<!--- Get the items out of the cart. ---> | |
<cfset qItem = objCart.GetItems() /> | |
<!--- Output the items (wish list). ---> | |
<cfloop query="qItem"> | |
<h4> | |
#qItem.Name# | |
</h4> | |
<p> | |
Price: #DollarFormat( qItem.price )#<br /> | |
Quantity: #qItem.quantity#<br /> | |
Sub Total: #DollarFormat( qItem.sub_total )#<br /> | |
</p> | |
</cfloop> | |
<h4> | |
Cart Overview: | |
</h4> | |
<p> | |
Size: #objCart.GetSize()#<br /> | |
Total: #DollarFormat( objCart.GetTotal() )#<br /> | |
</p> |
Running the above code, we get the following output:
Girl's Baby Doll T-Shirt
Price: $15.95
Quantity: 1
Sub Total: $15.95Men's Boxer Briefs
Price: $7.95
Quantity: 3
Sub Total: $23.85Cart Overview:
Size: 4
Total: $39.80
I am not an eCommerce person. I have not done that much with shopping carts so I am not sure if this is the best way to go. But, it seems like a fairly straight forward way of keeping a Wish List. Here is the code that makes the Cart.cfc possible:
<cfcomponent | |
output="false"> | |
<!--- | |
Run pseudo constructor. Set up unique key for this | |
instance of the Cart object. | |
---> | |
<cfset VARIABLES.InstanceID = CreateUUID() /> | |
<!--- | |
Set up the Instance structure to hold instance- | |
specific data. | |
---> | |
<cfset VARIABLES.Instance = StructNew() /> | |
<!--- | |
This is the internal query that will be used to | |
keep track of the items in our cart. Notice that | |
our cart query has some default column values. | |
- PKEY: The unique ID used for each row which will be | |
used to reference and update a particular row. | |
- NAME: Name of the item in the cart. | |
- SKU: Internal SKU value for inventory. | |
- PRICE: Base cost of the item. | |
- QUANTITY: The number of items that match a particlar | |
row of item criteria. | |
- SUB_TOTAL: The cost of the row based on the tiem | |
price and its quantity. | |
---> | |
<cfset VARIABLES.Instance.Items = QueryNew( | |
"pkey, name, sku, price, quantity, sub_total", | |
"CF_SQL_INTEGER, CF_SQL_VARCHAR, CF_SQL_VARCHAR, CF_SQL_DECIMAL, CF_SQL_INTEGER, CF_SQL_DECIMAL" | |
) /> | |
<!--- | |
This will be a list of all the additional, | |
custom properties (columns) that were added to | |
the Items query. | |
---> | |
<cfset VARIABLES.Instance.CustomProperties = "" /> | |
<!--- | |
The number of items in the cart. This will be | |
equivalent to the sum of the quantity column of | |
the Items query. | |
---> | |
<cfset VARIABLES.Instance.Size = 0 /> | |
<!--- The total cost of the cart. ---> | |
<cfset VARIABLES.Instance.Total = 0 /> | |
<!--- | |
This is a structure that will be duplicated prior | |
to adding a new item to the query. | |
---> | |
<cfset VARIABLES.Instance.NewItem = StructNew() /> | |
<!--- | |
Now, we can create a "empty-value" struct that will | |
be used to populate new items in the cart. This will | |
include every value EXCEPT the PKEY column and the | |
SUB_TOTAL column as these are auto-generated after | |
insert. First, let's add the values that we KNOW are | |
going to exist. During the constructor (Init method) | |
we will add the rest of the properties that are | |
passed in. | |
---> | |
<cfset VARIABLES.Instance.NewItem.Name = "" /> | |
<cfset VARIABLES.Instance.NewItem.SKU = "" /> | |
<cfset VARIABLES.Instance.NewItem.Price = 0 /> | |
<cfset VARIABLES.Instance.NewItem.Quantity = 0 /> | |
<cffunction | |
name="Init" | |
access="public" | |
returntype="any" | |
output="false" | |
hint="Returns an initialized Cart instance."> | |
<!--- Define arguments. ---> | |
<cfargument | |
name="Properties" | |
type="string" | |
required="false" | |
default="" | |
hint="A list of additional properties / columns to add to our internal Items query. Duplicate values will be ignored." | |
/> | |
<!--- Define the local scope. ---> | |
<cfset var LOCAL = StructNew() /> | |
<!--- Loop over the new properties. ---> | |
<cfloop | |
index="LOCAL.Property" | |
list="#ARGUMENTS.Properties#" | |
delimiters=","> | |
<!--- Make sure we have a trimmed value. ---> | |
<cfset LOCAL.Property = Trim( LOCAL.Property ) /> | |
<!--- | |
Check to see if this property is already a | |
column in our internal Items query. | |
---> | |
<cfif NOT ListFindNoCase( | |
VARIABLES.Instance.Items.ColumnList, | |
LOCAL.Property | |
)> | |
<!--- | |
Add this column. All new columns are added | |
as varchar values to keep it simple. | |
---> | |
<cfset QueryAddColumn( | |
VARIABLES.Instance.Items, | |
LOCAL.Property, | |
"CF_SQL_VARCHAR", | |
ArrayNew( 1 ) | |
) /> | |
<!--- | |
Add this property to our list of | |
custom properties. | |
---> | |
<cfset VARIABLES.Instance.CustomProperties = ListAppend( | |
VARIABLES.Instance.CustomProperties, | |
LOCAL.Property | |
) /> | |
<!--- | |
Add this property to the NewItem struct | |
that has already been created. | |
---> | |
<cfset VARIABLES.Instance.NewItem[ LOCAL.Property ] = "" /> | |
</cfif> | |
</cfloop> | |
<!--- Return This reference. ---> | |
<cfreturn THIS /> | |
</cffunction> | |
<cffunction | |
name="AddItem" | |
access="public" | |
returntype="struct" | |
output="false" | |
hint="Adds a new item to the Items query. If an item with the matching criteria already exists, the quantity is simply updated."> | |
<!--- Define arguments. ---> | |
<cfargument | |
name="Item" | |
type="struct" | |
required="true" | |
hint="The struct generated by a call to GetNewItem()." | |
/> | |
<!--- Define the local scope. ---> | |
<cfset var LOCAL = StructNew() /> | |
<!--- | |
Since we are referencing "shared" memory space, we | |
really should be locking all Add-Item relate logic. | |
Use a named lock based on this CFC's Instance ID. | |
---> | |
<cflock | |
name="#VARIABLES.InstanceID#" | |
type="EXCLUSIVE" | |
timeout="10"> | |
<!--- | |
First thing we want to do is check to see if | |
this item already exists in the Items query. | |
We will know this if key columns match up. | |
---> | |
<cfquery name="LOCAL.Exists" dbtype="query"> | |
SELECT | |
pkey | |
FROM | |
VARIABLES.Instance.Items | |
WHERE | |
name = <cfqueryparam value="#ARGUMENTS.Item.Name#" cfsqltype="CF_SQL_VARCHAR" /> | |
AND | |
sku = <cfqueryparam value="#ARGUMENTS.Item.SKU#" cfsqltype="CF_SQL_VARCHAR" /> | |
<!--- | |
We have to cast this to a decimal | |
because otherwise, it doesn't seem to | |
match up internally. | |
---> | |
AND | |
CAST( price AS DECIMAL ) = <cfqueryparam value="#ARGUMENTS.Item.Price#" cfsqltype="CF_SQL_DECIMAL" /> | |
<!--- | |
Loop over the custom properties to | |
check for equality. Remember that each | |
of the custom properties has been stored | |
as a string. | |
---> | |
<cfloop | |
index="LOCAL.Property" | |
list="#VARIABLES.Instance.CustomProperties#" | |
delimiters=","> | |
AND | |
[#LOCAL.Property#] = <cfqueryparam value="#ARGUMENTS.Item[ LOCAL.Property ]#" cfsqltype="CF_SQL_VARCHAR" /> | |
</cfloop> | |
</cfquery> | |
<!--- | |
Check to see if an existing row of matching | |
data was found. | |
---> | |
<cfif LOCAL.Exists.RecordCount> | |
<!--- | |
Since we found a matching item, all we have | |
to do is update the quantity. Based on the | |
PKEY, get the matching row number. | |
---> | |
<cfset LOCAL.RowIndex = VARIABLES.Instance.Items[ "pkey" ].IndexOf( | |
JavaCast( "int", LOCAL.Exists.pkey ) | |
) /> | |
<!--- | |
Add one to the value of the returned index. | |
Remember, since IndexOf() uses the Java | |
indexing, this value will be zero-based, | |
not one-based. | |
---> | |
<cfset LOCAL.RowIndex = (LOCAL.RowIndex + 1) /> | |
<!--- Update the quantity. ---> | |
<cfset VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ] = (VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ] + ARGUMENTS.Item.Quantity) /> | |
<!--- Update the sub total. ---> | |
<cfset VARIABLES.Instance.Items[ "sub_total" ][ LOCAL.RowIndex ] = (VARIABLES.Instance.Items[ "price" ][ LOCAL.RowIndex ] * VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ]) /> | |
<!--- | |
Set the pkey back into the passed in item | |
so that this row can be easily referenced. | |
---> | |
<cfset ARGUMENTS.Item.Pkey = LOCAL.Exists.pkey /> | |
<cfelse> | |
<!--- | |
Since no matching record could be found, we | |
are going to need to add a completely new | |
record. To start with, we are going to need | |
to get a new PKEY value. | |
---> | |
<cfif VARIABLES.Instance.Items.RecordCount> | |
<!--- | |
Since we do have items in our cart | |
already, we need to add ONE to the MAX | |
of the pkey column (for ease of use). | |
---> | |
<cfset LOCAL.Pkey = ( | |
ArrayMax( | |
VARIABLES.Instance.Items[ "pkey" ] | |
) + 1 | |
) /> | |
<cfelse> | |
<!--- | |
Since we have no items yet, we can just | |
set our first PKEY to 1. | |
---> | |
<cfset LOCAL.Pkey = 1 /> | |
</cfif> | |
<!--- Add a row to the items query. ---> | |
<cfset QueryAddRow( | |
VARIABLES.Instance.Items | |
) /> | |
<!--- | |
Get a short hand value for the new row | |
index. We don't need this, but it makes | |
the code slightly shorter. | |
---> | |
<cfset LOCAL.RowIndex = VARIABLES.Instance.Items.RecordCount /> | |
<!--- Set the PKEY value. ---> | |
<cfset VARIABLES.Instance.Items[ "pkey" ][ LOCAL.RowIndex ] = JavaCast( "int", LOCAL.Pkey ) /> | |
<!--- Set the built-in values. ---> | |
<cfset VARIABLES.Instance.Items[ "price" ][ LOCAL.RowIndex ] = JavaCast( "float", ARGUMENTS.Item.Price ) /> | |
<cfset VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ] = JavaCast( "int", ARGUMENTS.Item.Quantity ) /> | |
<cfset VARIABLES.Instance.Items[ "sub_total" ][ LOCAL.RowIndex ] = JavaCast( "float", (ARGUMENTS.Item.Price * ARGUMENTS.Item.Quantity) ) /> | |
<!--- | |
Set the other string and customer | |
properties. | |
---> | |
<cfloop | |
index="LOCAL.Property" | |
list="name,sku,#VARIABLES.Instance.CustomProperties#" | |
delimiters=","> | |
<cfset VARIABLES.Instance.Items[ LOCAL.Property ][ LOCAL.RowIndex ] = JavaCast( "string", ARGUMENTS.Item[ LOCAL.Property ] ) /> | |
</cfloop> | |
<!--- | |
Set the Pkey value back into the | |
passed in Item. | |
---> | |
<cfset ARGUMENTS.Item.Pkey = LOCAL.Pkey /> | |
</cfif> | |
<!--- | |
ASSERT: At this point, we have either added a | |
new item or updated the quantity of an | |
existing item. Either way, we can update the | |
size and total of the cart. | |
---> | |
<cfset VARIABLES.Instance.Size = ArraySum( | |
VARIABLES.Instance.Items[ "quantity" ] | |
) /> | |
<cfset VARIABLES.Instance.Total = ArraySum( | |
VARIABLES.Instance.Items[ "sub_total" ] | |
) /> | |
</cflock> | |
<!--- Return the updated struct. ---> | |
<cfreturn ARGUMENTS.Item /> | |
</cffunction> | |
<cffunction | |
name="Clear" | |
access="public" | |
returntype="void" | |
output="false" | |
hint="Clears the carts."> | |
<!--- | |
Query the cart using a query of queries. This | |
will keep the structure in tact. | |
---> | |
<cfquery name="VARIABLES.Instance.Items" dbtype="query"> | |
SELECT | |
* | |
FROM | |
VARIABLES.Instance.Items | |
WHERE | |
1 = 0 | |
</cfquery> | |
<!--- Reset the size and total. ---> | |
<cfset VARIABLES.Instance.Size = 0 /> | |
<cfset VARIABLES.Instance.Total = 0 /> | |
<!--- Return out. ---> | |
<cfreturn /> | |
</cffunction> | |
<cffunction | |
name="GetItems" | |
access="public" | |
returntype="query" | |
output="false" | |
hint="Returns a duplicate of the internal Items query."> | |
<cfreturn Duplicate( VARIABLES.Instance.Items ) /> | |
</cffunction> | |
<cffunction | |
name="GetNewItem" | |
access="public" | |
returntype="struct" | |
output="false" | |
hint="This returns and empty-value struct that can be used to add a new item to the Items query."> | |
<!--- | |
Return a dupcliate of the empty-value struct. | |
Remember, since structs are passed by reference, | |
it is important that each call to this method | |
result in an entirely new struct instance. | |
---> | |
<cfreturn Duplicate( | |
VARIABLES.Instance.NewItem | |
) /> | |
</cffunction> | |
<cffunction | |
name="GetSize" | |
access="public" | |
returntype="numeric" | |
output="false" | |
hint="Returns the number of items in the cart."> | |
<cfreturn VARIABLES.Instance.Size /> | |
</cffunction> | |
<cffunction | |
name="GetTotal" | |
access="public" | |
returntype="numeric" | |
output="false" | |
hint="Returns the total price of the cart."> | |
<cfreturn VARIABLES.Instance.Total /> | |
</cffunction> | |
</cfcomponent> |
Want to use code from this post? Check out the license.
Reader Comments
good stuff. just a quick note, missing <cfoutput> around <cfloop query="qItem">
@falconseye,
Yeah, I have (bad)? habit of excluding the CFOutput tags if they wrap an entire page.
I do the same, and don't think it's a bad habbit. Why start/stop the car's engine over and over again, right?