Tip

Automating Amazon research with the Zend Framework

Web frameworks have been all the rage lately, and for good reason. They eliminate a great deal of the mindless repetition involved in creating Web applications large and small. How many times have you written and revised ugly SQL statements? Refactored looping mechanisms only to later find out you accidentally removed part of the table tag structure, a side effect of mixing logic and presentation? Repeated code in far too many locations throughout your application? These annoyances and more can be resolved by using a framework that facilitates rapid development of applications by abstracting necessary yet menial tasks such as SQL statement creation. The framework provides clean separation of logic and presentation along with easy maintenance and extensibility through a well-organized application structure.

Spurred on by the enormous success of

Requires Free Membership to View

Rails, PHP developers have been hard at work creating a number of framework solutions. Notable efforts include Cake, Symfony, and, more recently, the Zend Framework. Because all of these solutions remain under heavy development, I suggest you spend some time experimenting with each to determine which best fits your present needs.

In fact, you might consider evaluating each on a per-project basis, as I've done for a recent application I built to track historical Amazon sales rankings for Apress' open source line. (We've long had a company-wide solution in place, but I wanted to build a custom-solution for my own use.) This application uses Amazon Web Services to periodically retrieve sales ranking figures for a select set of books, allowing me to more effectively monitor sales trends. Two screenshots of the application in action can be found in Figures 1 and 2.


Figure 1. Tracking Amazon sales rankings.


Figure 2. Viewing historical rankings for a given book.

The Zend Framework offers a pretty slick solution for accessing Amazon's Web Services. For this reason, I decided to adopt it for my project. Using its Amazon Service component in conjunction with a few others that are packaged with the framework, I created the application in almost no time at all. Here, my article recounts how I went about creating this application and shows you just how rapidly you can create applications using this wonderful solution.

Although the tutorial won't be as feature-complete as the actual application, it will nevertheless give you a great foundation from which to begin further exploration of this promising framework. I'll assume you understand both PHP and the MVC (model-view-controller) design pattern; if you're not yet familiar with the latter, please take some time to read the Wikipedia definition and explore some of the sites linked from it. Furthermore, this isn't a basic introduction to the Zend Framework. I won't show you how to install it, nor will I go over Zend Framework fundamentals, because several other tutorials have already been published regarding such matters. Instead, this tutorial concentrates solely on building a practical application using the framework.

Also, throughout the article, I purposefully demonstrate different approaches to doing things like executing queries for the sake of showing new users the framework's flexibility.

Required Zend components

I used five Zend Framework components to build the application:

  • Zend_Controller: This component is responsible for orchestrating the application's execution, receiving requests and ensuring the appropriate action executes.
  • Zend_Db: This component handles the framework's database operations. It provides the ability to easily interact with a database both in an abstract fashion and with standard queries (the component is built on top of PDO and therefore supports all databases supported by PDO) and profile queries. It generally makes database-related tasks very easy to implement.
  • Zend_Filter_Input: This component offers several solutions for filtering input, a task overlooked by many Web developers because of the tedium and complexity.
  • Zend_Service_Amazon: This component facilitates the retrieval of information found within Amazon's product database and made available through the aforementioned Amazon Web Services feature.
  • Zend_View: This component generates the application interface or pages.

Before we can begin taking advantage of these components, you must complete two preliminary tasks: registering for an Amazon WS account and creating the database tables that will be used to store the book and ranking information.

Create an Amazon Web Services account

Amazon requires users register to use its array of Web Services offerings. Not to worry -- registration is free. You can sign up for an account here. Once registered, you'll be provided with a unique authentication key that you will embed into any applications you create and which talks to Amazon's data store via its Web Services API. Later in this article, you'll learn where to embed this key.

Creating the MySQL tables

A storage mechanism is required for tracking the book information and historical sales rankings. For this exercise, I'll use MySQL, although because the Zend Framework's DB component is built on top of the PDO extension, you're free to use any database it supports (PostgreSQL, Oracle and SQLite, among others). The books table stores each book's title and ISBN, and the ranks table stores the periodical sales rankings for each book:

CREATE TABLE books (
   id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
   isbn CHAR(10) NOT NULL,
   title VARCHAR(100) NOT NULL
);

CREATE TABLE ranks (
   id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
   bookid SMALLINT UNSIGNED NOT NULL,
   tstamp TIMESTAMP NOT NULL,
   rank MEDIUMINT NOT NULL DEFAULT '0'
);

Take a few moments to populate the books table. In a later section, you'll learn how to populate the ranks table using an automated task.

Creating the Amazon Controller

Creating the controller (I called it AmazonController.php) used to power this application consists of just three actions: indexAction(), historicalAction() and populateAction(). The first two actions are presented in this section, along with the preliminary calls required to properly configure the action. The third action is covered in a later section.

<?php

// Include the necessary components
require_once 'Zend/Service/Amazon.php';
require_once 'Zend/Db.php';
require_once 'Zend/Db/Table.php';
require_once 'Zend/Filter.php';   

// Load up the required classes
Zend::loadClass('Zend_Controller_Action');
Zend::loadClass('Zend_View');
Zend::loadClass('Zend_Filter_Input');
   
// Create a new view object and set the view path
$view = new Zend_View();
$view->setScriptPath('c:\Apache2\htdocs\frameworks\amazon\app\views');
Zend::register('view', $view);

// Create necessary Zend_Db_Table instances
class Books extends Zend_Db_Table {}
class Ranks extends Zend_Db_Table {}

// Create the AmazonController class
   
class AmazonController extends Zend_Controller_Action 
{

   // MySQL connection variables
   private $params = array ('host'     => '127.0.0.1',
                            'username' => 'root',
                            'password' => 'secret',
                            'dbname'   => 'amazon');

Next, the indexAction() is created, which retrieves and presents the latest sales rankings for all books stored in the books table, as depicted in Figure 1. The URL used to trigger this action would look like this: http://www.example.com/amazon.

// indexAction presents the latest sales ranks for all books
public function indexAction()
{

   $db = Zend_Db::factory('pdomysql', $this->params);

   // Using temporary table for reasons of tutorial simplicity
   $result = $db->query('create temporary table tmp select bookid, max(tstamp)
                         as tstamp from ranks group by bookid');

   // Select latest ranks for each book
   $result = $db->query('select ranks.rank, books.isbn, books.title from ranks, books, tmp 
                         where ranks.bookid=books.id and ranks.tstamp = tmp.tstamp 
                         group by books.title order by ranks.bookid');
         
   $rows = $result->fetchAll();
         
   $data = array();

   foreach($rows as $row) {

      $id = $row["id"];
      $isbn = $row["isbn"];
      $title = $row["title"];
      $rank = number_format($row["rank"]);

      array_push($data, array('id'=>$id, 'isbn'=> $isbn, 'title'=>$title, 'rank'=>$rank));

  }

  $view = Zend::registry('view');
  $view->books = $data;
  echo $view->render('viewsalesrank.php');

} #end indexAction()

Next up is historicalAction(), which retrieves the historical sales rank data for a specific book, as depicted in Figure 2. This action requires one parameter -- the book's id value as stored in the books table. A typical URL would look like this: http://www.example.com/amazon/historical/id/4.

public function historicalAction()
{

   // Retrieve the book identifier 
   $params = new Zend_Filter_Input($this->_getAllParams());

   // Make sure the id only consists of numbers
   $id = $params->getDigits('id');

   //  Create the view object
   $view = Zend::registry('view');

   // Connect to the database
   $db = Zend_Db::factory('pdomysql', $this->params);
   Zend_Db_Table::setDefaultAdapter($db);

   // Retrieve Book information
   $books = new Books();
   $bookinfo = $books->find($id);

   // Retrieve Historical Rank Data
   $ranks = new Ranks();
   $rankrows = $ranks->fetchAll("bookid=$id",'tstamp DESC');

   // Assign the view data
   $view->title = $bookinfo->title;
   $view->isbn = $bookinfo->isbn;
   $view->ranks = $rankrows;

   // Render the view   
   echo $view->render('viewhistory.php');

} # end historicalAction()

}

?> 

We'll add a third action for populating the sales ranking data later in the article.

Creating the Views

Two views are required for my application. One shows the sales ranking summary for all books (viewsalesrank.php) and another shows historical data for a given book (viewhistory.php).

Because the views are very straightforward, I'll only show the viewsalesrank.php view, presented below. I'll also save some space and not include the style sheet, instead referring you to Cleiton Francisco's "Like Adwords CSS example, contributed to the icant.co.uk cascading style sheets gallery. Thanks, Cleiton, for the sweet CSS markup.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>

<title>Amazon Tracker</title>

  
<style type="text/css" media="Screen">
   @import url("http://localhost/frameworks/amazon/app/views/table.css");</style>

</head>

<body>

<h3>Apress OS Amazon Sales Rank Tracker</h3>

<?php if ($this->books): ?>

   <!-- A table of some books. -->
   <table border='1'>
      <thead>
      <tr>

         <th>ISBN</th>
         <th>Title</th>
         <th>Recent Sales Rank</th>
         <th>Historical</th>            
      </tr>

      </thead>
      <tbody>
         <?php foreach ($this->books as $key => $val): ?>
            <tr>
            <td><?php echo $val['isbn']; ?></td>

            <td><?php echo $val['title']; ?></td>
            <td><?php echo $val['rank']; ?></td>          
            <td><a href="/amazon/historical/id/<?php echo $val['id'];?>">Historical</a></td>            
         </tr>

         <?php endforeach; ?>
      </tbody>
   </table>
    
<?php else: ?>
 
   <p>There are no books to display.</p>

    
<?php endif; ?>

</body>
</html>

Populating the database

To populate the ranks table, let's create a new action called populateAction(), which will contact Amazon's database via its Web Services API. Add the following action to AmazonController.php:
public function populateAction()
{
   // Create Amazon Service object 

   $amazon = new Zend_Service_Amazon('YOURAMAZONKEYHERE');

   // Connect to the database

   $db = Zend_Db::factory('pdomysql', $this->params);
   Zend_Db_Table::setDefaultAdapter($db);

   // Create appropriate Zend_Db_Table instances

   $books = new Books();
   $table = new Ranks();

   // Retrieve all books found in the books table

   $rows = $books->fetchAll();

   // Loop through each book, grab latest sales rank, update ranks table

   foreach($rows as $row) {

      $rowid = $row->id;
      $isbn = $row->isbn;

      $item = $amazon->itemLookup($isbn, 
              array('ResponseGroup' => 'Medium'));
      $rank = $item->SalesRank;

      $data = array (
         'bookid'   => $rowid,
         'rank'     => $rank,
         'tstamp'   => NULL
      );

      $id = $table->insert($data);
   }

   echo "Population complete";

} #end populateAction

To populate the ranks table with the first set of rankings, execute the following URL (replacing example.com with the appropriate domain), which executes the AmazonController.php's populateAction() method:

http://www.example.com/amazon/populate

Of course, you're going to want a more automated approach to updating the ranks table. Just create a cron job, timed to execute periodically. For instance, the following cron job will cause the populate action to be executed each hour:

0 * * * * wget -q http://www.example.com/amazon/populate

Conclusion

I want this tutorial to send your mind racing regarding just how much more efficient you can be by embracing a solution such as the Zend Framework. I invite you to email me with your questions and comments!

About the author: W. Jason Gilmore has developed countless Web applications over the past seven years and has dozens of articles to his credit on Internet application development topics. He is Apress' Open Source Editorial Director and is the author of three books, including Beginning PHP 5 and MySQL 5: From Novice to Professional, (Apress), now in its second edition. With co-author Robert Treat, he also contributed to Beginning PHP and PostgreSQL 8: From Novice to Professional (Apress). Gilmore is the co-founder of IT Enlightenment, a technical training company.


This was first published in July 2006

There are Comments. Add yours.

 
TIP: Want to include a code block in your comment? Use <pre> or <code> tags around the desired text. Ex: <code>insert code</code>

REGISTER or login:

Forgot Password?
By submitting you agree to receive email from TechTarget and its partners. If you reside outside of the United States, you consent to having your personal data transferred to and processed in the United States. Privacy
Sort by: OldestNewest

Forgot Password?

No problem! Submit your e-mail address below. We'll send you an email containing your password.

Your password has been sent to:

Disclaimer: Our Tips Exchange is a forum for you to share technical advice and expertise with your peers and to learn from other enterprise IT professionals. TechTarget provides the infrastructure to facilitate this sharing of information. However, we cannot guarantee the accuracy or validity of the material submitted. You agree that your use of the Ask The Expert services and your reliance on any questions, answers, information or other materials received through this Web site is at your own risk.