Enhancing the User Experience with PHP 5 Ecommerce: Part 2

Exclusive offer: get 50% off this eBook here
PHP 5 E-commerce Development

PHP 5 E-commerce Development — Save 50%

Create a flexible framework in PHP for a powerful ecommerce solution

$23.99    $12.00
by Michael Peacock | January 2010 | e-Commerce PHP

Read Part One of Enhancing the User Experience with PHP 5 Ecommerce here.
Read Part Three of Enhancing the User Experience with PHP 5 Ecommerce here.

Providing wish lists

Wish lists allow customers to maintain a list of products that they would like to purchase at some point, or that they would like others to purchase for them as a gift.

Creating the structure

To effectively maintain wish lists for customers, we need to keep a record of:

  • The product the customer desires
  • The quantity of the product
  • If they are a logged-in customer, their user ID
  • If they are not a logged-in customer, some way to identify their wish-list products for the duration of their visit to the site
  • The date they added the products to their wish list
  • The priority of the product in their wish lists; that is, if they really want the product, or if it is something they wouldn't mind having

Let's translate that into a suitable database table that our framework can interact with:

Field

Type

Description

ID

Integer (Primary Key, Auto Increment)

A reference for the database

Product

Integer

The product the user wishes to purchase

Quantity

Integer

The number of them the user would like

Date added

Datetime

The date they added the product to their wish list

Priority

Integer

Relative to other products in their wish list, and how important is this one

Session ID

Varcharr

The user's session id(so they don't need to be logged in)

IP Address

Varchar

The user's IP address (so they don't need to be logged in)

By combining the session ID and IP address of the customer, along with the timestamp of when they added the product to their wish list, we can maintain a record of their wish list for the duration of their visit. Of course, they would need to register, or log in, before leaving the site, for their wish list to be permanently saved. This also introduces an element of maintenance to this feature, as once a customer who has not logged in closes their session, their wish-list data cannot be retrieved, so we would need to implement some garbage collection functions to prune this table.

The following SQL represents this table:

CREATE TABLE `wish_list_products` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product` INT NOT NULL,
`quantity` INT NOT NULL,
`user` INT NOT NULL,
`dateadded` TIMESTAMP NOT NULL
DEFAULT CURRENT_TIMESTAMP,
`priority` INT NOT NULL,
`sessionID` VARCHAR( 50 ) NOT NULL,
`IPAddress` VARCHAR( 50 ) NOT NULL,
INDEX ( `product` )
) ENGINE = INNODB COMMENT = 'Wish list products'
ALTER TABLE `wish_list_products` ADD FOREIGN KEY ( `product` )
REFERENCES `book4`.`content` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE;

Saving wishes

Now that we have a structure in place for storing wish-list products, we need to have a process available to save them into the database. This involves a link or button placed on the product view, and either some modifications to our product controller, or a wish-list controller, to save the wish. As wish lists will have their own controller and model for viewing and managing the lists, we may as well add the functionality into the wish-list controller.

So we will need:

  • a controller
  • a link in our product view

Wish-list controller

The controller needs to detect if the user is logged in or not; if they are, then it should add products to the user's wish list; otherwise, it should be added to a session-based wish list, which lasts for the duration of the user's session.

The controller also needs to detect if the product is valid; we can do this by linking it up with the products model, and if it isn't a valid product, the customer should be informed of this. Let's look through a potential addProduct() method for our wish-list controller.

/**
* Add a product to a user's wish list
* @param String $productPath the product path
* @return void
*/

We first check if the product is valid, by creating a new product model object, which informs us if the product is valid.

private function addProduct( $productPath )
{
// check product path is a valid and active product
$pathToRemove = 'wishlist/add/';
$productPath = str_replace( $pathToRemove, '',
$this->registry->getURLPath() );
require_once( FRAMEWORK_PATH . 'models/products/model.php');
$this->product = new Product( $this->registry, $productPath );
if( $this->product->isValid()
{
// check if user is logged in or not
if( $this->registry->getObject('authenticate')->
loggedIn() == true )
{
//Assuming the user is logged in, we can also store their ID,
// so the insert data is slightly different. Here we insert the
// wish into the database.
$wish = array();
$pdata = $this->product->getData();
$wish['product'] = $pdata['ID'];
$wish['quantity'] = 1;
$wish['user'] = $this->registry->getObject('authenticate')->
getUserID();
$this->registry->getObject('db')->
insertRecords('wish_list_products', $wish );
// inform the user
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Product added to your wish list');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'A ' . $pdata['name']
.' has been added to your wish list');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'message.tpl.php',
'footer.tpl.php');
}

The customer isn't logged into the website, so we add the wish to the database, using session and IP address data to tie the wish to the customer.

else
{
// insert the wish
$wish = array();
$wish['sessionID'] = session_id();
$wish['user'] = 0;
$wish['IPAddress'] = $_SERVER['REMOTE_ADDR'];
$pdata = $this->product->getData();
$wish['product'] = $pdata['ID'];
$wish['quantity'] = 1;
$this->registry->getObject('db')->
insertRecords('wish_list_products', $wish );
// inform the user
$this->registry->getObject('template')->getPage()->
addTag('message_heading',
'Product added to your wish list');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'A ' . $pdata['name']
.' has been added to your wish list');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'message.tpl.php',
'footer.tpl.php');
}
}

The product wasn't valid, so we can't insert the wish, so we need to inform the customer of this.

else
{
// we can't insert the wish, so inform the user
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Invalid product');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Unfortunately, the product you
tried to add to your wish list was invalid, and was not
added, please try again');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'message.tpl.php',
'footer.tpl.php');
}
}

Add to wish list

To actually add a product to our wish list, we need a simple link within our products view. This should be /wishlist/add/product-path.

<p>
<a href="wishlist/add/{product_path}"
title="Add {product_name} to your wishlist">
Add to wishlist.
</a>
</p>

We could encase this link around a nice image if we wanted, making it more user friendly. When the user clicks on this link, the product will be added to their wish list and they will be informed of that.

PHP 5 E-commerce Development Create a flexible framework in PHP for a powerful ecommerce solution
Published: January 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Viewing a wish list

As our customers are now able to create their own wish lists, we need to allow them to view and manage their wish lists.

Controller changes

Our controller needs to be modified to list items in a user's wish list; this involves detecting if the user is logged in or not, as this will determine the query it must use to lookup products. In addition to a function to display the list to the customer, we need to detect if the customer is trying to add a product to the list, or if they are trying to view the list, though a switch statement in the constructor.

private function viewList()
{
$s = session_id();
$ip = $_SERVER['REMOTE_ADDR'];
$uid = $this->regisry->getObject('authenticate')->getUserID();
if( $this->registry->getObject('authenticate')->loggedIn() )
{
$when = strtotime("-1 week");
$when = date("Y-m-d h:i:s", $when);
$sql = "SELECT p.price AS product_price,
v.name AS product_name,
c.path AS product_path
FROM content c,
content_versions v,
content_types_products p,
wish_list_items w
WHERE c.ID=w.product
AND p.content_version=v.ID
AND v.ID=c.current_revision
AND c.active=1
AND ( w.user='{$uid}'
OR ( w.sessionID='{$s}' AND w.IPAddress='{$ip}'
AND w.dateadded > '{$when}' ) )";
}
else
{
$sql = "SELECT p.price AS product_price,
v.name AS product_name,
c.path AS product_path
FROM content c,
content_versions v,
content_types_products p,
wish_list_items w
WHERE c.ID=w.product
AND p.content_version=v.ID
AND v.ID=c.current_revision
AND c.active=1
AND w.user=0
AND ( w.sessionID='{$s}' AND w.IPAddress='{$ip}'
AND w.dateadded > '{$when}' )";
}
$cache = $this->registry->getObject('database')->
cacheQuery( $sql );
if( $this->registry->getObject('database')->
numRowsFromCache( $cache ) == 0 )
{
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'No products');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Unfortunately, there are no
products in your wish-list at this time.');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php',
'message.tpl.php',
'footer.tpl.php');
}
else
{
$this->registry->getObject('template')->getPage()->
addTag( 'wishes', array( 'SQL', $cache ) );
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'wishlist.tpl.php',
'footer.tpl.php');
}
}

The highlighted section within that code segment illustrates the difference between a user being logged out or logged in. For a logged-in user, the wish list displays products both associated to their user account and also ones associated with their session and IP address details. For users who are logged out, it ensures the user ID is set to 0; otherwise, they may end up viewing other customers' wish-list products. It limits the timeframe for which session-based products are viewed. This is because session IDs and IP addresses are not a secure method to "authenticate" against. We should investigate some form of garbage collection to ensure out-of-date session-based wish-list products are removed, and also something to detect if the logged-in user has some wish-list products that are not associated with their user ID, and transfer them to their user account to prevent them from losing them.

Wish-list view

Although we would need to extend this later, to include a purchase button, a priority, and quantity, this code would suffice for a basic view to show our customers their desired products for the time being:

<h2>Your wishlist</h2>
<ul>
<!-- START wishes -->
<li><a href="products/view/{product_path}">{product_name}</a></li>
<!-- END wishes -->
</ul>

Purchases

The purchases aspect of this feature is the most complicated, as it needs to facilitate both customers making purchases for themselves, and also others making a gift purchase for someone. We can't actually implement this aspect yet, as we don't have a shopping basket or a complete order process. However, we can discuss what will be involved.

Gift purchases

The complication of gift purchases is that the purchases need to be stored with the delivery address from the customer who created the wish list. The difficulty here is that, at any stage, the delivery address should not be presented to the user making the purchase, as this is private information.

Self purchases

Self purchases should be very straightforward to handle. Essentially, all the customer would be doing is adding their own wish-list product to their own shopping basket, the only difference being that we must maintain a record that this is from their wish list up until the point of their order being finalized; then we must remove the product from their wish-list completely, to prevent them from making a duplicate purchase.

Improving the wish list

There are a number of ways in which we could improve the wish-list feature we have added to our framework, including:

  • Multiple lists per customer, allowing customers to maintain separate lists
  • Garbage collection for session-based wish-list products, ensuring we don't have useless data in our database
  • Transferring of session-based wish-list products to user account-based wish-list products when a user is logged in
  • Model, as we didn't implement a model with this wish list, and we should do so to make it easier to extend
  • Priority isn't considered or displayed to the customer, or anyone who would like to buy the product as a gift for someone
  • Quantities, at present, they are not considered when adding a product to a list; perhaps we should look for existing products in the wish list and increment their quantity
  • Public and private lists, allowing customers to have a private list, and also a public list of items they may wish for others to purchase for them

These improvements are ones you should investigate by adding yourself.

Recommendations

Sometimes, we may find that certain products go hand in hand, or that customers interested in certain products also find another group of products interesting or relevant. If we can suggest some relevant products to our customers, we increase the chances of them making a new purchase or adding something else to their shopping basket. There are two methods of recommendation that we should look into:

  • Displaying related products on a products page
  • E-mailing customers who have made certain purchases to inform them of some other products they may be interested in

Related products

The simplest way to inform customers of related products from within the product view is to maintain a relationship of related products within the database and within the products model, so we could cache the result of a subset of these related products. This way, the controller needs to only detect that there are more than zero related products, insert the relevant template bit into the view, and then associate the cached query as the template tag variable to ensure that they are displayed.

There are a few ways in which we can maintain this relationship of related products:

  • Within the products table we maintain a serialized array of related product IDs
  • We group related products together by themes
  • We relate pairs of related products together

A serialized array isn't the most effective way to store related product data. Relating them by themes would prove problematic with multiple themes, and also when it comes to the administrator relating products to each other, as they would have to select or create a new theme. Relating pairs of products together would require a little trick with the query to get the product name, as the ID of the product being viewed could be one of two fields, as illustrated by the following table structure:

  • ID (Integer, Primary Key, Auto Increment)
  • ProductA (Integer)
  • ProductB (Integer)

The difference between using productA or productB to store a particular product reference would be determined by the administration panel we develop. So if we were to view and edit a product in the administration panel, and we chose to set a related product, the product we were currently viewing would be productA and the related one, productB.

The SQL for this table structure is as follows:

CREATE TABLE `product_relevant_products` (
`ID` int(11) NOT NULL auto_increment,
`productA` int(11) NOT NULL,
`productB` int(11) NOT NULL,
PRIMARY KEY (`ID`),
KEY `productB` (`productB`),
KEY `productA` (`productA`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
ALTER TABLE `product_relevant_products`
ADD CONSTRAINT `product_relevant_products_ibfk_2`
FOREIGN KEY (`productB`) REFERENCES `content` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `product_relevant_products_ibfk_1`
FOREIGN KEY (`productA`) REFERENCES `content` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE;

We can get round the issue of the fact that the current product ID could be found in both the productA and productB columns in the database with an IF statement within our query. The IF statement would work by checking to see if productA is the product the customer is viewing; if it is, then the name of productB is returned; otherwise, the name of productB is returned. This gives us a query such as the following, where CURRENT_PRODUCT_ID is the ID of the product the customer is currently viewing.

SELECT IF(rp.productA<>CURRENT_PRODUCT_ID,v.name,vn.name)
AS product_name,
IF(rp.productA<>CURRENT_PRODUCT_ID,c.path,cn.path)
AS product_path, rp.productA, rp.productB,
c.path AS cpath, cn.path AS cnpath, c.ID AS cid,
cn.ID AS cnid
FROM content c, content cn, product_relevant_products rp,
content_versions v, content_versions vn
WHERE (rp.productA= CURRENT_PRODUCT_ID
OR rp.productB= CURRENT_PRODUCT_ID)
AND c.ID=rp.productA AND cn.ID=rp.productB
AND v.ID=c.current_revision AND vn.ID=cn.current_revision

As we may have a lot of related products, we may wish to put a limit on the number of related products displayed, and randomize the results.

SELECT IF(rp.productA<>CURRENT_PRODUCT_ID,v.name,vn.name)
AS product_name,
IF(rp.productA<>CURRENT_PRODUCT_ID,c.path,cn.path)
AS product_path, rp.productA, rp.productB,
c.path AS cpath, cn.path AS cnpath, c.ID AS cid,
cn.ID AS cnid
FROM content c, content cn, product_relevant_products rp,
content_versions v, content_versions vn
WHERE (rp.productA= CURRENT_PRODUCT_ID
OR rp.productB= CURRENT_PRODUCT_ID) AND c.ID=rp.productA
AND cn.ID=rp.productB AND v.ID=c.current_revision
AND vn.ID=cn.current_revision
ORDER BY RAND() LIMIT 5

Controlling the related products

If we create a new function within our controller to run our random related products query, cache the result, and associate the cached results with a template variable, all we would need to do is call this function from within the product view function, passing the product ID as the parameter.

private function relatedProducts( $currentProduct )
{
$relatedProductsSQL = "SELECT ".
" IF(rp.productA<>{$currentProduct},v.name,vn.name)
AS product_name,
IF(rp.productA<>{$currentProduct},c.path,cn.path)
AS product_path, rp.productA, rp.productB, c.path as cpath,
cn.path AS cnpath, c.ID AS cid, cn.ID AS cnid ".
" FROM ".
" content c, content cn, product_relevant_products rp,
content_versions v, content_versions vn".
" WHERE ".
" (rp.productA={$currentProduct}
OR rp.productB={$currentProduct}) ".
" AND c.ID=rp.productA ".
" AND cn.ID=rp.productB ".
" AND v.ID=c.current_revision ".
" AND vn.ID=cn.current_revision ".
" ORDER BY RAND() ".
" LIMIT 5";
$relatedProductsCache = $this->registry->getObject('db')->
cacheQuery( $relatedProductsSQL );
$this->registry->getObject('template')->getPage()->
addTag('relatedproducts', array( 'SQL', $relatedProductsCache ));
}

The following line calls our new function:

$this->relatedProducts( $productData['ID'] );

Viewing the related products

Related products are now cached and associated with a template variable. We now need to add relevant mark up into our view product template, to display related products.

<h2>Related products</h2>
<!-- START relatedproducts -->
<div class="floatingbox">
<a href="products/view/{product_path}">{product_name}</a>
</div>
<!-- END relatedproducts -->

Of course, we can style this to however we wish: we could display them as small boxes alongside each other, or we could also use JavaScript to toggle between them. However, for now, a simple list or floating <div> should suffice.

Here, beneath the product details, we have a list of related products:

PHP 5 E-commerce Development

E-mail recommendations

There is only so much we can do for this feature at the moment, as it requires our customers to have made some purchases, and at the moment, we don't have the functionality available for customers to make a purchase. However, we can discuss what would be involved in creating this feature:

  1. Search customers with previous purchases that match a subset of the product catalog (for example customers who purchased a red t-shirt and a red baseball cap).
  2. Select products that are related to the subset we defined earlier and we think those customers would be interested in.
  3. Generate an e-mail based on those products, a set template, and other content we may wish to supply.
  4. Send the e-mail to all of the customers found in step 1.

>> Continue Reading: Enhancing the User Experience with PHP 5 Ecommerce - Part 3

[ 1 | 2 | 3 ]

 

If you have read this article you may be interested to view :

 

PHP 5 E-commerce Development Create a flexible framework in PHP for a powerful ecommerce solution
Published: January 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Michael Peacock

Michael Peacock is a web developer from Newcastle, UK and has a degree in Software Engineering from the University of Durham. After meeting his business partner at Durham, he co-founded Peacock Carter, a Newcastle based creative consultancy specializing in web design, web development and corporate identity.

Michael loves working on web related projects. When he is not working on client projects, he is often tinkering with a web app of his own.

He has been involved with a number of books, having written two books himself (and working on his third): Selling online with Drupal e-Commerce Packt, and Building websites with TYPO3 Packt. He has also done technical reviews of two other books: Mobile Web Development Packt, and Drupal Education & E-Learning Packt.

You can follow Michael on Twitter.

Contact Michael Peacock

Books From Packt

Joomla! E-Commerce with VirtueMart
Joomla! E-Commerce with VirtueMart

Drupal E-commerce with Ubercart 2.x
Drupal E-commerce with Ubercart 2.x

AJAX and PHP: Building Modern Web Applications 2nd Edition
AJAX and PHP: Building Modern Web Applications 2nd Edition

Magento 1.3: PHP Developer's Guide
Magento 1.3: PHP Developer's Guide

TYPO3 4.3 Multimedia Cookbook
TYPO3 4.3 Multimedia Cookbook

CodeIgniter 1.7
CodeIgniter 1.7

jQuery 1.3 with PHP
jQuery 1.3 with PHP

MySQL Admin Cookbook
MySQL Admin Cookbook

Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software