12 min read

Our next task in creating the storefront is to create the shopping cart. This will allow users to select the products they wish to purchase. Users will be able to select, edit, and delete items from their shopping cart.

Lets get started.

Creating the Cart Model and Resources

We will start by creating our model and model resources. The Cart Model differs from our previous model in the fact that it will use the session to store its data instead of the database.

Cart Model

The Cart Model will store the products that they wish to purchase. Therefore, the Cart Model will contain Cart Items that will be stored in the session. Let’s create this class now.

application/modules/storefront/models/Cart.php

class Storefront_Model_Cart extends SF_Model_Abstract implements
SeekableIterator, Countable, ArrayAccess
{
protected $_items = array();
protected $_subTotal = 0;
protected $_total = 0;
protected $_shipping = 0;
protected $_sessionNamespace;
public function init()
{
$this->loadSession();
}
public function addItem(
Storefront_Resource_Product_Item_Interface $product,$qty)
{
if (0 > $qty) {
return false;
}
if (0 == $qty) {
$this->removeItem($product);
return false;
}
$item = new Storefront_Resource_Cart_Item(
$product, $qty
);
$this->_items[$item->productId] = $item;
$this->persist();
return $item;
}
public function removeItem($product)
{
if (is_int($product)) {
unset($this->_items[$product]);
}
if ($product instanceof
Storefront_Resource_Product_Item_Interface) {
unset($this->_items[$product->productId]);
}
$this->persist();
}
public function setSessionNs(Zend_Session_Namespace $ns)
{
$this->_sessionNamespace = $ns;
}
public function getSessionNs()
{
if (null === $this->_sessionNamespace) {
$this->setSessionNs(new
Zend_Session_Namespace(__CLASS__));
}
return $this->_sessionNamespace;
}
public function persist()
{
$this->getSessionNs()->items = $this->_items;
$this->getSessionNs()->shipping = $this->getShippingCost();
}
public function loadSession()
{
if (isset($this->getSessionNs()->items)) {
$this->_items = $this->getSessionNs()->items;
}
if (isset($this->getSessionNs()->shipping)) {
$this->setShippingCost($this->getSessionNs()->shipping);
}
}
public function CalculateTotals()
{
$sub = 0;
foreach ($this as $item) {
$sub = $sub + $item->getLineCost();
}
$this->_subTotal = $sub;
$this->_total = $this->_subTotal + (float) $this->_shipping;
}
public function setShippingCost($cost)
{
$this->_shipping = $cost;
$this->CalculateTotals();
$this->persist();
}
public function getShippingCost()
{
$this->CalculateTotals();
return $this->_shipping;
}
public function getSubTotal()
{
$this->CalculateTotals();
return $this->_subTotal;
}
public function getTotal()
{
$this->CalculateTotals();
return $this->_total;
}
/*...*/
}

We can see that the Cart Model class is fairly weighty and in fact, we have not included the full class here. The reason we have slightly truncated the class is that we are implementing the SeekableIterator, Countable, and ArrayAccess interfaces. These interfaces are defined by PHP’s SPL Library and we use them to provide a better way to interact with the cart data. For the complete code, copy the methods below getTotal() from the example files for this article. We will look at what each method does shortly in the Cart Model implementation section, but first, let’s look at what functionality the SPL interfaces allow us to add.

Cart Model interfaces

The SeekableIterator interface allows us to access our cart data in these ways:

// iterate over the cart
foreach($cart as $item) {}
// seek an item at a position
$cart->seek(1);
// standard iterator access
$cart->rewind();
$cart->next();
$cart->current();

The Countable interface allows us to count the items in our cart:

count($cart);

The ArrayAccess interface allows us to access our cart like an array:

$cart[0];

Obviously, the interfaces provide no concrete implementation for the functionality, so we have to provide it on our own. The methods not listed in the previous code listing are:

  • offsetExists($key)
  • offsetGet($key)
  • offsetSet($key, $value)
  • offsetUnset($key)
  • current()
  • key()
  • next()
  • rewind()
  • valid()
  • seek($index)
  • count()

We will not cover the actual implementation of these interfaces, as they are standard to PHP. However, you will need to copy all these methods from the example files to get the Cart Model working.

Documentation for the SPL library can be found athttp://www.php.net/~helly/php/ext/spl/

 

 

Cart Model implementation

Going back to our code listing, let’s now look at how the Cart Model is implemented. Let’s start by looking at the properties and methods of the class.

The Cart Model has the following class properties:

  • $_items:An array of cart items
  • $_subTotal: Monetary total of cart items
  • $_total: Monetary total of cart items plus shipping
  • $_shipping: The shipping cost
  • $_sessionNamespace: The session store

The Cart Model has the following methods:

  • init(): Called during construct and loads the session data
  • addItem(Storefront_Resource_Product_Item_Interface $product, $qty): Adds or updates items in the cart
  • removeItem($product): Removes a cart item
  • setSessionNs(Zend_Session_Namespace $ns): Sets the session instance to use for storage
  • getSessionNs(): Gets the current session instance
  • persist(): Saves the cart data to the session
  • loadSession(): Loads the stored session values
  • calculateTotals(): Calculates the cart totals
  • setShippingCost($cost): Sets the shipping cost
  • getShippingCost(): Gets the shipping cost
  • getSubTotal(): Gets the total cost for items in the cart (not including the shipping)
  • getTotal(): Gets the subtotal plus the shipping cost

When we instantiate a new Cart Model instance, the init() method is called. This is defined in the SF_Model_Abstract class and is called by the __construct() method. This enables us to easily extend the class’s instantiation process without having to override the constructor.

The init() method simply calls the loadSession() method. This method populates the model with the cart items and shipping information stored in the session. The Cart Model uses Zend_Session_Namespace to store this data, which provides an easy-to-use interface to the $_SESSION variable. If we look at the loadSession() method, we see that it tests whether the items and shipping properties are set in the session namespace. If they are, then we set these values on the Cart Model.

To get the session namespace, we use the getSessionNs() method. This method checks if the $_sessionNs property is set and returns it. Otherwise it will lazy load a new Zend_Session_Namespace instance for us. When using Zend_Session_ Namespace, we must provide a string to its constructor that defines the name of the namespace to store our data in. This will then create a clean place to add variables to, without worrying about variable name clashes. For the Cart Model, the default namespace will be Storefront_Model_Cart.

The Zend_Session_Namespace component provides a range of functionality that we can use to control the session. For example, we can set the expiration time as follows:

$ns = new Zend_Session_Namespace('test');
$ns->setExpirationSeconds(60, 'items');
$ns->setExpirationHops(10);
$ns->setExpirationSeconds(120);

This code would set the item’s property expiration to 60 seconds and the namespaces expiration to 10 hops (requests) or 120 seconds, whichever is reached first. The useful thing about this is that the expiration is not global. Therefore, we can have specialized expiration per session namespace. There is a full list of Zend_Session_Namespace functionalities in the reference manual.

Testing with Zend_Session and Zend_Session_Namespace
Testing with the session components can be fairly diffi cult. For the Cart Model, we use the setSessionNs() method to allow us to inject a mock object for testing, which you can see in the Cart Model unit tests. There are plans to rewrite the session components to make testing easier in the future, so keep an eye out for those updates.

To add an item to the cart, we use the addItem() method. This method accepts two parameters,$product and $qty. The $product parameter must be an instance of the Storefront_Resource_Product_Item class, and the $qty parameter must be an integer that defines the quantity that the customer wants to order.

If the addItem() method receives a valid $qty, then it will create a new Storefront_Resource_Cart_Item and add it to the $_items array using the productId as the array key. We then call the persist() method. This method simply stores all the relevant cart data in the session namespace for us. You will notice that we are not using a Model Resource in the Cart Model and instead we are directly instantiating a Model Resource Item. This is because the Model Resources represent store items and the Cart Model is already doing this for us so it is not needed.

To remove an item, we use the removeItem() method. This accepts a single parameter $product which can be either an integer or a Storefront_Resource_Product_Item instance. The matching cart item will be removed from the $_items array and the data will be saved to the session. Also, addItem() will call removeItem() if the quantity is set to zero.

The other methods in the Cart Model are used to calculate the monetary totals for the cart and to set the shipping. We will not cover these in detail here as they are fairly simple mathematical calculations.

Cart Model Resources

Now that we have our Model created, let’s create the Resource Interface and concrete Resource class for our Model to use.

application/modules/storefront/models/resources/Cart/Item/Interface.php

interface Storefront_Resource_Cart_Item_Interface
{
public function getLineCost();
}

The Cart Resource Item has a very simple interface that has one method, getLineCost(). This method is used when calculating the cart totals in the Cart Model.

application/modules/storefront/models/resources/Cart/Item.php

class Storefront_Resource_Cart_Item
implements Storefront_Resource_Cart_Item_Interface
{
public $productId;
public $name;
public $price;
public $taxable;
public $discountPercent;
public $qty;
public function __construct(Storefront_Resource_Product_Item_
Interface $product, $qty)
{
$this->productId = (int) $product->productId;
$this->name = $product->name;
$this->price = (float) $product->getPrice(false,false);
$this->taxable = $product->taxable;
$this->discountPercent = (int) $product->discountPercent;
$this->qty = (int) $qty;
}
public function getLineCost()
{
$price = $this->price;
if (0 !== $this->discountPercent) {
$discounted = ($price*$this->discountPercent)/100;
$price = round($price - $discounted, 2);
}
if ('Yes' === $this->taxable) {
$taxService = new Storefront_Service_Taxation();
$price = $taxService->addTax($price);
}
return $price * $this->qty;
}
}

The concrete Cart Resource Item has two methods __construct() and getLineCost(). The constructor accepts two parameters $product and $qty that must be a Storefront_Resource_Product_Item_Interface instance and integer respectively. This method will then simply copy the values from the product instance and store them in the matching public properties. We do this because we do not want to simply store the product instance because it has all the database connection data contained within. This object will be serialized and stored in the session.

The getLineCost() method simply calculates the cost of the product adding tax and discounts and then multiplies it by the given quantity.

Shipping Model

We also need to create a Shipping Model so that the user can select what type of shipping they would like. This Model will simply act as a data store for some predefined shipping values.

application/modules/storefront/models/Shipping.php

class Storefront_Model_Shipping extends SF_Model_Abstract
{
protected $_shippingData = array(
'Standard' => 1.99,
'Special' => 5.99,
);
public function getShippingOptions()
{
return $this->_shippingData;
}
}

The shipping Model is very simple and only contains the shipping options and a single method to retrieve them. In a normal application, shipping would usually be stored in the database and most likely have its own set of business rules. For the Storefront, we are not creating a complete ordering process so we do not need these complications.

Creating the Cart Controller

With our Model and Model Resources created, we can now start wiring the application layer together. The Cart will have a single Controller, CartController that will be used to add, view, and update cart items stored in the Cart Model.

application/modules/storefront/controllers/CartController.php

class Storefront_CartController extends Zend_Controller_Action
{
protected $_cartModel;
protected $_catalogModel;
public function init()
{
$this->_cartModel = new Storefront_Model_Cart();
$this->_catalogModel = new Storefront_Model_Catalog();
}
public function addAction()
{
$product = $this->_catalogModel->getProductById(
$this->_getParam('productId')
);
if(null === $product) {
throw new SF_Exception('Product could not be added
to cart as it does not exist'
);
}
$this->_cartModel->addItem(
$product, $this->_getParam('qty')
);
$return = rtrim(
$this->getRequest()->getBaseUrl(), '/'
) . $this->_getParam('returnto');
$redirector = $this->getHelper('redirector');
return $redirector->gotoUrl($return);
}
public function viewAction()
{
$this->view->cartModel = $this->_cartModel;
}
public function updateAction()
{
foreach($this->_getParam('quantity') as $id => $value)
{
$product = $this->_catalogModel->getProductById($id);
if (null !== $product) {
$this->_cartModel->addItem($product, $value);
}
}
$this->_cartModel->setShippingCost(
$this->_getParam('shipping')
);
return $this->_helper->redirector('view');
}
}

The Cart Controller has three actions that provide a way to:

  • add: add cart items
  • view: view the cart contents
  • update: update cart items

The addAction() first tries to find the product to be added to the cart. This is done by searching for the product by its productId field, which is passed either in the URL or by post using the Catalog Model. If the product is not found, then we throw an SF_Exception stating so. Next, we add the product to the cart using the addItem() method. When adding the product, we also pass in the qty. The qty can again be either in the URL or post.

Once the product has been successfully added to the cart, we then need to redirect back to the page where the product was added. As we can have multiple locations, we send a returnto variable with the add request. This will contain the URL to redirect back to, once the item has been added to the cart. To stop people from being able to redirect away from the storefront, we prepend the baseurl to the redirect string. To perform the actual redirect, we use the redirector Action Helper’s gotoUrl() method. This will create an HTTP redirect for us.

The viewAction() simply assigns the Cart Model to the cartModel View property. Most of the cart viewing functionality has been pushed to the Cart View Helper and Forms, which we will create shortly.

The updateAction() is used to update the Cart Items already stored in the cart. The first part of this updates the quantities. The quantities will be posted to the Action as an array in the quantity parameter. The array will contain the productId as the array key, and the quantity as the value. Therefore, we iterate over the array fi nding the product by its ID and adding it to the cart. The addItem() method will then update the quantity for us if the item exists and remove any with a zero quantity. Once we have updated the cart quantities, we set the shipping and redirect back to the viewAction.

>> Continue Reading Creating a Shopping Cart using Zend Framework: Part 2

LEAVE A REPLY

Please enter your comment!
Please enter your name here