Implementing shopping cart... design idea and some issues
Moderator: General Moderators
Implementing shopping cart... design idea and some issues
Hi,
I did a quick search and found many topics containing 'cart', but it seems no one was related to actual cart implementation. Additionally, I already have a design plan, but I would like to discuss it, to hear critiques and to find answer of some, right now, confusing points.
FYI I'm not implementing a shopping cart, it's just king of brainstorming... actually a friend of mine asked me how would I implement a shopping cart and here is what I've answered in 10 minutes and is my basic idea.
***
Goals:
* My friend asked about implementing shopping cart when he wants to store cart contents in session for non logged in users, in database for the users with accounts, and to merge them when user logs in. Additionally he would like to be able to store cart in 2 places at time (session and cookie), although I can't find a good reason and I have a solution (see bellow)
* my goal as a software designer: to build it flexible, so when wishlist is implemented for example, to be able to merge wishlist contents with the cart for example. I think most of the cart code can be reused for the wishlist actually
***
Terminology:
I came up with the fact that I need a:
* Items container, which contains the products in the cart, as well as those in the wishlist
* Storage, which handles the loading and saving data
***
Obviously, we will have abstract storage class, with subclasses for dealing with database, session, cookie
If we really need to load/save data to more than one place, use Storage Composite
The items container should be able to add item, remove item, remove all items, merge it's contents with another container or replace contents with those from another container.
The cart class.
Singleton, because there is no reason we would like to have more than 1 cart (we still can have multiple items containers).
It have an Items container or it is an Items container itself. Actually, I would prefer that it have a items container, because if we need multiple items containers (for example, if we implement multiple providers, to separate items from the different providers internally in the cart). So it have add/remove/merge methods, that just pass to the container's methods...
And I thought about using Strategy for the storage, so we just pass the storage class to the Cart.
Any critiques? Something important missing?
And what is confusing me is really the storage. I mean the cart is meant to handle load and save (saving for sure). On the other hand, I can load an item container elsewhere, for example wishlist. OK, if we say that cart loads it contents, and wishlist loads its own, that we will just get the container from the wishlist and pass to the cart. But: after login, we should merge the cart contents with those in the database. And here I have no clear design decision. Should we pass another Storage to the Cart and force re-initialize? Or if we load the container and pass to the cart, what is the clear design of container loading/saving, having in mind that cart should also be possible to load/save?
What you think about that? How would you design the Cart class?
I did a quick search and found many topics containing 'cart', but it seems no one was related to actual cart implementation. Additionally, I already have a design plan, but I would like to discuss it, to hear critiques and to find answer of some, right now, confusing points.
FYI I'm not implementing a shopping cart, it's just king of brainstorming... actually a friend of mine asked me how would I implement a shopping cart and here is what I've answered in 10 minutes and is my basic idea.
***
Goals:
* My friend asked about implementing shopping cart when he wants to store cart contents in session for non logged in users, in database for the users with accounts, and to merge them when user logs in. Additionally he would like to be able to store cart in 2 places at time (session and cookie), although I can't find a good reason and I have a solution (see bellow)
* my goal as a software designer: to build it flexible, so when wishlist is implemented for example, to be able to merge wishlist contents with the cart for example. I think most of the cart code can be reused for the wishlist actually
***
Terminology:
I came up with the fact that I need a:
* Items container, which contains the products in the cart, as well as those in the wishlist
* Storage, which handles the loading and saving data
***
Obviously, we will have abstract storage class, with subclasses for dealing with database, session, cookie
If we really need to load/save data to more than one place, use Storage Composite
The items container should be able to add item, remove item, remove all items, merge it's contents with another container or replace contents with those from another container.
The cart class.
Singleton, because there is no reason we would like to have more than 1 cart (we still can have multiple items containers).
It have an Items container or it is an Items container itself. Actually, I would prefer that it have a items container, because if we need multiple items containers (for example, if we implement multiple providers, to separate items from the different providers internally in the cart). So it have add/remove/merge methods, that just pass to the container's methods...
And I thought about using Strategy for the storage, so we just pass the storage class to the Cart.
Any critiques? Something important missing?
And what is confusing me is really the storage. I mean the cart is meant to handle load and save (saving for sure). On the other hand, I can load an item container elsewhere, for example wishlist. OK, if we say that cart loads it contents, and wishlist loads its own, that we will just get the container from the wishlist and pass to the cart. But: after login, we should merge the cart contents with those in the database. And here I have no clear design decision. Should we pass another Storage to the Cart and force re-initialize? Or if we load the container and pass to the cart, what is the clear design of container loading/saving, having in mind that cart should also be possible to load/save?
What you think about that? How would you design the Cart class?
- Christopher
- Site Administrator
- Posts: 13596
- Joined: Wed Aug 25, 2004 7:54 pm
- Location: New York, NY, US
Re: Implementing shopping cart... design idea and some issues
I'll have more comments and hopefully some code examples to follow (I am at a conference). But I did want to say that there are lots of objects that you would never have more than one, but that is not the reason to use a Singleton. In fact in this case it just adds the over head of calling the static to get the instance every time you show the cart. And there actually are good reasons to have multiple carts, such as saved carts, favorites, etc. which can reuse the same code.Darhazer wrote:Singleton, because there is no reason we would like to have more than 1 cart (we still can have multiple items containers).
(#10850)
Re: Implementing shopping cart... design idea and some issues
Thank you, I will wait for your comments.
OK, what I mean for the cart and singleton is that we need an easy access to the current cart, the one that will be merged with the saved cart upon login, the one that user will eventually check out... And this is why I separated the items container from the cart - the saved cart is actually saved items container, wishlist and favorites - too.
OK, what I mean for the cart and singleton is that we need an easy access to the current cart, the one that will be merged with the saved cart upon login, the one that user will eventually check out... And this is why I separated the items container from the cart - the saved cart is actually saved items container, wishlist and favorites - too.
- kaisellgren
- DevNet Resident
- Posts: 1675
- Joined: Sat Jan 07, 2006 5:52 am
- Location: Lahti, Finland.
Re: Implementing shopping cart... design idea and some issues
I agree. I would only use a Singleton if I wanted to make sure no one is ever going to instantiate it more than once.arborint wrote:But I did want to say that there are lots of objects that you would never have more than one, but that is not the reason to use a Singleton.
Neither do I, so, don't do it. Besides, cookies cannot contain much of information. A cookie can store something like 640 bytes of meaningful data, so, I would never recommend it for any kind of data storage.Darhazer wrote:Additionally he would like to be able to store cart in 2 places at time (session and cookie), although I can't find a good reason
- allspiritseve
- DevNet Resident
- Posts: 1174
- Joined: Thu Mar 06, 2008 8:23 am
- Location: Ann Arbor, MI (USA)
Re: Implementing shopping cart... design idea and some issues
I currently have a cart ID from the database saved in a cookie. I'm not sure the best way to manage that and a user's cart, maybe a user column on your cart table?Darhazer wrote:My friend asked about implementing shopping cart when he wants to store cart contents in session for non logged in users, in database for the users with accounts, and to merge them when user logs in.
Here's my cart class, it may give you some ideas.
Code: Select all
class Cart {
private $id;
private $userId;
private $items;
private $discounts;
public function __construct ($id = null, $userId = null) {
$this->id = $id;
$this->userId = $userId;
$this->items = new Icebox_Collection();
$this->discounts = new Icebox_Collection();
}
public function getId() {
return $this->id;
}
public function setId ($id) {
$this->id = $id;
}
public function getUserId() {
return $this->userId;
}
public function setUserId ($userId) {
$this->userId = $userId;
}
public function addItem ($item) {
foreach ($this->items as $existing_item) {
if ($existing_item->getAttributeId() == $item->getAttributeId()) {
return $existing_item->setQuantity ($existing_item->getQuantity() + $item->getQuantity());
}
}
return $this->items->add ($item);
}
public function removeItem ($id) {
$this->items->remove ($id);
}
public function getItems() {
return $this->items;
}
public function setItems ($items) {
$this->items = $items;
}
public function getItem ($key) {
return $this->items->get ($key);
}
public function removeItems() {
$this->items = new Icebox_Collection();
}
public function getWeight() {
$weight = 0;
foreach ($this->items as $item) {
$weight += ($item->getWeight());
}
return $weight;
}
public function getSubtotal() {
$total = 0;
foreach ($this->items as $item) {
$total += ($item->getTotal());
}
return $total;
}
public function getTotal() {
return $this->getSubtotal() - $this->getDiscountTotal() + $this->shipping + $this->getTax();
}
public function getItemCount() {
$count = 0;
foreach ($this->items as $item) {
$count += $item->getQuantity();
}
return $count;
}
public function setZipCode ($zipCode) {
$this->zipCode = $zipCode;
}
public function getZipCode() {
return $this->zipCode;
}
public function setShipping ($shipping) {
$this->shipping = $shipping;
}
public function getShipping() {
return $this->shipping;
}
public function getTax() {
return ($this->getSubtotal() - $this->getDiscountTotal() + $this->getShipping()) * .06;
}
public function setDestType ($type) {
$this->destType = $type;
}
public function getDestType() {
return $this->destType;
}
public function addDiscount ($discount) {
$this->discounts->add ($discount);
}
public function getDiscounts() {
return $this->discounts;
}
public function getDiscountTotal() {
$discountTotal = 0;
foreach ($this->discounts as $discount) {
$discountTotal += $discount->apply ($this);
}
return $discountTotal;
}
}Re: Implementing shopping cart... design idea and some issues
Thank you for your answers. The question actually was how you will design the loading and storing of the cart contents?
allspiritseve how you are storing your cart contents?
In implementations I have seen, the whole object is serialized in the session. I don't want to do this, because:
* Independent storage. I can save in session, database or everywhere else
* Flexibility... serializing data means that we should load all the data, unserialize it and then perform actions, and maybe we will need to load only part of the data.
allspiritseve how you are storing your cart contents?
In implementations I have seen, the whole object is serialized in the session. I don't want to do this, because:
* Independent storage. I can save in session, database or everywhere else
* Flexibility... serializing data means that we should load all the data, unserialize it and then perform actions, and maybe we will need to load only part of the data.
- allspiritseve
- DevNet Resident
- Posts: 1174
- Joined: Thu Mar 06, 2008 8:23 am
- Location: Ann Arbor, MI (USA)
Re: Implementing shopping cart... design idea and some issues
In the database. I have a table for carts that saves the zip code, destination type (home/business) and shipping costs. I have a table for cart items that contains quantity and a foreign key to a product id.Darhazer wrote:allspiritseve how you are storing your cart contents?
Re: Implementing shopping cart... design idea and some issues
Well, I did not ask where, but how? What's your cart saving / loading code?
Can you show also the interface of the Icebox_Collection ? (no full implementation needed, just interface)
Thank you.
Can you show also the interface of the Icebox_Collection ? (no full implementation needed, just interface)
Thank you.
- jayshields
- DevNet Resident
- Posts: 1912
- Joined: Mon Aug 22, 2005 12:11 pm
- Location: Leeds/Manchester, England
Re: Implementing shopping cart... design idea and some issues
@allspiritsteve It's not such a good idea to hard code tax percentages. In the UK the VAT changed from 17.5% to 15% just recently.
- allspiritseve
- DevNet Resident
- Posts: 1174
- Joined: Thu Mar 06, 2008 8:23 am
- Location: Ann Arbor, MI (USA)
Re: Implementing shopping cart... design idea and some issues
Well, this was code specifically for one site, and my state (Michigan) has had a 6% sales tax for as long as I've been alivejayshields wrote:@allspiritsteve It's not such a good idea to hard code tax percentages. In the UK the VAT changed from 17.5% to 15% just recently.
Here's the Cart Mapper:Darhazer wrote:Well, I did not ask where, but how? What's your cart saving / loading code?
Code: Select all
class Products_Cart_Mapper {
public function __construct ($registry) {
$this->registry = $registry;
}
public function loadCart ($array) {
$cart = new Cart ($array['id'], $array['user_id']);
$cart->setZipCode ($array['zip_code']);
$cart->setShipping ($array['shipping']);
$cart->setDestType ($array['dest_type']);
$cart->setItems ($this->registry->get ('mapper:items')->loadItemsInCart ($cart));
$cart->addDiscount (new Cart_Discounts_VolumeFifteenPercent());
$cart->addDiscount (new Cart_Discounts_VolumeTwentyFivePercent());
return $cart;
}
public function unloadCart ($cart, $stmt) {
}
public function create() {
return $this->insert (new Cart());
}
public function getById ($id) {
$stmt = $this->registry->db->prepare('SELECT * FROM carts WHERE id = :id');
$stmt->bindValue(':id', $id);
$stmt->execute();
return $this->loadCart ($stmt->fetch (PDO::FETCH_ASSOC));
}
public function save ($cart) {
$cart->getId() ? $this->update ($cart) : $this->insert ($cart);
}
public function insert ($cart) {
$stmt = $this->registry->db->prepare ('INSERT INTO carts SET user_id = :user_id, zip_code = :zip_code, shipping = :shipping, dest_type = :dest_type');
$stmt->bindValue (':user_id', $cart->getUserId());
$stmt->bindValue (':zip_code', $cart->getZipCode());
$stmt->bindValue (':shipping', $cart->getShipping());
$stmt->bindValue (':dest_type', $cart->getDestType());
$stmt->execute();
$cart->setId ($this->registry->db->lastInsertId());
$cart->setItems ($this->registry->get('mapper:items')->saveItemsInCart ($cart->getItems(), $cart));
return $cart;
}
public function update ($cart) {
$stmt = $this->registry->db->prepare('UPDATE carts SET user_id = :user_id, zip_code = :zip_code, shipping = :shipping, dest_type = :dest_type WHERE id = :id');
$stmt->bindValue (':user_id', $cart->getUserId());
$stmt->bindValue (':zip_code', $cart->getZipCode());
$stmt->bindValue (':shipping', $cart->getShipping());
$stmt->bindValue (':dest_type', $cart->getDestType());
$stmt->bindValue (':id', $cart->getId());
$stmt->execute();
$this->registry->get('mapper:items')->saveItemsInCart ($cart->getItems(), $cart);
return $cart;
}
public function delete ($cart) {
$this->registry->get('mapper:items')->deleteItemsInCart ($cart->getItems(), $cart);
$stmt = $this->registry->db->prepare ('DELETE FROM carts WHERE id = :id');
$stmt->bindValue (':id', $cart->getId());
$stmt->execute();
}
}Code: Select all
class Products_Cart_ItemsMapper {
public function __construct ($registry) {
$this->registry = $registry;
}
public function loadItem ($array) {
extract($array);
$item = new Cart_Item(new Product($title,$description,$price,$category_id,$attribute_id),$quantity);
$item->setId($id);
return $item;
}
public function loadItemsInCart ($cart) {
$stmt = $this->registry->db->prepare('
SELECT
cart_items.*,
product_attributes.id as attribute_id,
product_attributes.price_retail as price,
product_attributes.description as description,
products.title as title,
products.category_id as category_id
FROM cart_items
INNER JOIN product_attributes
ON (cart_items.attribute_id = product_attributes.id)
INNER JOIN products
ON (product_attributes.product_id = products.id)
WHERE cart_id = :cart_id');
$stmt->bindValue (':cart_id', $cart->getId());
$stmt->execute();
$items = new Icebox_Collection();
foreach ($stmt->fetchAll (PDO::FETCH_ASSOC) as $item) {
$items->add ($item['id'], $this->loadItem ($item));
}
return $items;
}
public function saveItemsInCart ($items, $cart) {
$stmt = $this->registry->db->prepare('DELETE FROM cart_items WHERE cart_id = :cart_id');
$stmt->bindValue (':cart_id', $cart->getId());
$stmt->execute();
foreach ($items as $item) {
$stmt = $this->registry->db->prepare('INSERT INTO cart_items SET cart_id = :cart_id, attribute_id = :attribute_id, quantity = :quantity');
$stmt->bindValue (':cart_id', $cart->getId());
$stmt->bindValue (':attribute_id', $item->getAttributeId());
$stmt->bindValue (':quantity', $item->getQuantity());
$stmt->execute();
$item->setId ($this->registry->db->lastInsertId());
}
return $items;
}
public function deleteItemsInCart ($items, $cart) {
$stmt = $this->registry->db->prepare('DELETE FROM cart_items WHERE cart_id = :cart_id');
$stmt->bindValue (':cart_id', $cart->getId());
$stmt->execute();
}
}Code: Select all
class Icebox_Collection implements Iterator, ArrayAccess {
function __construct ($collection = array();
function get ($key);
function add();
function remove ($key);
function count();
function slice ($offset, $length);
function reverse();
function has ($key);
function current();
function key();
function next();
function rewind();
function valid();
function toArray();
function join ($glue);
function order ($sorter);
function orderBy ($key, $order = 'asc');
function toString ($glue);
function set ($key, $value);
function __get ($key);
function __set ($key, $value);
function __toString();
function offsetExists ($offset);
function offsetGet ($offset);
function offsetSet ($offset, $value);
function offsetUnset ($offset);
}- allspiritseve
- DevNet Resident
- Posts: 1174
- Joined: Thu Mar 06, 2008 8:23 am
- Location: Ann Arbor, MI (USA)
Re: Implementing shopping cart... design idea and some issues
@Darhazer sorry, I know that's probably a lot to go through, but I'm curious to see what you think. Can I answer any questions for you?
Re: Implementing shopping cart... design idea and some issues
allspiritseve thank you for the code examples, but it is far away from the design I have in mind. What I'm thinking about is:
So, actually, the storage knows how to save and load itemscontainer.
The cart uses the storage member to load and save it's contents
The only problem is that I have to pass the default storage somewhere (I will change the storage upon login), and using a static function getCurrentCart is not the best option to do so, unless I do not pass the storage object to the getCurrentCart. On the other hand, if I call StorageFactory::getStorage() in the constructor, I'm coupling the cart with the StorageFactory....
Code: Select all
abstract class ItemsContainerStorage {
public function load(); // returns an ItemsContainer
public function save(ItemsContainer $container); // saves the container
}
class ItemsContainer implements Iterator, ArrayAccess;
class Cart {
protected $_itemsContainer;
protected $_storage;
public function setStorage(ItemsContainterStorage $storage);
public function addItem();
public function removeItem();
public function merge(ItemsContainer $container);
public static function getCurrentCart();
}The cart uses the storage member to load and save it's contents
The only problem is that I have to pass the default storage somewhere (I will change the storage upon login), and using a static function getCurrentCart is not the best option to do so, unless I do not pass the storage object to the getCurrentCart. On the other hand, if I call StorageFactory::getStorage() in the constructor, I'm coupling the cart with the StorageFactory....