Enhancing the User Experience with PHP 5 Ecommerce: Part 3

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 Two of Enhancing the User Experience with PHP 5 Ecommerce here.

Help! It's out of stock!

If we have a product that is out of stock, we need to make it possible for our customers to sign up to be alerted when they are back in stock. If we don't do this, then they will be left with the option of either going elsewhere, or regularly returning to our store to check on the stock levels for that particular product. To try and discourage these customers from going elsewhere a "tell me when it is back in stock" option saves them the need to regularly check back, which would be off-putting. Of course, it is still likely that the customer may go elsewhere; however, if our store is niche, and the products are not available elsewhere, then if we give the customer this option they will feel more valued.

There are a few stages involved in extending our framework to support this:

  1. Firstly, we need to take into account stock levels.
  2. If a product has no stock, we need to insert a new template bit with an "alert me when it is back in stock" form.
  3. We need a template to be inserted when this is the case.
  4. We then need functionality to capture and store the customer's e-mail address, and possibly their name, so that they can be informed when it is back in stock.
  5. Next, we need to be able to inform all of the customers who expressed an interest in a particular product when it is back in stock.
  6. Once our customers have been informed of the new stock level of the product, we need to remove their details from the database to prevent them from being informed at a later stage that there are more products in stock.
  7. Finally, we will also require an e-mail template, which will be used when sending the e-mail alerts to our customers.

Detecting stock levels

With customizable products, stock levels won't be completely accurate. Some products may not require stock levels, such as gift vouchers and other non-tangible products. To account for this, we could either add a new field to our database to indicate to the framework that a products stock level isn't required for that particular product, or we could use an extreme or impossible value for the stock level, for example -1 to indicate this.

Changing our controller

We already have our model set to pull the product stock level from the database; we just need our controller to take this value and use different template bits where appropriate. We could also alter our model to detect stock levels, and if stock is required for a product.

if( $productData['stock'] == 0 )
{
$this->registry->getObject('template')->
addTemplateBit( 'stock', 'outofstock.tpl.php' );
}
elseif( $productData['stock'] > 0 )
{
$this->registry->getObject('template')->
addTemplateBit( 'stock', 'instock.tpl.php' );
}
else
{
$this->registry->getObject('template')->getPage()->
addTag( 'stock', '' );
}

This simple code addition imports a template file into our view, depending on the stock level.

Out of stock: a new template bit

When the product is out of stock, we need a template to contain a form for the user to complete, so that they can register their interest in that product.

<h2>Out of stock!</h2>
<p>
We are <strong>really</strong> sorry, but this product is currently
out of stock. If you let us know your name and email address, we
will let you know when it is back in stock.
</p>
<form action="products/stockalert/{product_path}" method="post">
<label for="stock_name">Your name</label>
<input type="text" id="stock_name" name="stock_name" />
<label for="stock_email">Your email address</label>
<input type="text" id="stock_email" name="stock_email" />
<input type="submit" id="stock_submit" name="stock_submit"
value="Let me know, when it is back in stock!" />
</form>

Here we have the form showing our product view, allowing the customer to enter their name and e-mail address:

PHP 5 E-commerce Development

Tell me when it is back in stock please!

Once a customer has entered their name, e-mail address, and clicked on the submit button, we need to store these details and associate them with the product. This is going to involve a new database table to maintain the relationship between products and customers who wish to be notified when they are back in stock.

Stock alerts database table

We need to store the following information in the database to manage a list of customers interested in being alerted when products are back in stock:

  • Customer name
  • Customer e-mail address
  • Product

In terms of a database, the following table structure would represent this:

Field

Type

Description

ID

Integer (Primary Key, Auto Increment)

The ID for the stock alert request

Customer

Varchar

The customer's name

Email

Varchar

The customer's e-mail address

ProductID

Integer

The ID of the product the customer wishes to be informed about when it is back in stock

The following SQL represents this table:

CREATE TABLE `product_stock_notification_requests` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`customer` VARCHAR( 100 ) NOT NULL ,
`email` VARCHAR( 255 ) NOT NULL ,
`product` INT NOT NULL ,
`processed` BOOL NOT NULL ,
INDEX ( `product` , `processed` )
) ENGINE = INNODB COMMENT = 'Customer notification requests for
new stock levels'
ALTER TABLE `product_stock_notification_requests`
 
ADD FOREIGN KEY ( `product` ) REFERENCES `book4`.`content` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE 

More controller changes

Some modifications are needed to our product's controller to process the customer's form submission and save it in the stock alerts database table.

In addition to the following code, we must also change our switch statement to detect that the customer is visiting the stockalert section, and that the relevant function should be called.

private function informCustomerWhenBackInStock()
{
$pathToRemove = 'products/stockalert/';
$productPath = str_replace( $pathToRemove, '',
$this->registry->getURLPath() );
require_once( FRAMEWORK_PATH . 'models/products/model.php');
$this->model = new Product( $this->registry, $productPath );

Once we have included the model and checked that the product is valid, all we need to do is build our insert array, containing the customer's details and the product ID, and insert it into the notifications table.

if( $this->model->isValid() )
{
$pdata = $this->product->getData();
$alert = array();
$alert['product'] = $pdata['ID'];
$alert['customer'] = $this->registry->getObject('db')->
sanitizeData( $_POST['stock_name'] );
$alert['email'] = $this->registry->getObject('db')->
sanitizeData( $_POST['stock_email'] );
$alert['processed'] = 0;
$this->registry->getObject('db')->
insertRecords('product_stock_notification_requests', $alert );
// We then inform the customer that we have saved their request.
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Stock alert saved');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Thank you for your interest in
this product, we will email you when it is back in stock.');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'message.tpl.php',
'footer.tpl.php');
}

If the product wasn't valid, we tell them that, so they know the notification request was not saved.

else
{
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Invalid product');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Unfortunately, we could not find
the product you requested.');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'message.tpl.php',
'footer.tpl.php');
}
}

This code is very basic, and does not validate e-mail address formats, something which must be done before we try to send any e-mails out.

It is back!

Once the product is back in stock, we need to then alert those customers that the product which they were interested in is back in stock, and that they can proceed to make their purchase. This isn't something we can implement now, as we don't have an administrative interface in place yet. However, we can discuss what is involved in doing this:

  1. The administrator alters the stock level.
  2. Customers interested in that product are looked up.
  3. E-mails for each of those customers are generated with relevant details, such as their name and the name of the product being automatically inserted.
  4. E-mails are sent to the customers.

The database contains a processed field, so once an e-mail is sent, we can set the processed value to 1, and then once we have alerted all of our customers, we can delete those records. This covers us in the unlikely event that all the new stock sells out while we are e-mailing customers, and a new customer completes the notification form.

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:

Giving power to customers

There are two very powerful social-oriented features, which we can implement into our framework.

Product ratings

Product ratings are quite simple to add to our framework: we simply need to record a series of ratings between one and five, and display the average of these on the product view. We can enhance the view by making the rating system a clickable image, where the customer can click on the number of stars they wish to give the product and their rating is saved.

There are a few minor considerations that need to be taken into account. However, if the logged-in customer has already rated a product, we should then update their rating. If the customer is not logged in, we must record some information about them such as their IP address and the date and time of the rating. This way we prevent duplicate ratings from the same customer.

From a database perspective, we would need to capture the following information:

  • ID (Integer, Primary Key, Auto Increment)
  • ContentID (Integer)
  • Rating (Integer)
  • User ID (Integer)
  • Timestamp (datetime)
  • Session ID (Varchar)
  • IP Address (Varchar)

The following SQL represents that table in our database:

CREATE TABLE `content_ratings` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`contentID` INT NOT NULL ,
`rating` INT NOT NULL ,
`userID` INT NOT NULL ,
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ,
`sessionID` VARCHAR( 255 ) NOT NULL ,
`IPAddress` VARCHAR( 50 ) NOT NULL
) ENGINE = INNODB;

Saving a rating

When a rating is made, we need to check to see if the current user has already rated that content element; if they have, then we must update that rating. For users who are not logged in, we should use their session name and IP address to lookup a potential rating from the past 30 days; if a rating is found, that should be updated.

Saving the rating should be made simple by processing values from the URL. This way, we can have a graphic of five stars or hearts, which when clicked, contain a link to the corresponding number of stars, to save the suitable rating.

As ratings and reviews (comments) will not be content specific, we will need a separate controller for these. To return the customer to the page they were on previously, we could investigate looking up the referring page, and then redirecting the user to that page once their rating has been saved.

The constructor of the controller needs to parse the URL bits to extract the content ID and the rating value, ensure that the rating is within allowed limits, and then call the saveRating function, which either inserts or updates a rating as appropriate.

To check if the user has rated the product already, we query the database; depending on if the user is logged in, this query is different. For users who are not logged in, we assume users with the same IP address and session data within the past 30 days were the current users.

private function saveRating( $contentID, $rating )
{
if( $this->regisry->getObject('authenticate')->isLoggedIn() )
{
$u = $this->registry->getObject('authenticate')->getUserID();
$sql = "SELECT ID FROM content_ratings
WHERE contentID={$contentID} AND userID={$u}";
}
else
{
$when = strtotime("-30 days");
$when = date( 'Y-m-d h:i:s', $when );
$s = session_id();
$ip = $_SERVER['REMOTE_ADDR'];
$sql = "SELECT ID FROM content_ratings
WHERE content_id={$contentID} AND userID=0
AND sessionID='{$s}' AND IPAddress='{$ip}'
AND timestamp > '{$when}'";
}
$this->registry->getObject('db')->executeQuery( $sql );

If the product has already been rated, we update the rating.

if( $this->regisry->getObject('db')->numRows() == 1 )
{
// update
$data = $this->registry->getObject('db')->getRows();
$update = array();
$update['rating'] = $rating;
$update['timestamp'] = date('Y-m-d h:i:s');
$this->registry->getObject('db')->
updateRecords( 'content_ratings', $update, 'ID=' . $data['ID']);
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Rating changed');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Your rating has been changed');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'message.tpl.php',
'footer.tpl.php');
}

Otherwise, we insert a new record in the ratings table.

else
{
// insert
$rating = array();
$rating['rating'] = $rating;
$rating['contentID'] = $contentID;
$rating['sessionID'] = session_id();
$rating['userID'] = ( $this->registry->
getObject('authenticate')->isLoggedIn() == true ) ?
$this->registry->getObject('authenticate')->getUserID() : 0;
$rating['IPAddress'] = $_SERVER['REMOTE_ADDR'];
$this->registry->getObject('db')->
insertRecords( 'content_ratings', $rating );
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Rating saved');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Your rating has been saved');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php', 'message.tpl.php',
'footer.tpl.php');
}
}

Viewing ratings

To display the ratings, we need to alter the content or products query to also perform a subquery, which averages out the rating.

( SELECT sum(rating)/count(*)
FROM content_ratings
WHERE contentID=c.ID ) AS rating

Improving the user interface for ratings

Displaying ratings as nice graphics, which display both the current rating and allow the user to select their own rating from them, are chapters in themselves. There are a number of Internet tutorials that document this process; you may find them useful:

Product reviews

Product reviews can work as a simple comment form for the products, taking the name and e-mail address of the customer, as well as their review. Product reviews can be represented in the same way that we would represent comments on pages or blog entries, and because we have set up our database to store pages, products, and other types of content with a reference to a single database table, we can reference our reviews or comments to any content type. From a database perspective, a table with the following fields would suffice:

Field

Type

Description

ID

Integer (Auto Increment, Primary Key)

Review ID

Content

Integer

The content entity the user is reviewing

Customer name

Varchar

The customer

Customer email

Varchar

The customer's e-mail address

Review

Longtext

The customer's review

IPAddress

Varchar

The user's IP address

Date Added

Timestamp

The date they added the review

Approved

Boolean

If the review is approved and shown on the site

The following SQL code represents that table in our database:

CREATE TABLE `content_comments` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`content` INT NOT NULL,
`authorName` VARCHAR( 50 ) NOT NULL,
`authorEmail` VARCHAR( 50 ) NOT NULL,
`comment` LONGTEXT NOT NULL,
`IPAddress` VARCHAR( 40 ) NOT NULL,
`dateadded` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`approved` BOOL NOT NULL,
INDEX ( `content` )
) ENGINE = INNODB COMMENT = 'Content comments - also for product
reviews';
ALTER TABLE `content_comments`
 
ADD FOREIGN KEY ( `content` ) REFERENCES `book4`.`content` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE 

Processing reviews/comments

With a database in place for our product reviews (or page comments), we need to provide a form for our customers to enter their review, and then process this submission and save it in the database.

Submission form

The submission form can be quite simple; we will only be collecting the customer's name, e-mail address, and their review:

<h2>Review this product</h2>
<form action="contentcomment/{ID}" method="post">
<label for="comment_name">Your name</label>
<input type="text" id="comment_name" name="comment_name" />
<label for="comment_email">Your email address</label>
<input type="text" id="comment_email" name="comment_email" />
<label for="comment">Your review</label>
<textarea name="comment" id="comment"></textarea>
<input type="submit" id="savecomment" name="savecomment"
value="Add review" />
</form>

Adding the review

When it comes to saving the review, it is a simple case of sanitizing our data, and inserting it into the database.

I've not added checks to ensure the page or product exists, and the error checking for name and e-mail addresses is basic. Ideally, we would want to return the customer to the contact form, with the invalid fields highlighted.

private function saveComment( $contentID )
{
//We build our insert array of data for the review record.
$insert = array();
$insert['content'] = $contentID;
$insert['authorName'] = strip_tags($this->registry->
getObject('db')->sanitizeData( $_POST['comment_name'] ) );
$insert['authorEmail'] = $this->registry->getObject('db')->
sanitizeData( $_POST['comment_email'] );
$insert['comment'] = strip_tags( $this->registry->getObject('db')->
sanitizeData( $_POST['comment'] ) );
$insert['IPAddress'] = $_SERVER['REMOTE_ADDR'];
$valid = true;
if( $_POST['comment_name'] == '' ||
$_POST['comment_email'] == '' || $_POST['comment'] == '' )
{
$valid = false;
}

If enough information was provided, we insert the review into the database.

if( $valid == true )
{
$this->registry->getObject('db')->
insertRecords( 'content_comments', $insert );
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Review added');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Your review has been added');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php',
'message.tpl.php',
'footer.tpl.php');
}

Otherwise, we display an error to the customer.

else
{
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Error');
$this->registry->getObject('template')->getPage()->
addTag('message_heading', 'Either your name, email address or
review was empty, please try again');
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php',
'message.tpl.php',
'footer.tpl.php');
}
}

Displaying reviews/comments

What do we need to do to display reviews and comments?

  1. Check to see if there are any comments.
  2. Display either the comments or a "no comments" template.
  3. Check to see if new comments are allowed.
  4. Display either the comments form or a "no comments allowed" template.

Displaying and allowing comments is a very generic feature, which we could either add into our product's controller or in a controller that all of our controllers inherit from, ensuring all content types have support for it. I'll leave the choice down to you; however, it would be useful if you chose to use inheritance in order to override it within the products, so the templates replace the word comments with reviews, and perhaps have a larger comments box.

Combining the two?

E-commerce sites such as Amazon combine these two features: allowing customers to select a rating when they leave a review and have this rating display alongside their review. What would be involved in combining the two features?

  1. Associate ratings with a review or account for rating reviews when working out the average rating. We could either extend the ratings table to include a potential reference to a review, which the review can pull in, or we can store the ratings along with the reviews and have the product's average rating calculated by combining both standard ratings, and ratings that were left with a review.
  2. Save a rating at the same time as processing a review.
  3. If a review has a rating associated with it, then it should be displayed.

Try it yourself
Once you have decided which of the two methods you think is best for your particular framework, why not try combining the two features yourself, should this be a useful feature for your framework?

Any other experience improvements to consider?

A fantastic user experience for our customers by no means ends here; there are many other ways in which we could improve our customers' experience. Let's discuss a few of them:

  • Suggest a product: We could allow customers to suggest new products for our store.
  • Suggest a related product: We could allow customers to suggest a product that is related to another product.
  • Report inaccurate information: We could allow customers to report inaccurate product information.
  • Report an inappropriate comment: We could allow customers to bring to our attention inappropriate comments left on product pages.
  • Pre-orders: If we looked into registering customers' interest when a product was out of stock, we could extend and improve this to indicate that a product is not in stock because it is currently only available for pre-order.
  • "Feedback about this page": A simple link taking customers to a contact form to leave feedback about a specific page could be useful, as it could allow them to inform us that a page is too thin on details, the information is out of date, or that a link or image is broken and needs to be fixed.

Summary

In this article we have discussed the advantages of improving the user experience for our customers, and we have worked to improve their experience by:

  • Introducing product search and filtering options
  • Recommending relevant products to our customers
  • Giving our customers wish lists
  • Informing our customers when products they are interested in are back in stock
  • Allowing customers to rate and review products

We have also discussed how we could extend and enhance these user experience improvements, which we have implemented, including:

  • How to improve our search feature
  • Maintaining multiple wish lists
  • Maintaining public and private wish lists
  • Combining the ratings and reviews feature

Now that the user experience is improved, and we have some ideas on how to further improve it, we can move onto the shopping basket, bringing us one step closer to being able to trade online using our e-commerce framework.

[ 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