Enhancing the User Experience with PHP 5 Ecommerce: Part 1

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

PHP 5 E-commerce Development — Save 80%

Create a flexible framework in PHP for a powerful ecommerce solution

₨739.00    ₨147.80
by Michael Peacock | January 2010 | e-Commerce PHP

In this three-part article by Michael Peacock, author of PHP 5 e-commerce Development, you will learn how to enhance the user experience by:

  • Allowing customers to search our product catalog effectively
  • Enhancing this search by allowing our customers to filter products Providing wish lists for our customers
  • Generating recommendations for customers based on previous purchases
  • Informing customers when their desired products are back in stock
  • Enabling social aspects such as product ratings and reviews from customers

Juniper Theatricals

Juniper Theatricals want to have a lot of products on their online store, and as a result they fear that some products may get lost within the website, or not be as obvious to their customers. To help prevent this problem, we will integrate product searching to make products easy to find, and we will add filters to product lists allowing customers to see products that match what they are looking for (for example, ones within their price range).

As some products could still be lost, they want to be able to recommend related products to customers when they view particular products. If a customer wants a product, and it happens to be out of stock, then they want to prevent the customer from purchasing it elsewhere; so we will look at stock notifications too.

The importance of user experience

Our customers' experience on the stores powered by our framework is veryimportant. A good user experience will leave them feeling wanted and valued, whereas a poor user experience will leave them feeling unwanted, unvalued, and may leave a bad taste in their mouths.

Search

The ability for customers to be able to search, find, and filter products is vital, as if they cannot find what they are looking for they will be frustrated by our site and go somewhere where they can find what they are looking for much more easily.

There are two methods that can make it much easier for customers to find what they are looking for:

  • Keyword search: This method allows customers to search the product catalog based on a series of keywords.
  • Filtering: This method allows customers to filter down lists of products based on attributes, refining larger lists of products into ones that better match their requirements.

Finding products

The simplest way for us to implement a search feature is to search the product name and product description fields. To make the results more relevant, we can place different priorities on where matches were found; for instance, if a word or phrase is found in both the name and description then that would be of the highest importance; next would be products with the word or phrase in the name; and finally, we would have products that just have the word or phrase contained within the product description itself.

So, what is involved in adding search features to our framework? We need the following:

  • Search box: We need a search box for our customers to type in words or phrases.
  • Search feature in the controller: We need to add some code to search the products database for matching products.
  • Search results: Finally, we need to display the matching products to the customer.

Search box

We need a search box where our customers can type in words or phrases to search our stores. This should be a simple POST form pointing to the path products/search with a search field of product_search. The best place for this would be in our website's header, so customers can perform their search from anywhere on the site or store.

<div id="search">
<form action="products/search" method="post">
<label for="product_search">Search for a product</label>
<input type="text" id="product_search" name="product_search" />
<input type="submit" id="search" name="search" value="Search" />
</form>
</div>

We now have a search box on the store:

PHP 5 E-commerce Development

Controlling searches with the products controller

A simple modification to our products controller will allow customers to search products. We need to make a small change to the constructor, to ensure that it knows when to deal with search requests. Then we need to create a search function to search products, store the results, and display them in a view.

Constructor changes

A simple switch statement can be used to detect if we are viewing a product, performing a search, or viewing all of the products in the database as a list.

$urlBits = $this->registry->getURLBits();
if( !isset( $urlBits[1] ) )
{
$this->listProducts();
}
else
{
switch( $urlBits[1] )
{
case 'view':
$this->viewProduct();
break;
case 'search':
$this->searchProducts();
break;
default:
$this->listProducts();
break;
}
}

This works by breaking down the URL and, depending on certain aspects of the URL, different methods are called from within the controller.

Search function

We now need a function to actually search our products database, such as the following:

private function searchProducts()
{
// check to see if the user has actually submitted the search form
if( isset( $_POST['product_search'] ) &&
$_POST['product_search'] != '' )
{

Assuming the customer has actually entered something to search, we need to clean the search phrase, so it is suitable to run in our database query, and then we perform the query. The phrase is checked against the name and description of the product, with the name taking priority within the results. The highlighted code illustrates the query with prioritization.

// clean up the search phrase
$searchPhrase = $this->registry->getObject('db')->
sanitizeData( $_POST['product_search'] );
$this->registry->getObject('template')->getPage()->
addTag( 'query', $_POST['product_search'] );
// perform the search, and cache the results, ready for the
// results template
$sql = "SELECT v.name, c.path,
IF(v.name LIKE '%{$searchPhrase}%', 0, 1) AS priority,
IF(v.content LIKE '%{$searchPhrase}%', 0, 1)
AS priorityb
FROM content c, content_versions v, content_types t
WHERE v.ID=c.current_revision AND c.type=t.ID
AND t.reference='product' AND c.active=1
AND ( v.name LIKE '%{$searchPhrase}%' OR v.content
LIKE '%{$searchPhrase}%' )
ORDER BY priority, priorityb ";
$cache = $this->registry->getObject('db')->cacheQuery( $sql );
if( $this->registry->getObject('db')->
numRowsFromCache( $cache ) == 0 )
{
// no results from the cached query, display the no results
// template
}

If there are some products matching the search, then we display the results to the customer.

else
{
// some results were found, display them on the results page
// IMPROVEMENT: paginated results
$this->registry->getObject('template')->getPage()->
addTag( 'results', array( 'SQL', $cache ) );
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php',
'products-searchresults.tpl.php', 'footer.tpl.php');
}
}
else
{
// search form not submitted, so just display the search box page
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php',
'products-searchform.tpl.php', 'footer.tpl.php');
}
}

As the results from the query are stored in a cache, we can simply assign this cache to a template tag variable, and the results will be displayed. Of course, as we need to account for the fact that there may be no results, we must check to ensure there are some results, and if there are none, we must display the relevant template.

Search results

Finally, we need a results page to display these results on.

<h2>Products found...</h2>
<p>The following products were found, matching your search for
{query}.</p>
<ul>
<!-- START results -->
<li><a href="products/view/{path}">{ name}</a></li>
<!-- END results -->
</ul>

Our search results page looks like this:

PHP 5 E-commerce Development

Improving searches

We could improve this search function by making it applicable for all types of content managed by the framework. Obviously if we were going to do this, it would need to be taken out of the products controller, perhaps either as a controller itself, or as a core registry function, or as part of the main content/pages controller.

The results could either be entirely in a main list, with a note of their type of content, or tabbed, with each type of content being displayed in a different tab. The following diagrams represent these potential Search Results pages.

PHP 5 E-commerce Development

 

And, of course, the tab-separated search results.

PHP 5 E-commerce Development

PHP 5 E-commerce Development Create a flexible framework in PHP for a powerful ecommerce solution
Published: January 2010
eBook Price: ₨739.00
Book Price: ₨1,232.00
See more
Select your format and quantity:

Filtering products

Another useful way to allow customers to better find the products they are looking for is with filtering. Customers can filter down lists of products based on attributes, such as price ranges, manufacturer, weight, brands, and so on.

Price range filtering should be simple enough. However, with attributes such as manufacturer or brands, we would need to extend the database and models representation of a product to maintain this additional information, and allow us to filter down based on these attributes.

There are a few different ways in which we can store filtered results:

  • In the user's session: This will be lost when the user closes their browser.
  • In a cookie: This information will stay when the user closes their browser.
  • In the URL: This would allow the customer to filter results and send the link of those results to a friend.
  • In POST data: The information will only be stored for the one instance the filter is used.

Let's try using the URL to store filter data. If we format filter data within the URL as filter/attribute-type/attribute-value-ID, then we can simply iterate through the bits of the URL, find bits containing filter, and then take the next two parts of the URL to help build the filter. This way we can filter down products based on a number of attributes, for example filter/price/5/filter/weight/6. Of course, there is a limit to this, and that is the maximum length of a URL.

Product attributes

Some attributes are already stored within the product, such as the weight and the price. However, we still need to store some ranges of these for our customers to filter by. As we discussed earlier, that we will store the attribute type as well as the attribute value within the URL, we can take the attribute type and either filter based on attribute values associated in the database (for example, products associated with brands for filtering by brand) or if the type is price or weight, we can detect that these should be filtered based on values stored in the products table.

Database changes

We are going to need to create three new database tables to effectively support product filtering as we have discussed. We will need:

  • An attribute types table to manage types of attributes; for example, price, weight, brand, manufacturer, color, and so on
  • An attribute values table to manage values and ranges of attributes; for example, < $5, $5 - $10, < 5 KG, Nike, Adidas, gold, red, and so on
  • An associations table to associate products with attribute values

Filter attribute types

The attribute types table needs to be able to act as a grouping reference for attribute values, and also detect if an attribute value should be referenced against the products table, or the attribute associations table. Prices and weights would be referenced against the products table, where as brands, colors, and manufacturers would be referenced against the associations table.

Field

Type

Description

ID

Integer (Primary Key, Auto Increment)

A database reference for the attribute type.

Reference

Varchar

 

Name

Varchar

The name of the type of attribute, for example price.

ProductContainedAttribute

Boolean

Specifies if the attribute is part of a field defined in the products table, such as price or weight, or not.

The following SQL represents this table:

CREATE TABLE `product_filter_attribute_types` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`reference` VARCHAR( 25 ) NOT NULL,
`name` VARCHAR( 50 ) NOT NULL ,
`ProductContainedAttribute` BOOL NOT NULL
) ENGINE = INNODB COMMENT = 'Product Attributes for Filtering
Product Lists';

Filter attribute values

The attribute values table needs to store the name of the attribute and its relevant attribute type. Required fields are:

Field

Type

Description

ID

Integer (Primary Key, Auto Increment)

 

Name

Varchar

The name of the attribute, for example <$10

Attribute type

Integer

A reference to the type of attribute (for example size, price)

Lower value

Integer

Used for attributes that are referenced within the products table (see below)

Upper value

Integer

Used for attributes that are referenced within the products table (see below)

Order

Integer

The order of the attribute in a list

The upper and lower values are used when referencing against the products table, so we can get our framework to quickly construct queries using the lower and upper values as ranges for the WHERE clause of the query. The following SQL represents this table:

CREATE TABLE `product_filter_attribute_values` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR( 100 ) NOT NULL,
`attributeType` INT NOT NULL,
`order` INT NOT NULL,
`lowerValue` INT NOT NULL,
`upperValue` INT NOT NULL,
INDEX ( `attributeType` )
) ENGINE = INNODB COMMENT = 'Attribute values for filtering products'
ALTER TABLE `product_filter_attribute_values`
ADD FOREIGN KEY ( `attributeType` )
REFERENCES `book4`.`product_filter_attribute_types` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE;

Attribute associations

The final table we need is the one to associate various attributes with various products, the data we need to store is:

  • The product ID
  • The attribute ID

The following SQL represents the previous table:

CREATE TABLE `product_filter_attribute_associations` (
`attribute` INT NOT NULL,
`product` INT NOT NULL,
PRIMARY KEY ( `attribute` , `product` )
) ENGINE = INNODB COMMENT = 'Product attribute associations
for filtering product lists';
ALTER TABLE `product_filter_attribute_associations`
ADD FOREIGN KEY ( `attribute` )
REFERENCES `book4`
.`product_attribute_values` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `product_filter_attribute_associations`
ADD FOREIGN KEY ( `product` )
REFERENCES `book4`.`content` (`ID`)
ON DELETE CASCADE ON UPDATE CASCADE;

Filter options

To display these attributes to our customers, and allow them to click on them to perform a filter, we need to build a list of attributes, build suitable URLs based on the attributes, and display them within the product list view.

Displaying these attributes will involve some nested-looped template tags. The first loop will be to generate headings and empty lists (with suitable template tags within) for the attribute types. Then we need to insert the loops of values into these.

The simplest way to do this would be to do a query of all of the attribute types, cache it, and assign it to a template variable, and then do this for each set of values. Of course, that isn't a very good way, as we end up doing one query per set of attribute types, which isn't very efficient. We need to query the attribute types, query all attribute values and then process them into groups, and associate these groups with relevant template tags.

Let's look at this as a step-by-step process:

  1. We query the database for attribute types.
  2. We cache the results of this query.
  3. The cache is associated with a template tag. (This allows the template engine to generate a list of attribute types, and for each attribute type, it can build an empty list, surrounded by template tags, which will eventually contain the attribute values.)
  4. We query the database for all attribute types, ordering by their own order. (Although the order is their order within their group, this does not matter, as we filter them out.)
  5. We iterate through the results, putting each value into an array for its corresponding attribute type.
  6. For each attribute type, we cache the array, and assign it to a template tag, allowing each group of values to populate the appropriate list for the attribute type.

Our modified controller now looks like this, with our aforementioned six steps commented in for reference:

private function generateFilterOptions()
{
// 1. Query the database for attribute types
$attrTypesSQL = "SELECT reference, name
FROM product_filter_attribute_types";
$this->registry->getObject('db')->executeQuery( $attrTypesSQL );
if( $this->registry->getObject('db')->numRows() != 0 )
{
$attributeValues = array();
$attributeTypes = array();
while( $attributeTypeData = $this->registry->
getObject('db')->getRows() )
{
$attributeValues[ $attributeTypeData['reference'] ] = array();
$attributeTypes[] = array(
'filter_attr_reference' => $attributeTypeData['reference'],
'filter_attr_name' => $attributeTypeData['name'] );
}
// 2. cache the results of this query
$attributeTypesCache = $this->registry->getObject('db')->
cacheData( $attributeTypes );
// 3. The cache is associated with a template tag
$this->registry->getObject('template')->getPage()->
addTag( 'filter_attribute_types',
array( 'DATA', $attributeTypesCache ) );
// 4. We query the database for all attribute types,
// ordering by their own order
$attrValuesSQL = "SELECT v.name AS attrName,
t.reference AS attrType,
v.ID AS attrID
FROM product_filter_attribute_values v,
product_filter_attribute_types t
WHERE t.ID=v.attributeType
ORDER BY v.order ASC";
$this->registry->getObject('db')->executeQuery( $attrValuesSQL );
if( $this->registry->getObject('db')->numRows() != 0 )
{
// 5. We iterate through the results, putting each value into
// an array for its corresponding attribute type.
while( $attributeValueData = $this->registry->getObject('db')->
getRows() )
{
$data = array();
$data['attribute_value'] = $attributeValueData['attrName'];
$data['attribute_URL_extra'] = 'filter/'
. $attributeValueData['attrType'] . '/'
. $attributeValueData['attrID'];
$attributeValues[ $attributeValueData['attrType'] ][] =
$data;
}
}
// 6. For each attribute type, we cache the array, and assign it
// to a template tag, allowing each group of values to
// populate the appropriate list for the attribute type.
foreach( $attributeValues as $type => $data )
{
//echo '<pre>' . print_r( $attributeValues, true ) . '</pre>';
$cache = $this->registry->getObject('db')->cacheData( $data );
$this->registry->getObject('template')->getPage()->
addPPTag( 'attribute_values_'
. $type, array( 'DATA', $cache ) );
}
}
}
PHP 5 E-commerce Development Create a flexible framework in PHP for a powerful ecommerce solution
Published: January 2010
eBook Price: ₨739.00
Book Price: ₨1,232.00
See more
Select your format and quantity:

Processing filter requests

With the relevant database structure in place, and functionality available for our customers to select attributes for which they wish to filter their product viewings, we need a method to process the request and actually filter the products listing. This involves iterating through the bits within the URL, and for every instance of filter found, storing the following two values. Once all bits of the URL have been processed, the saved bits should be processed to build a suitable query to filter the products.

We will need some variables within our controller to store some of the data we will be processing. These would include:

  • An array containing the filter attribute types, so we can pass the components of the URL to it in order to determine if the attribute value is from part of the products table itself, or if it is from an attribute association
  • An array containing the filter attribute values, so when we find an attribute type that refers to the products table, we can get the upper- and lower-bound values for this
  • An array of pieces of SQL to search for attribute associations
  • An array of pieces of SQL to search for attribute values within the products table
  • A counter for the number of filters by association, as we will group this part of the search into a subquery, returning the results of a count, and we will know if we have a match if the count matches the number of conditions to the subquery.

These variables are displayed as follows:

// Filter count: to count how many attributes by association
// must match
private $filterCount=0;
// SQL statement parts where products are associated with
// attributes
private $filterAssociations = array();
// SQL statement parts where products are filtered by their own
// direct properties i.e. price, weight.
private $filterDirect = array();
// Array of filter attribute types
private $filterTypes = array();
// Array of filter attribute values
private $filterValues = array();
// our SQL statement for filtered products
private $filterSQL = '';

We now need a function to search through the URL and another function to add query pieces to our various arrays when it is passed the filter type and filter value once an occurrence of the word filter is found in the URL.

So, firstly we'll see a function to go through the URL.

/**
* Generate an SQL statement for filtering products, based on URL
* paramaters
* @param array $bits the bits contained within the URL
* @return void
*/

We first get all of the attribute types available, and then we get all of the attribute values.

private function filterProducts( $bits )
{
// get our attribute types
$attributeTypesSQL = "SELECT ID, reference, name,
ProductContainedAttribute
FROM product_filter_attribute_types ";
$this->registry->getObject('db')->executeQuery( $attributeTypesSQL );
while( $type = $this->registry->getObject('db')->getRows() )
{
$this->filterTypes[ $type['reference'] ] =
array( 'ID' => $type['ID'],
'reference'=>$type['reference'],
'ProductContainedAttribute'=>
$type['ProductContainedAttribute'] );
}
// get our attribute values
$attributeValuesSQL = "SELECT ID, name, lowerValue, upperValue
FROM product_filter_attribute_values";
$this->registry->getObject('db')->
executeQuery( $attributeValuesSQL );
while( $value = $this->registry->getObject('db')->getRows() )
{
$this->filterValues[ $value['ID'] ] =
array( 'ID' => $value['ID'],
'name' => $value['name'],
'lowerValue' => $value['lowerValue'],
'upperValue' => $value['upperValue'] );
}

For each part of the URL, we go through and find anything that relates to the filter functionality, which is of the format filter/attribute-type/attribute-value.

// process the URL
foreach( $bits as $position => $bit )
{
// if we find filter in the URL
if( $bit == 'filter' )
{
// send the nex two bits to the addToFilter method
$this->addToFilter( $bits[ $position+1], $bits[ $position+2] );
}
}

We assume there are no filter requests being made, and set the basic filter query. Then we check if we have any filters that are not based on the product table values; if there are, we set the somethingToFilter variable, then we do the same for the filters based on the product table values. Each filter found adds additional restrictions to the basic filter SQL query.

// assume no filter requests
$somethingToFilter = false;
// basic filter query
$sql = "SELECT p.price AS product_price, v.name AS product_name,
c.path AS product_path
FROM content c, content_types t, content_versions v,
content_types_products p
WHERE v.ID=c.current_revision AND c.active=1
AND p.content_version=v.ID AND t.reference='product'
AND c.type=t.ID ";
if( !empty( $this->filterAssociations ) )
{
// we have some filter requests
$somethingToFilter = true;
// build the query
$sqla = " AND ( SELECT COUNT( * )
FROM product_filter_attribute_associations pfaa
WHERE ( ";
$assocs = implode( " AND ", $this->filterAssociations );
$sqla .= $assocs;
$sqla .= " )AND pfaa.product = c.ID )={$this->filterCount}";
$sql .= $sqla;
}
if( !empty( $this->filterDirect ) )
{
// we have some filter requests
$somethingToFilter = true;
// build the query
$sql .= " AND ";
$assocs = implode( " AND ", $this->filterDirect );
$sql .= $assocs;
}
if( $somethingToFilter )
{
// since we have some filter requests, store the query.
$this->filterSQL = $sql;
}
}

And secondly, we look at a function to build our SQL statement.

/**
* Add SQL chunks to our filter arrays, to help build our query,
* based on actual filter requests in the URL
* @param String $filterType the reference of the attribute type we
* are filtering by
* @param int $filterValue the ID of the attribute value
* @return void
*/
private function addToFilter( $filterType, $filterValue )
{
if( $this->filterTypes[ $filterType ]
['ProductContainedAttribute'] == 1 )
{
$lower = $this->filterValues[ $filterValue ]['lowerValue'];
$upper = $this->filterValues[ $filterValue ]['upperValue'];
$sql = " p.{$filterType} >= {$lower}
AND p.{$filterType} < {$upper}";
$this->filterDirect[] = $sql;
}
else
{
$this->filterCount++;
$sql = " pfaa.attribute={$filterValue} ";
$this->filterAssociations[] = $sql;
}
}

Displaying filtered products

Assuming we call our filterProducts() method within the products controller at some point, we can filter our products list quite easily. In our "products list" page, for instance, we can simply detect if the filter SQL field is empty; if it is not, we can replace the list query with the filter query. Of course, we should also swap our template, to indicate that the results are a filtered subset.

private function listProducts()
{
if( $this->filterSQL == '' )
{
$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
WHERE p.content_version=v.ID AND v.ID=c.current_revision
AND c.active=1 ";
}
else
{
$sql = $this->filterSQL;
}
$cache = $this->registry->getObject('db')->cacheQuery( $sql );
$this->registry->getObject('template')->getPage()->
addTag( 'products', array( 'SQL', $cache ) );
$this->registry->getObject('template')->
buildFromTemplates('header.tpl.php',
'list-products.tpl.php',
'footer.tpl.php');
$this->generateFilterOptions();
}

Remember, we must first call our filterProducts method, so I've added this to the switch statement within the controller's constructor.

$urlBits = $this->registry->getURLBits();
$this->filterProducts( $urlBits );

If we have filters in place with respect to price and weight (which are based off the products table), they would look like this:

PHP 5 E-commerce Development

If we click on one of the options, the products list would update to show products matching that criterion.

Improving product filtering

As with everything, there is always room for improvement. For the filter feature, potential improvements include:

  • Displaying the number of products matching a filter next to it.
  • Pagination—limiting the number of products displayed to the initial Y results, allowing the customer to move to the next set of results, so they are not overwhelmed with products.
  • Updating this number to account for filters already in place (that is, if there are 100 brand X products and we filter the price to < $5, there may only be 20 matching brand X products, and the number should update to reflect this).
  • Filter options with no matching products could be hidden, to prevent the customer from clicking them, and finding that it made no change.

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

>> 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 :

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