33 Connecting to Web Services with XML and SOAP

IN THE PAST FEW YEARS, EXTENSIBLE MARKUP LANGUAGE (XML) has become an important means of communication. In this chapter, you use Amazon’s Web Services interface to build a shopping cart on your website that uses Amazon as a back end. (This application is named Tahuayo, which is the name of an Amazonian tributary.) You use two different methods to do this: SOAP and REST. REST is also known as XML over HTTP. You use PHP’s built-in SimpleXML library and the external NuSOAP library to implement these two methods.

In this chapter, we discuss the following topics:

image  Understanding the basics of XML and Web Services

image  Using XML to communicate with Amazon

image  Parsing XML with PHP’s SimpleXML library

image  Caching responses

image  Talking to Amazon with NuSOAP

Project Overview: Working with XML and Web Services

We have two goals with this project: The first is to help you gain an understanding of what XML and SOAP are and how to use them in PHP. The second is to put these technologies to use to communicate with the outside world. We chose the Amazon Web Services program as an interesting example that you might find useful for your own website.

Amazon has long offered an associate program that allows you to advertise Amazon’s products on your website. Users can then follow a link to each product’s page on Amazon’s site. If someone clicks through from your site and then buys that product, you receive a small commission.

The Web Services program enables you to use Amazon more as an engine: You can search it and display the results via your own site, or fill a user’s shopping cart directly with the contents of items he has selected while browsing your site. In other words, the customer uses your site until it is time to check out, which he then has to do via Amazon.

Communications between you and Amazon can take place in two possible ways. The first way is by using XML over HTTP, which is also known as Representational State Transfer (REST). If, for example, you want to perform a search using this method, you send a normal HTTP request for the information you require, and Amazon will respond with an XML document containing the information you requested. You can then parse this XML document and display the search results to the end user using an interface of your choice. The process of sending and receiving data via HTTP is very simple, but how easy it is to parse the resulting document depends on the complexity of the document.

The second way to communicate with Amazon is to use SOAP, which is one of the standard Web Services protocols. It used to stand for Simple Object Access Protocol, but it was decided that the protocol wasn’t that simple and that the name was a bit misleading. The result is that the protocol is still called SOAP, but it is no longer an acronym.

In this project, you build a SOAP client that can send requests to and receive responses from the Amazon SOAP server. They contain the same information as the responses you get using the XML over HTTP method, but you will use a different approach to extract the data, namely the NuSOAP library.

Our final goal in this project is for you to build your own book-selling website that uses Amazon as a back end. You build two alternative versions: one using REST and one using SOAP.

Before moving into the specific elements of your application, take a moment to familiarize yourself with the structure and use of XML as well as Web Services in general.

Understanding XML

Let’s spend a few moments examining XML and Web Services, in case you are not familiar with these concepts.

As mentioned previously, XML is the Extensible Markup Language. The specification is available from the W3C. Lots of information about XML can be found at the W3C’s XML site at http://www.w3.org/XML/.

XML is derived from the Standard Generalized Markup Language, or SGML. If you already know Hypertext Markup Language, or HTML (and if you don’t, you have started reading this book at the wrong end), you will have little difficulty with the concepts of XML.

XML is a tag-based text format for documents. As an example of an XML document, Listing 33.1 shows a response Amazon sends to an XML over HTTP request given a certain set of request parameters.

Listing 33.1  XML Document Describing the First Edition of This Book


<?xml version="1.0" encoding="UTF-8"?>
<ItemLookupResponse
   xmlns="http://webservices.amazon.com/AWSECommerceService/2005-03-23">
  <Items>
     <Request>
       <IsValid>True</IsValid>
         <ItemLookupRequest>
         <IdType>ASIN </IdType>
         <ItemId>0672317842</ItemId>
         <ResponseGroup>Similarities</ResponseGroup>
         <ResponseGroup>Small</ResponseGroup>
       </ItemLookupRequest>
     </Request>
    <Item>
      <ASIN>0672317842</ASIN>
      <DetailPageURL>http://www.amazon.com/PHP-MySQL-Development-Luke-
Welling/dp/0672317842%3F%26linkCode%3Dsp1%26camp%3D2025%26creative%3D165953%26crea
tiveASIN%3D0672317842
      </DetailPageURL>
      <ItemAttributes>
        <Author>Luke Welling</Author>
        <Author>Laura Thomson</Author>
        <Manufacturer>Sams</Manufacturer>
        <ProductGroup>Book</ProductGroup>
        <Title>PHP and MySQL Web Development</Title>
      </ItemAttributes>
      <SimilarProducts>
         <SimilarProduct>
            <ASIN>1590598628</ASIN>
            <Title>Beginning PHP and MySQL: From Novice to Professional,
            Third Edition (Beginning from Novice to Professional)</Title>
         </SimilarProduct>
         <SimilarProduct>
            <ASIN>032152599X</ASIN>
            <Title>PHP 6 and MySQL 5 for Dynamic Web Sites:
                Visual QuickPro Guide</Title>
         </SimilarProduct>
         <SimilarProduct>
            <ASIN>B00005UL4F</ASIN>
            <Title>JavaScript Definitive Guide</Title>
         </SimilarProduct>
         <SimilarProduct>
            <ASIN>1590596145</ASIN>
            <Title>CSS Mastery: Advanced Web Standards Solutions</Title>
         </SimilarProduct>
         <SimilarProduct>
            <ASIN>0596005431</ASIN>
            <Title>Web Database Applications with PHP &amp; MySQL,
               2nd Edition</Title>
         </SimilarProduct>
      </SimilarProducts>
  </Item>
</Items>


The document begins with the following line:

xml version= “1.0” encoding=“UTF-8”?>

This standard declaration tells you the following document will be XML using UTF-8 character encoding.

Now look at the body of the document. The whole document consists of pairs of opening and closing tags, such as what you see between the opening and closing Item tags:

<Item>

</Item> Item is an element, just as it would be in HTML. And, just as in HTML, you can nest elements, such as this example of the ItemAttributes element, within the Item element, which also has elements within it such as the Author element:

<ItemAttributes>
   <Author>Luke Welling</Author>
   <Author>Laura Thomson</Author>
   <Manufacturer>Sams</Manufacturer>
   <ProductGroup>Book</ProductGroup>
   <Title>PHP and MySQL Web Development</Title>

There are also some differences from HTML. The first is that each opening tag must have a closing tag. The exception to this rule is empty elements that open and close in a single tag because they do not enclose any text. If you are familiar with XHTML, you have seen the <br/> tag used in place of <br> for this exact reason. In addition, all elements must be properly nested. You would probably get away with <b><i>Text</b></i> using an HTML parser, but to be valid XML or XHTML, the tags would need to be properly nested as <b><i>Text</i></b>.

The main difference you will notice between XML and HTML is that we seem to be making up our own tags as we go along! This is the flexibility of XML. You can structure your documents to match the data that you want to store. You can formalize the structure of XML documents by writing either a Document Type Definition (DTD) or an XML Schema. Both of these documents are used to describe the structure of a given XML document. If you like, you can think of the DTD or Schema as being like a class declaration and the XML document as being like an instance of that class. In this particular example, you do not use a DTD or Schema.

You can read Amazon’s current XML schema for web services at http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.xsd. You should be able to open the XML Schema directly in your browser.

Notice that, other than the initial XML declaration, the entire body of the document is contained inside the ItemLookupResponse element. This is called the root element of the document. Let’s take a closer look:

<ItemLookupResponse
   xmlns="http://webservices.amazon.com/AWSECommerceService/2005-03-23">

This element has a slightly unusual attribute, the XML namespaces. You do not need to understand namespaces for what you will do in this project, but they can be very useful. The basic idea is to qualify element and attribute names with a namespace so that common names do not clash when dealing with documents from different sources.

If you would like to know more about namespaces, you can read the document “Namespaces in XML Recommendation” at http://www.w3.org/TR/REC-xml-names/.

If you would like to know more about XML in general, a huge variety of resources is available. The W3C site is an excellent place to start, and there are also hundreds of excellent books and web tutorials. ZVON.org includes one of the best web-based tutorials on XML.

Understanding Web Services

Web Services are application interfaces made available via the World Wide Web. If you like to think in object-oriented terms, a Web Service can be seen as a class that exposes its public methods via the Web. Web Services are now widespread, and some of the biggest names in the business are making some of their functionality available via Web Services.

For example, Google, Amazon, eBay, and PayPal all offer a range of Web Services. After you go through the process of setting up a client to the Amazon interface in this chapter, you should find it very straightforward to build a client interface to Google. You can find more information at http://code.google.com/apis/.

Several core protocols are involved in this remote function call methodology. Two of the most important ones are SOAP and WSDL.

SOAP

SOAP is a request-and-response–;driven messaging protocol that allows clients to invoke Web Services and allows servers to respond. Each SOAP message, whether a request or response, is a simple XML document. A sample SOAP request you might send to Amazon is shown in Listing 33.2. In fact, this request produced the XML response in Listing 33.1.

Listing 33.2  SOAP Request for a Search Based on the ASIN


<SOAP-ENV:Envelope>
  <SOAP-ENV:Body>
    <m:ItemLookup>
      <m:Request>
        <m:AssociateTag>webservices-20</m:AssociateTag>
        <m:IdType>ASIN</m:IdType>
        <m:ItemId>0672317842</m:ItemId>
        <m:AWSAccessKeyId>0XKKZBBJHE7GNBWF2ZG2</m:AWSAccessKeyId>
        <m:ResponseGroup>Similarities</m:ResponseGroup>
        <m:ResponseGroup>Small</m:ResponseGroup>
      </m:Request>
    </m:ItemLookup>
  </SOAP-ENV:Body>


The SOAP message begins with the declaration that this is an XML document. The root element of all SOAP messages is the SOAP envelope. Within it, you find the Body element that contains the actual request.

This request is an ItemLookup, which in this instance asks the Amazon server to look up a particular item in its database based on the ASIN (Amazon.com Standard Item Number). This is a unique identifier given to every product in the Amazon.com database.

Think of ItemLookup as a function call on a remote machine and the elements contained within this element as the parameters you are passing to that function. In this example, after passing the value “ASIN” via the IdType element, the actual ASIN (0672317842) is passed via the ItemId element; this is the ASIN for the first edition of this book. You also need to pass in the AssociateTag, which is your Associate ID; the type of responses you would like (via the ResponseGroup element); and the AWSAccessKeyId, which is a developer token value Amazon will give you.

The response to this request is similar to the XML document you looked at in Listing 33.1, but it is enclosed in a SOAP envelope.

When dealing with SOAP, you usually generate SOAP requests and interpret responses programmatically using a SOAP library, regardless of the programming language you are using. This is a good thing because it saves on the effort of having to build the SOAP request and interpret the response manually.

WSDL

WSDL stands for Web Services Description Language. (It is often pronounced “wiz-dul.”) This language is used to describe the interface to available services at a particular website. If you would like to see the WSDL document describing the Amazon Web Services used in this chapter, it is located at http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl.

As you will see if you follow this link, WSDL documents are significantly more complex than SOAP messages. You will always generate and interpret them programmatically, if given a choice.

If you would like to know more about WSDL, you can consult the W3C Recommendation at http://www.w3.org/TR/wsdl20/.

Solution Components

There are a few parts you need to build your solution. As well as the most obvious parts—a shopping cart interface to show to customers and code to connect to Amazon via REST or SOAP—you need some ancillary parts. Having retieved an XML document, your code needs to parse it to extract the information your cart will display. To meet Amazon’s requirements and to improve performance, you need to consider caching. Finally, as the checkout activity needs to be done at Amazon, you need some functionality to hand over the contents of the user’s cart to Amazon and pass the user over to that service.

You obviously need to build a shopping cart as the front end for the system. You’ve done this before, in Chapter 28, “Building a Shopping Cart.” Because shopping carts are not the main focus in this project, this chapter contains a simplified application. You just need to provide a basic cart so that you can track what the customer would like to buy and report it to Amazon upon checkout.

Using Amazon’s Web Services Interfaces

To use the Amazon Web Services interface, you need to sign up for a developer token at http://aws.amazon.com. This token is used to identify you to Amazon when your requests come in.

You might also like to sign up for an Amazon Associate ID. It enables you to collect commission if people buy any products via your interface.

The Amazon Web Services (AWS) Resource Center for Developers, found at http://developer.amazonwebservices.com/, contains significant amounts of documentation, tutorials, and sample code for connecting to all of the Amazon Web Services via SOAP and REST. Following along with the samples in this chapter will produce a working system and introduce you to the basics of connecting to AWS and retrieving information, but you should spend some time with the documentation if you plan to build upon the application described in this chapter. For instance, you may search for and retrieve a variety of items from both the browsing and direct searching interfaces. The data returned to you can be in a variety of structures, depending on what elements you need. All of this information is documented in the AWS Developer Guide, available on the web site.

When you register for a developer token, you need to agree to the license agreement. This is worth reading because it is not the usual yada-yada software license. Some of the license conditions that are important during implementation are the following:

image  You must not make more than one request per second.

image  You must cache data coming from Amazon.

image  You may cache most data for 24 hours and some stable attributes for up to three months.

image  If you cache prices and availability for more than an hour, you must display a disclaimer.

image  You must link back to a page on Amazon.com and must not link text or graphics downloaded from Amazon to another commercial website.

With a hard-to-spell domain name, no promotion, and no obvious reason to use Tahuayo.com instead of going straight to Amazon.com, you do not need to take any further steps to keep requests below one per second.

In this project, you implement caching to meet the conditions at points 2 to 4. The application caches images for 24 hours and product data (which contains prices and availability) for 1 hour.

Your application also follows the fifth point. You want items on the main page to link to detailed pages on your site, but you link to Amazon when an order is complete.

Parsing XML: REST Responses

The most popular interface Amazon offers to its Web Services is via REST. This interface accepts a normal HTTP request and returns an XML document. To use this interface, you need to be able to parse the XML response Amazon sends back to you. You can do this by using PHP’s SimpleXML library.

Using SOAP with PHP

The other interface offering the same Web Services is SOAP. To access these services using SOAP, you need to use one of the various PHP SOAP libraries. There is a built-in SOAP library, but because it will not always be available, you can use the NuSOAP library. Because NuSOAP is written in PHP, it does not need compiling. It is just a single file to be called via require_once().

NuSOAP is available from http://sourceforge.net/projects/nusoap/. NuSOAP is available under the Lesser GPL; that is, you may use it in any application, including nonfree applications.

Caching

As we mentioned previously, one of the terms and conditions imposed upon developers by Amazon is that data downloaded from Amazon via Web Services must be cached. In this solution, you will need to find a way to store and reuse the data that you download until it has passed its use-by date.

Solution Overview

This project again uses an event-driven approach to run the code, as in Chapters 29, “Building a Web-Based Email Service,” and 30, “Building a Mailing List Manager.” We did not draw a system flow diagram for you in this example because there are only a few screens in the system, and the links between them are simple.

Users will begin at the main Tahuayo screen, shown in Figure 33.1.

Figure 33.1  The first screen for Tahuayo shows all the main features of the site: category navigation, searching, and the shopping cart.

Image

As you can see, the main features of the site are the Selected Categories display and the items in those categories. By default, you display the current best-sellers in the nonfiction category on the front page. If a user clicks on another category, she will see a similar display for that category.

A brief piece of terminology before we go further: Amazon refers to categories as browse nodes. You will see this expression used throughout our code and the official documentation.

The documentation provides a list of popular browse nodes. In addition, if you want a particular one, you can browse the normal Amazon.com site and read it from the URL, you can use the Browse Nodes resource at http://www.browsernodes.com/. Frustratingly, some important categories, such as best-selling books, cannot be accessed as browse nodes.

More books and links to additional pages are available at the bottom of this page, but you can’t see them in the screenshot. You will display 10 books on each page, along with links to up to 30 other pages. This 10-per page value is set by Amazon. The 30-page limit was our own arbitrary choice.

From here, users can click through to detailed information on individual books. This screen is shown in Figure 33.2.

Figure 33.2  The details page shows more information about a particular book, including similar products and reviews.

Image

Although it does not all fit in a screenshot, the script shows most, but not all, of the information that Amazon sends with a heavy request on this page. We chose to ignore parts aimed at products other than books and the list of categories the book fits in.

If users click through the cover image, they will be able to see a larger version of the image.

You might have noticed the search box at the top of the screen in these figures. This feature runs a keyword search through the site and searches Amazon’s catalog via its Web Services interface. An example of the output of a search is shown in Figure 33.3.

Figure 33.3  This screen shows the results of searching for batman.

Image

Although this project lists only a few categories, customers can get to any book by using the search facility and navigating to particular books.

Each individual book has an Add to Cart link with it. Clicking on this or the Details link in the cart summary takes the customer to a display of the cart contents. This page is shown in Figure 33.4.

Figure 33.4  From the shopping cart page, the customer can delete items, clear the cart, or check out

Image

Finally, when a customer checks out by clicking on one of the Checkout links, you send the details of her shopping cart to Amazon and take her there. She will see a page similar to the one in Figure 33.5.

You should now understand what we mean by building your own front end and using Amazon as the back end.

Because this project also uses the event-driven approach, most of the core decision-making logic of the application is in one file, index.php. A summary of the files in the application is shown in Table 33.1.

Figure 33.5  Before putting the items in the Amazon cart, the system confirms the transaction and shows all the items from the Tahuayo cart.

Image

Table 33.1  Files in the Tahuayo Application

Image

You also need the nusoap.php file we mentioned previously because it is required in these files. NuSOAP is in the chapter33 directory on the CD-ROM at the back of the book, but you might like to replace it with a newer version from http://sourceforge.net/projects/nusoap/if a new version is released.

Let’s begin this project by looking at the core application file index.php.

Core Application

The application file index.php is shown in Listing 33.3.

Listing 33.3  index.php—The Core Application File


<?php
//we are only using one session variable 'cart' to store the cart contents
session_start();
require_once('constants.php'),
require_once('Product.php'),
require_once('AmazonResultSet.php'),
require_once('utilityfunctions.php'),
require_once('bookdisplayfunctions.php'),
require_once('cartfunctions.php'),
require_once('categoryfunctions.php'),
// These are the variables we are expecting from outside.
// They will be validated and converted to globals
$external = array('action', 'ASIN', 'mode', 'browseNode', 'page', 'search'),
// the variables may come via Get or Post
// convert all our expected external variables to short global names
foreach ($external as $e) {
  if(@$_REQUEST[$e]) {
    $$e = $_REQUEST[$e];
  } else {
    $$e = ' ';
    }
    $$e = trim($$e);
  }
  // default values for global variables
  if($mode==' ') {
    $mode = 'Books'; // No other modes have been tested
  }
  if($browseNode==' '){
    $browseNode = 53; //53 is bestselling non-fiction books
  }
  if($page==' '){
    $page = 1; // First Page - there are 10 items per page
  }
  //validate/strip input
  if(!eregi('^[A-Z0-9]+$', $ASIN)) {
    // ASINS must be alpha-numeric
    $ASIN =' ';
  }
  if(!eregi('^[a-z]+$', $mode)) {
    // mode must be alphabetic
    $mode = 'Books';
  }
  $page=intval($page); // pages and browseNodes must be integers
  $browseNode = intval($browseNode);
  // it may cause some confusion, but we are stripping characters out from
  // $search it seems only fair to modify it now so it will be displayed
  // in the heading
  $search = safeString($search);
  if(!isset($_SESSION['cart'])) {
    session_register('cart'),
    $_SESSION['cart'] = array();
  }
  // tasks that need to be done before the top bar is shown
  if($action == 'addtocart') {
    addToCart($_SESSION['cart'], $ASIN, $mode);
  }
  if($action == 'deletefromcart') {
    deleteFromCart($_SESSION['cart'], $ASIN);
  }
  if($action == 'emptycart') {
    $_SESSION['cart'] = array();
  }
  // show top bar
  require_once ('topbar.php'),
  // main event loop. Reacts to user action on the calling page
  switch ($action) {
    case 'detail':
      showCategories($mode);
      showDetail($ASIN, $mode);
    break;
    case 'addtocart':
    case 'deletefromcart':
    case 'emptycart':
    case 'showcart':
      echo "<hr /><h1>Your Shopping Cart</h1>";
      showCart($_SESSION['cart'], $mode);
    break;
    case 'image':
      showCategories($mode);
      echo "<h1>Large Product Image</h1>";
      showImage($ASIN, $mode);
    break;
    case 'search':
      showCategories($mode);
      echo "<h1>Search Results For ".$search."</h1>";
      showSearch($search, $page, $mode);
    break;
    case 'browsenode':
    default:
      showCategories($mode);
      $category = getCategoryName($browseNode);
      if(!$category || ($category=='Best Selling Books')) {
        echo "<h1>Current Best Sellers</h1>";
      } else {
        echo "<h1>Current Best Sellers in ".$category."</h1>";
      }
      showBrowseNode($browseNode, $page, $mode) ;
    break;
}
require ('bottom.php'),


Let’s work our way through this file. You begin by creating a session. You store the customer’s shopping cart as a session variable as you have done before.

You then include several files. Most of them are functions that we discuss later, but we need to address the first included file now. This file, constants.php, defines some important constants and variables that will be used throughout the application. The contents of constants.php can be found in Listing 33.4.

Listing 33.4  constants.php—Declaring Key Global Constants and Variables


<?php
// this application can connect via REST (XML over HTTP) or SOAP
// define one version of METHOD to choose.
// define('METHOD', 'SOAP'),
define('METHOD', 'REST'),
// make sure to create a cache directory an make it writable
define('CACHE', 'cache'), // path to cached files
define('ASSOCIATEID', 'XXXXXXXXXXXXXX'), //put your associate id here
define('DEVTAG', 'XXXXXXXXXXXXXX'), // put your developer tag here
//give an error if software is run with the dummy devtag
if(DEVTAG=='XXXXXXXXXXXXXX') {
  die ("You need to sign up for an Amazon.com developer tag at
       <a href="https://aws.amazon.com/">Amazon</a>
       when you install this software. You should probably sign up
       for an associate ID at the same time. Edit the file constants.php.");
}
// (partial) list of Amazon browseNodes.
$categoryList = array(5=>'Computers & Internet', 3510=>'Web Development',
                      295223=>'PHP', 17=>'Literature and Fiction',
                      3=>'Business & Investing', 53=>'Non Fiction',
                      23=>'Romance', 75=>'Science', 21=>'Reference',
                      6 =>'Food & Wine', 27=>'Travel',
                      16272=>'Science Fiction'
                     );


This application has been developed to use either REST or SOAP. You can set which one it should use by changing the value of the METHOD constant.

The CACHE constant holds the path to the cache for the data you download from Amazon. Change this to the path you would like to use on your system.

The ASSOCIATEID constant holds the value of your Associate ID. If you send this value to Amazon with transactions, you get a commission. Be sure to change it to your own Associate ID.

The DEVTAG constant holds the value of the developer token Amazon gives you when you sign up. You need to change this value to your own developer token; otherwise, the application will not work. You can sign up for a tag at http://aws.amazon.com.

Now let’s look back at index.php. It contains some preliminaries and then the main event loop. You begin by extracting any incoming variables from the $_REQUEST superglobal that came via GET or POST. You then set up some default values for some standard global variables that determine what will be displayed later, as follows:

// default values for global variables
if($mode==' ') {
  $mode = 'Books'; // No other modes have been tested
}
if($browseNode==' '){
  $browseNode = 53; //53 is bestselling non-fiction books
}
if($page==' ') {
  $page = 1; // First Page - there are 10 items per page

You set the mode variable to Books. Amazon supports many other modes (types of products), but for this application, you just need to worry about books. Modifying the code in this chapter to deal with other categories should not be too hard. The first step in this expansion would be to reset $mode. You would need to check the Amazon documentation to see what other attributes are returned for nonbook products and remove book-specific language from the user interface.

The browseNode variable specifies what category of books you would like displayed. This variable may be set if the user has clicked through one of the Selected Categories links. If it is not set—for example, when the user first enters the site—you will set it to 53. Amazon’s browse nodes are simply integers that identify a category. The value 53 represents the category Non-Fiction Books, which seems as good a node as any other to display on the initial front page given that some of the best generic categories (such as Best Sellers) are not available as browse nodes.

The page variable tells Amazon which subset of the results you would like displayed within a given category. Page 1 contains results 1–;10, page 2 has results 11–;20, and so on. Amazon sets the number of items on a page, so you do not have control over this number. You could, of course, display two or more Amazon “pages” of data on one of your pages, but 10 is both a reasonable figure and the path of least resistance.

Next, you need to tidy up any input data you have received, whether through the search box or via GET or POST parameters:

//validate/strip input
if(!eregi('^[A-Z0-9]+$', $ASIN)) {
  // ASINS must be alpha-numeric
  $ASIN =' ';
}
if(!eregi('^[a-z]+$', $mode)) {
  // mode must be alphabetic
  $mode = 'Books';
}
$page=intval($page); // pages and browseNodes must be integers
$browseNode = intval($browseNode);
// it may cause some confusion, but we are stripping characters out from
// $search it seems only fair to modify it now so it will be displayed
// in the heading

This is nothing new. The safeString() function is in the utilityfunctions.php file. It simply removes any nonalphanumeric characters from the input string via a regular expression replacement. Because we have covered this topic before, we did not include it here in the text.

The main reason that you need to validate input in this application is that you use the customer’s input to create filenames in the cache. You could run into serious problems if you allow customers to include .. or / in their input.

Next, you set up the customer’s shopping cart, if she does not already have one:

if(!isset($_SESSION['cart'])){
  session_register('cart'),
  $_SESSION['cart'] = array();

You still have a few tasks to perform before you can display the information in the top information bar on the page (see Figure 33.1 for a reminder of what this looks like). A glimpse of the shopping cart is shown in the top bar of every page. It is therefore important that the cart variable is up to date before this information is displayed:

// tasks that need to be done before the top bar is shown
if($action == 'addtocart') {
  addToCart($_SESSION['cart'], $ASIN, $mode);
}
if($action == 'deletefromcart') {
  deleteFromCart($_SESSION['cart'], $ASIN);
}
if($action == 'emptycart') {
  $_SESSION['cart'] = array();

Here, you add or delete items from the cart as necessary before displaying the cart. We come back to these functions when we discuss the shopping cart and checking out. If you want to look at them now, they are in the file cartfunctions.php. We are leaving them aside for a minute because you need to understand the interface to Amazon first.

Next, you include the file topbar.php. This file simply contains HTML and a style sheet and a single function call to the ShowSmallCart() function (from cartfunctions.php). It displays the small shopping cart summary in the top-right corner of the figures. We come back to this when we discuss the cart functions.

Finally, we come to the main event-handling loop. A summary of the possible actions is shown in Table 33.2.

Table 33.2  Possible Actions in the Main Event Loop

Image

As you can see, the first four actions in this table relate to retrieving and displaying information from Amazon. The second group of four deals with managing the shopping cart.

The actions that retrieve data from Amazon all work in a similar way. Let’s consider retrieving data about books in a particular browsenode (category) as an example.

Showing Books in a Category

The code executed when the action is browsenode (view a category) is as follows:

showCategories($mode);
$category = getCategoryName($browseNode);
if(!$category || ($category=='Best Selling Books')) {
  echo "<h1>Current Best Sellers</h1>";
} else {
  echo "<h1>Current Best Sellers in ".$category."</h1>";
}

The showCategories() function displays the list of selected categories you see near the top of most of the pages. The getCategoryName() function returns the name of the current category given its browsenode number. The showBrowseNode() function displays a page of books in that category.

Let’s begin by considering the showCategories() function. The code for this function is shown in Listing 33.5.

Listing 33.5  showCategories()Function from categoryfunctions.php—A List of Categories


//display a starting list of popular categories
function showCategories($mode) {
  global $categoryList;
  echo "<hr/><h2>Selected Categories</h2>";
  if($mode == 'Books') {
   asort($categoryList);
    $categories = count($categoryList);
    $columns = 4;
    $rows = ceil($categories/$columns);
    echo "<table border="0" cellpadding="0" cellspacing="0"
            width="100%"><tr>";
    reset($categoryList);
    for($col = 0; $col<$columns; $col++) {
      echo "<td width="".(100/$columns)."%" valign="top"><ul>";
      for($row = 0; $row<$rows; $row++) {
        $category = each($categoryList);
        if($category) {
        $browseNode = $category['key'];
        $name = $category['value'];
        echo"<li><span class="category">
         <a href="index.php?action=browsenode&browseNode="
          .$browseNode."">".$name."</a></span></li>";
      }
    }
    echo "</ul></td>";
  }
  echo "</tr></table><hr/>";
}


This function uses an array called categoryList, declared in the file constants.php, to map browsenode numbers to names. The desired browsenodes are simply hard-coded into this array. This function sorts the array and displays the various categories.

The getCategoryName() function called next in the main event loop looks up the name of the browsenode that you are currently looking at so you can display a heading on the screen such as Current Best Sellers in Business & Investing. It looks up this heading in the categoryList array mentioned previously.

The fun really starts when you get to the showBrowseNode() function, shown in Listing 33.6.

Listing 33.6  showBrowseNode() Function from bookdisplayfunctions.php—A List of Categories


// For a particular browsenode, display a page of products
function showBrowseNode($browseNode, $page, $mode) {
  $ars = getARS('browse', array('browsenode'=>$browseNode,
        'page' => $page, 'mode'=>$mode));
  showSummary($ars->products(), $page, $ars->totalResults(),
        $mode, $browseNode);


The showBrowseNode() function does exactly two things. First, it calls the getARS() function from cachefunctions.php. This function gets and returns an AmazonResultSet object (more on this in a moment). Then it calls the showSummary() function from bookdisplayfunctions.php to display the retrieved information.

The getARS() function is absolutely key to driving the whole application. If you work your way through the code for the other actions—viewing details, images, and searching—you will find that it all comes back to this.

Getting an AmazonResultSet Class

Let’s look at that getARS() function in more detail. It is shown in Listing 33.7.

Listing 33.7  getARS() Function from cachefunctions.php—A Resultset for a Query


// Get an AmazonResultSet either from cache or a live query
// If a live query add it to the cache
function getARS($type, $parameters) {
  $cache = cached($type, $parameters);
  if ($cache) {
    // if found in cache
    return $cache;
  } else {
    $ars = new AmazonResultSet;
    if($type == 'asin') {
      $ars->ASINSearch(padASIN($parameters['asin']), $parameters['mode']);
    }
    if($type == 'browse') {
      $ars->browseNodeSearch($parameters['browsenode'],
             $parameters['page'], $parameters['mode']);
    }
    if($type == 'search') {
      $ars->keywordSearch($parameters['search'], $parameters['page'],
             $parameters['mode']);
    }
    cache($type, $parameters, $ars);
    
    }
    return $ars;
  }


This function is designed to drive the process of getting data from Amazon. It can do this in two ways: either from the cache or live from Amazon. Because Amazon requires developers to cache downloaded data, the function first looks for data in the cache. We discuss the cache shortly.

If you have not already performed this particular query, the data must be fetched live from Amazon. You do this by creating an instance of the AmazonResultSet class and calling the method on it that corresponds to the particular query you want to run. The type of query is determined by the $type parameter. In the category (or browse node) search example, you pass in browse as the value for this parameter (refer to Listing 33.6). If you want to perform a query about one particular book, you should pass in the value asin, and if you want to perform a keyword search, you should set the parameter to search.

Each of these parameters invokes a different method on the AmazonResultSet class. The individual item search calls the ASINSearch() method. The category search calls the browseNodeSearch() method. The keyword search calls the keywordSearch() method.

Let’s take a closer look at the AmazonResultSet class. The full code for this class is shown in Listing 33.8.

Listing 33.8  AmazonResultSet.php—A Class for Handling Amazon Connections


<?php
// you can switch between REST and SOAP using this constant set in
// constants.php
if(METHOD=='SOAP') {
  include_once('nusoap/lib/nusoap.php'),}
// This class stores the result of queries
// Usually this is 1 or 10 instances of the Product class
class AmazonResultSet {
  private $browseNode;
  private $page;
  private $mode;
  private $url;
  private $type;
  private $totalResults;
  private $currentProduct = null;
  private $products = array(); // array of Product objects
  function products() {
    return $this->products;
  }
  function totalResults() {
    return $this->totalResults;
  }
  function getProduct($i) {
    if(isset($this->products[$i])) {
      return $this->products[$i];
    } else {
      return false;
    }
  }
  // Perform a query to get a page full of products from a browse node
  // Switch between XML/HTTP and SOAP in constants.php
  // Returns an array of Products
  function browseNodeSearch($browseNode, $page, $mode) {
    $this->Service = "AWSECommerceService";
    $this->Operation = "ItemSearch";
    $this->AWSAccessKeyId = DEVTAG;
    $this->AssociateTag = ASSOCIATEID;
    $this->BrowseNode = $browseNode;
    $this->ResponseGroup = "Large";
    $this->SearchIndex = $mode;
    $this->Sort= 'salesrank';
    $this->TotalPages= $page;
    if(METHOD=='SOAP') {
      $soapclient = new nusoap_client(
      'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl',
      'wsdl'),
      $soap_proxy= $soapclient->getProxy();
      $request = array ('Service' => $this->Service,
      'Operation' => $this->Operation, 'BrowseNode' => $this->BrowseNode,
      'ResponseGroup' => $this->ResponseGroup, 'SearchIndex' =>
       $this->SearchIndex, 'Sort' => $this->Sort, 'TotalPages' =>
       $this->TotalPages);
      $parameters = array('AWSAccessKeyId' => DEVTAG,
       'AssociateTag' => ASSOCIATEID, 'Request'=>array($request));
       // perform actual soap query
       $result = $soap_proxy->ItemSearch($parameters);
       if(isSOAPError($result)) {
         return false;
       }
       $this->totalResults = $result['TotalResults'];
       foreach($result['Items']['Item'] as $product) {
         $this->products[] = new Product($product);
       }
       unset($soapclient);
       unset($soap_proxy);
     } else {
       // form URL and call parseXML to download and parse it
       $this->url = "http://ecs.amazonaws.com/onca/xml?".
                    "Service=".$this->Service.
                    "&Operation=".$this->Operation.
                    "&AssociateTag=".$this->AssociateTag.
                    "&AWSAccessKeyId=".$this->AWSAccessKeyId.
                    "&BrowseNode=".$this->BrowseNode.
                    "&ResponseGroup=".$this->ResponseGroup.
                    "&SearchIndex=".$this->SearchIndex.
                    "&Sort=".$this->Sort.
                    "&TotalPages=".$this->TotalPages;
      $this->parseXML();
    }
    return $this->products;
  }
  // Given an ASIN, get the URL of the large image
  // Returns a string
  function getImageUrlLarge($ASIN, $mode) {
    foreach($this->products as $product) {
      if( $product->ASIN()== $ASIN) {
       return $product->imageURLLarge();
      }
    }
    // if not found
    $this->ASINSearch($ASIN, $mode);
    return $this->products(0)->imageURLLarge();
  }
  // Perform a query to get a products with specified ASIN
  // Switch between XML/HTTP and SOAP in constants.php
  // Returns a Products object
  function ASINSearch($ASIN, $mode = 'books') {
    $this->type = 'ASIN';
    $this->ASIN=$ASIN;
    $this->mode = $mode;
    $ASIN = padASIN($ASIN);
    $this->Service = "AWSECommerceService";
    $this->Operation = "ItemLookup";
    $this->AWSAccessKeyId = DEVTAG;
    $this->AssociateTag = ASSOCIATEID;
    $this->ResponseGroup = "Large";
    $this->IdType = "ASIN";
    $this->ItemId = $ASIN;
    if(METHOD=='SOAP') {
      $soapclient = new nusoap_client(
      'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl',
      'wsdl'),
      $soap_proxy = $soapclient->getProxy();
      $request = array ('Service' => $this->Service, 'Operation' =>
      $this->Operation, 'ResponseGroup' => $this->ResponseGroup,
      'IdType' => $this->IdType, 'ItemId' => $this->ItemId);
      $parameters = array('AWSAccessKeyId' => DEVTAG,
      'AssociateTag' => ASSOCIATEID, 'Request'=>array($request));
      // perform actual soap query
      $result = $soap_proxy->ItemLookup($parameters);
      if(isSOAPError($result)) {
        return false;
      }
      $this->products[0] = new Product($result['Items']['Item']);
      $this->totalResults=1;
      unset($soapclient);
      unset($soap_proxy);
    } else {
      // form URL and call parseXML to download and parse it
      $this->url = "http://ecs.amazonaws.com/onca/xml?".
                   "Service=".$this->Service.
                   "&Operation=".$this->Operation.
                   "&AssociateTag=".$this->AssociateTag.
                   "&AWSAccessKeyId=".$this->AWSAccessKeyId.
                   "&ResponseGroup=".$this->ResponseGroup.
                   "&IdType=".$this->IdType.
                   "&ItemId=".$this->ItemId;
      $this->parseXML();
    }
    return $this->products[0];
   }
  // Perform a query to get a page full of products with a keyword search
  // Switch between XML/HTTP and SOAP in index.php
  // Returns an array of Products
  function keywordSearch($search, $page, $mode = 'Books') {
    $this->Service = "AWSECommerceService";
    $this->Operation = "ItemSearch";
    $this->AWSAccessKeyId = DEVTAG;
    $this->AssociateTag = ASSOCIATEID;
    $this->ResponseGroup = "Large";
    $this->SearchIndex= $mode;
    $this->Keywords= $search;
    if(METHOD=='SOAP') {
      $soapclient = new nusoap_client(
      'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl',
      'wsdl'),
      $soap_proxy = $soapclient->getProxy();
      $request = array ('Service' => $this->Service, 'Operation' =>
      $this->Operation, 'ResponseGroup' => $this->ResponseGroup,
      'SearchIndex' => $this->SearchIndex, 'Keywords' => $this->Keywords);
      $parameters = array('AWSAccessKeyId' => DEVTAG,
      'AssociateTag' => ASSOCIATEID, 'Request'=>array($request));
      //perform actual soap query
      $result = $soap_proxy->ItemSearch($parameters);
      if(isSOAPError($result)) {
        return false;
      }
      $this->totalResults = $result['TotalResults'];
      foreach($result['Items']['Item'] as $product) {
        $this->products[] = new Product($product);
      }
      unset($soapclient);
      unset($soap_proxy);
    } else {
      $this->url = "http://ecs.amazonaws.com/onca/xml?".
                   "Service=".$this->Service.
                   "&Operation=".$this->Operation.
                   "&AssociateTag=".$this->AssociateTag.
                   "&AWSAccessKeyId=".$this->AWSAccessKeyId.
                   "&ResponseGroup=".$this->ResponseGroup.
                   "&SearchIndex=".$this->SearchIndex.
                   "&Keywords=".$this->Keywords;
      $this->parseXML();
    }
    return $this->products;
  }
  // Parse the XML into Product object(s)
  function parseXML() {
    // suppress errors because this will fail sometimes
    $xml = @simplexml_load_file($this->url);
    if(!$xml) {
      //try a second time in case just server busy
      $xml = @simplexml_load_file($this->url);
      if(!$xml){
        return false;
      }
    }
    &this->totalResults = (integer)$xml->TotalResults;
    foreach($xml->Items->Item as $productXML) {
      $this->products[] = new Product($productXML);
    }
  }
}


This useful class does exactly the sort of thing classes are good for. It encapsulates the interface to Amazon in a nice black box. Within the class, the connection to Amazon can be made either via the REST method or the SOAP method. The method it uses is determined by the global METHOD constant you set in constants.php.

Let’s begin by going back to the Category Search example. You use the AmazonResultSet class as follows:

$ars = new AmazonResultSet;
$ars->browseNodeSearch($parameters['browsenode'],
                       $parameters['page'],
                       $parameters['mode']);

This class has no constructor, so you go straight to that browseNodeSearch() method. Here, you pass it three parameters: the browsenode number you are interested in (corresponding to, say, Business & Investing or Computers & Internet); the page number, representing the records you would like retrieved; and the mode, representing the type of merchandise you are interested in. An excerpt of the code for this method is shown in Listing 33.9.

Listing 33.9  browseNodeSearch()Method—Performing a Category Search


// Perform a query to get a page full of products from a browse node
// Switch between XML/HTTP and SOAP in constants.php
// Returns an array of Products
function browseNodeSearch($browseNode, $page, $mode) {
  $this->Service = "AWSECommerceService";
  $this->Operation = "ItemSearch";
  $this->AWSAccessKeyId = DEVTAG;
  $this->AssociateTag = ASSOCIATEID;
  $this->BrowseNode = $browseNode;
  $this->ResponseGroup = "Large";
  $this->SearchIndex= $mode;
  $this->Sort= "salesrank";
  $this->TotalPages= $page;
  if(METHOD=='SOAP') {
    $soapclient = new nusoap_client(
    'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl',
    'wsdl'),
    $soap_proxy = $soapclient->getProxy();
    $request = array ('Service' => $this->Service,
    'Operation' => $this->Operation, 'BrowseNode' => $this->BrowseNode,
    'ResponseGroup' => $this->ResponseGroup, 'SearchIndex' =>
    $this->SearchIndex, 'Sort' => $this->Sort, 'TotalPages' =>
    $this->TotalPages);
    $parameters = array('AWSAccessKeyId' => DEVTAG,
    'AssociateTag' => ASSOCIATEID, 'Request'=>array($request));
    //perform actual soap query
    $result = $soap_proxy->ItemSearch($parameters);
    if(isSOAPError($result)) {
      return false;
    }
    $this->totalResults = $result['TotalResults'];
    foreach($result['Items']['Item'] as $product) {
      $this->products[] = new Product($product);
    }
    unset($soapclient);
    unset($soap_proxy);
  } else {
    // form URL and call parseXML to download and parse it
    $this->url = "http://ecs.amazonaws.com/onca/xml?".
                 "Service=".$this->Service.
                 "&Operation=".$this->Operation.
                 "&AssociateTag=".$this->AssociateTag.
                 "&AWSAccessKeyId=".$this->AWSAccessKeyId.
                 "&BrowseNode=".$this->BrowseNode.
                 "&ResponseGroup=".$this->ResponseGroup.
                 "&SearchIndex=".$this->SearchIndex.
                 "&Sort=".$this->Sort.
                 "&TotalPages=".$this->TotalPages;
    $this->parseXML();
  }
  return $this->products;
}


Depending on the value of the METHOD constant, this method performs the query via REST or via SOAP. However, the information sent in both requests remains the same. The following lines appear at the beginning of the function and represent the request variables and their values:

$this->Service = "AWSECommerceService";
$this->Operation = "ItemSearch";
$this->AWSAccessKeyId = DEVTAG;
$this->AssociateTag = ASSOCIATEID;
$this->BrowseNode = $browseNode;
$this->ResponseGroup = "Large";
$this->SearchIndex= $mode;
$this->Sort="salesrank";
$this->TotalPages= $page;

Some of these values are set in other parts of the application, such as the values held in $browseNode, $mode, and $page. Other values are constants, such as DEVTAG and ASSOCIATEID. Others, such as the values for $this->Service, $this->Operation, and $this->Sort are static in this implementation.

The minimally required variables differ for each request type; the example above is used for browsing a particular node sorted by sales rank. The variables for a specific item lookup and for a keyword search are different. You can see the list of variables at the beginning of each of the browseNodeSearch(), ASINSearch(), and keywordSearch() functions in the AmazonResultSet.php file. Detailed information on the required variables for all request types can be found in the AWS Developer’s Guide.

Next, we look at the creation of the request in the browseNodeSearch() function for both REST and SOAP queries. The format for the request creation in the ASINSearch() and keywordSearch() functions is conceptually similar.

Using REST to Make a Request and Retrieve a Result

With the set of class member variables already set at the beginning of the browseNodeSearch() function (or ASINSearch() or keywordSearch()), all that remains for using REST/XML over HTTP is to format and send the URL:

$this->url = "http://ecs.amazonaws.com/onca/xml?".
             "Service=".$this->Service.
             "&Operation=".$this->Operation.
             "&AssociateTag=".$this->AssociateTag.
             "&AWSAccessKeyId=".$this->AWSAccessKeyId.
             "&BrowseNode=".$this->BrowseNode.
             "&ResponseGroup=".$this->ResponseGroup.
             "&SearchIndex=".$this->SearchIndex.
             "&Sort=".$this->Sort.
             "&TotalPages=".$this->TotalPages;

The base URL in this case is http://ecs.amazonaws.com/onca/xml. To this, you append the variable names and their values to form a GET query string. Complete documentation on these and other possible variables can be found in the AWS Developer’s Guide. After you set all these parameters, you call

$this->parseXML();

to actually do the work. The parseXML() method is shown in Listing 33.10.

Listing 33.10  parseXML() Method—Parsing the XML Returned from a Query


// Parse the XML into Product object(s)
function parseXML() {
  //suppress errors because this will fail sometimes
  $xml = @simplexml_load_file($this->url);
  if(!$xml) {
    //try a second time in case just server busy
    $xml = @simplexml_load_file($this->url);
    if(!$xml) {
      return false;
    }
  }
  &$this->totalResults = (integer)$xml->TotalResults;
  foreach($xml->Items->Item as $productXML) {
    $this->products[] = new Product($productXML);
  }


The function simplexml_load_file() does most of the work for you. It reads in the XML content from a file or, in this case, an URL. It provides an object-oriented interface to the data and the structure in the XML document. This is a useful interface to the data, but because you want one set of interface functions to work with data that has come in via REST or SOAP, you can build your own object-oriented interface to the same data in instances of the Product class. Note that you cast the attributes from the XML into PHP variable types in the REST version. You do not use the cast operator in PHP, but without it here, you would receive object representations of each piece of data that will not be very useful to you.

The Product class contains mostly accessor functions to access the data stored in its private members, so printing the entire file here is not worthwhile. The stucture of the class and constructor is worth visiting, though. Listing 33.11 contains part of the definition of Product.

Listing 33.11  The Product Class Encapsulates the Information You Have About an Amazon Product


class Product {
  private $ASIN;
  private $productName;
  private $releaseDate;
  private $manufacturer;
  private $imageUrlMedium;
  private $imageUrlLarge;
  private $listPrice;
  private $ourPrice;
  private $salesRank;
  private $availability;
  private $avgCustomerRating;
  private $authors = array();
  private $reviews = array();
  private $similarProducts = array();
  private $soap; // array returned by SOAP calls
  function __construct($xml) {
    if(METHOD=='SOAP') {
      $this->ASIN = $xml['ASIN'];
      $this->productName = $xml['ItemAttributes']['Title'];
      if (is_array($xml['ItemAttributes']['Author']) != " ") {
          foreach($xml['ItemAttributes']['Author'] as $author) {
            $this->authors[] = $author;
          }
      } else {
          $this->authors[] = $xml['ItemAttributes']['Author'];
      }
    $this->releaseDate = $xml['ItemAttributes']['PublicationDate'];
    $this->manufacturer = $xml['ItemAttributes']['Manufacturer'];
    $this->imageUrlMedium = $xml['MediumImage']['URL'];
    $this->imageUrlLarge = $xml['LargeImage']['URL'];
    $this->listPrice = $xml['ItemAttributes']['ListPrice']['FormattedPrice'];
    $this->listPrice = str_replace('$', ' ', $this->listPrice);
    $this->listPrice = str_replace(',', ' ', $this->listPrice);
    $this->listPrice = floatval($this->listPrice);
    $this->ourPrice = $xml['OfferSummary']['LowestNewPrice']['FormattedPrice'];
    $this->ourPrice = str_replace('$', ' ', $this->ourPrice);
    $this->ourPrice = str_replace(',', ' ', $this->ourPrice);
    $this->ourPrice = floatval($this->ourPrice);
    $this->salesRank = $xml['SalesRank'];
    $this->availability =
$xml['Offers']['Offer']['OfferListing']['Availability'];
    $this->avgCustomerRating = $xml['CustomerReviews']['AverageRating'];
    $reviewCount = 0;
    if (is_array($xml['CustomerReviews']['Review'])) {
        foreach($xml['CustomerReviews']['Review'] as $review) {
          $this->reviews[$reviewCount]['Rating'] = $review['Rating'];
          $this->reviews[$reviewCount]['Summary'] = $review['Summary'];
          $this->reviews[$reviewCount]['Content'] = $review['Content'];
          $reviewCount++;
        }
    }
    $similarProductCount = 0;
    if (is_array($xml['SimilarProducts']['SimilarProduct'])) {
        foreach($xml['SimilarProducts']['SimilarProduct'] as $similar) {
          $this->similarProducts[$similarProductCount]['Title'] =
$similar['Title'];
          $this->similarProducts[$similarProductCount]['ASIN'] =
$review['ASIN'];
          $similarProductCount++;
        }
    }
  } else {
    // using REST
    $this->ASIN = (string)$xml->ASIN;
    $this->productName = (string)$xml->ItemAttributes->Title;
    if($xml->ItemAttributes->Author) {
      foreach($xml->ItemAttributes->Author as $author) {
        $this->authors[] = (string)$author;
      }
    }
    $this->releaseDate = (string)$xml->ItemAttributes->PublicationDate;
    $this->manufacturer = (string)$xml->ItemAttributes->Manufacturer;
    $this->imageUrlMedium = (string)$xml->MediumImage->URL;
    $this->imageUrlLarge = (string)$xml->LargeImage->URL;
    $this->listPrice = (string)$xml->ItemAttributes->ListPrice->FormattedPrice;
    $this->listPrice = str_replace('$', ' ', $this->listPrice);
    $this->listPrice = str_replace(',', ' ', $this->listPrice);
    $this->listPrice = floatval($this->listPrice);
    $this->ourPrice = (string)$xml->OfferSummary->LowestNewPrice-
>FormattedPrice;
    $this->ourPrice = str_replace('$', ' ', $this->ourPrice);
    $this->ourPrice = str_replace(',', ' ', $this->ourPrice);
    $this->ourPrice = floatval($this->ourPrice);
    $this->salesRank = (string)$xml->SalesRank;
    $this->availability = (string)$xml->Offers->Offer->OfferListing-
>Availability;
    $this->avgCustomerRating = (float)$xml->CustomerReviews->AverageRating;
    $reviewCount = 0;
    if($xml->CustomerReviews->Review) {
      foreach ($xml->CustomerReviews->Review as $review) {
        $this->reviews[$reviewCount]['Rating'] = (float)$review->Rating;
        $this->reviews[$reviewCount]['Summary'] = (string)$review->Summary;
        $this->reviews[$reviewCount]['Content'] = (string)$review->Content;
        $reviewCount++;
      }
    }
    $similarProductCount = 0;
    if($xml->SimilarProducts->SimilarProduct) {
    foreach ($xml->SimilarProducts->SimilarProduct as $similar) {
      $this->similarProducts[$similarProductCount]['Title'] =
            (string)$similar->Title;
      $this->similarProducts[$similarProductCount]['ASIN'] =
            (string)$similar->ASIN;
      $similarProductCount++;
    }
   }
  }
 }
 // most methods in this class are similar
 // and just return the private variable
 function similarProductCount() {
   return count($this->similarProducts);
 }
 function similarProduct($i) {
   return $this->similarProducts[$i];
 }
 function customerReviewCount() {
   return count($this->reviews);
 }
 function customerReviewRating($i) {
   return $this->reviews[$i]['Rating'];
 }
 function customerReviewSummary($i) {
   return $this->reviews[$i]['Summary'];
 }
 function customerReviewComment($i) {
   return $this->reviews[$i]['Content'];
 }
 function valid() {
   if(isset($this->productName) && ($this->ourPrice>0.001) &&
            isset($this->ASIN)) {
     return true;
   } else {
     return false;
   }
   }
 function ASIN() {
   return padASIN($this->ASIN);
 }
 function imageURLMedium() {
   return $this->imageUrlMedium;
 }
 function imageURLLarge() {
   return $this->imageUrlLarge;
 }
 function productName() {
   return $this->productName;
 }
 function ourPrice() {
   return number_format($this->ourPrice,2, '.', ' '),
 }
 function listPrice() {
   return number_format($this->listPrice,2, '.', ' '),
 }
 function authors() {
   if(isset($this->authors)) {
     return $this->authors;
   } else {
     return false;
   }
 }
 function releaseDate() {
   if(isset($this->releaseDate)) {
     return $this->releaseDate;
   } else {
     return false;
   }
 }
 function avgCustomerRating() {
   if(isset($this->avgCustomerRating)) {
     return $this->avgCustomerRating;
   } else {
     return false;
   }
 }
 function manufacturer() {
   if(isset($this->manufacturer)) {
     return $this->manufacturer;
   } else {
     return false;
   }
 }
 function salesRank() {
   if(isset($this->salesRank)) {
     return $this->salesRank;
   } else {
     return false;
   }
 }
 function availability() {
   if(isset($this->availability)) {
     return $this->availability;
   } else {
     return false;
   }
 }
}


Again, this constructor takes two different forms of input data and creates one application interface. Note that while some of the handling code could be made more generic, some tricky attributes such as reviews have different names depending on the method.

Having gone through all this processing to retrieve the data, you now return control back to the getARS() function and hence back to showBrowseNode(). The next step is

showSummary($ars->products(), $page,
            $ars->totalResults(), $mode,
            $browseNode);

The showSummary() function simply displays the data in the AmazonResultSet, as you can see it all the way back in Figure 33.1. We therefore did not include the function here.

Using SOAP to Make a Request and Retrieve a Result

Let’s go back and look at the SOAP version of the browseNodeSearch() function. This section of the code is repeated here:

$soapclient = new nusoap_client(
  'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl',
  'wsdl'),
$soap_proxy = $soapclient->getProxy();
$request = array ('Service' => $this->Service, 'Operation' => $this->Operation,
  'BrowseNode' => $this->BrowseNode, 'ResponseGroup' => $this->ResponseGroup,
  'SearchIndex' => $this->SearchIndex, 'Sort' => $this->Sort,
  'TotalPages' => $this->TotalPages);
$parameters = array('AWSAccessKeyId' => DEVTAG, 'AssociateTag' => ASSOCIATEID,
  'Request'=>array($request));
// perform actual soap query
$result = $soap_proxy->ItemSearch($parameters);
if(isSOAPError($result)) {
  return false;
}
$this->totalResults = $result['TotalResults'];
foreach($result['Items']['Item'] as $product) {
  $this->products[] = new Product($product);
}
unset($soapclient);

There are no extra functions to go through here; the SOAP client does everything for you.

You begin by creating an instance of the SOAP client:

$soapclient = new nusoap_client(
  'http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl',
  'wsdl'),

Here, you provide the client with two parameters. The first is the WSDL description of the service, and the second parameter tells the SOAP client that this is a WSDL URL. Alternatively, you could just provide one parameter: the endpoint of the service, which is the direct URL of the SOAP Server.

We chose to do it this way for a good reason, which you can see right there in the next line of code:

$soap_proxy = $soapclient->getProxy();

This line creates a class according to the information in the WSDL document. This class, the SOAP proxy, will have methods that correspond to the methods of the Web Service. This makes life much easier. You can interact with the Web Service as though it were a local PHP class.

Next, you set up an array of the request elements you need to pass to the browsenode query:

$request = array ('Service' => $this->Service, 'Operation' => $this->Operation,
  'BrowseNode' => $this->BrowseNode, 'ResponseGroup' => $this->ResponseGroup,
  'SearchIndex' => $this->SearchIndex, 'Sort' => $this->Sort,
  'TotalPages' => $this->TotalPages);

There are two remaining elements you need to pass to the request: AWSAccessKeyID and AssociateTag. These elements, plus the array of elements in $request, are placed in another array called $paremeters:

$parameters = array('AWSAccessKeyId' => DEVTAG, 'AssociateTag' => ASSOCIATEID,
  'Request'=>array($request));

Using the proxy class, you can then just call the Web Service methods, passing in the array of parameters:

$result = $soap_proxy->ItemSearch($parameters);

The data stored in $result is an array that you can directly store as a Product object in the products array in the AmazonResultSet class.

Caching the Data from a Request

Let’s go back to the getARS() function and address caching. As you might recall, the function looks like this:

// Get an AmazonResultSet either from cache or a live query
// If a live query add it to the cache
function getARS($type, $parameters) {
  $cache = cached($type, $parameters);
  if ($cache) {
    // if found in cache
    return $cache;
  } else {
    $ars = new AmazonResultSet;
    if($type == 'asin') {
      $ars->ASINSearch(padASIN($parameters['asin']), $parameters['mode']);
  }
  if($type == 'browse') {
    $ars->browseNodeSearch($parameters['browsenode'], $parameters['page'],
          $parameters['mode']);
  }
  if($type == 'search') {
    $ars->keywordSearch($parameters['search'], $parameters['page'],
          $parameters['mode']);
  }
  cache($type, $parameters, $ars);
}
return $ars;

All the application’s SOAP or XML caching is done via this function. You also use another function to cache images. You begin by calling the cached() function to see whether the required AmazonResultSet is already cached. If it is, you return that data instead of making a new request to Amazon:

$cache = cached($type, $parameters);
if($cache) // if found in cache{
  return $cache;
}

If not, when you get the data back from Amazon, you add it to the cache:

cache($type, $parameters, $ars);

Let’s look more closely at these two functions: cached() and cache(). These functions, shown in Listing 33.12, implement the caching Amazon requires as part of its terms and conditions.

Listing 33.12  cached() and cache() Functions—Caching Functions from cachefunctions.php


// check if Amazon data is in the cache
// if it is, return it
// if not, return false
function cached($type, $parameters) {
  if($type == 'browse') {
    $filename = CACHE.'/browse.'.$parameters['browsenode'].'.'
               .$parameters['page'].'.'.$parameters['mode'].'.dat';
  }
  if($type == 'search') {
    $filename = CACHE.'/search.'.$parameters['search'].'.'
               .$parameters['page'].'.'.$parameters['mode'].'.dat';
  }
  if($type == 'asin') {
    $filename = CACHE.'/asin.'.$parameters['asin'].'.'
               .$parameters['mode'].'.dat';
  }
  // is cached data missing or > 1 hour old?
  if(!file_exists($filename) ||
     ((mktime() - filemtime($filename)) > 60*60)) {
    return false;
  }
  $data = file_get_contents($filename);
  return unserialize($data);
}
// add Amazon data to the cache
function cache($type, $parameters, $data) {
  if($type == 'browse') {
    $filename = CACHE.'/browse.'.$parameters['browsenode'].'.'
               .$parameters['page'].'.'.$parameters['mode'].'.dat';
  }
  if($type == 'search') {
    $filename = CACHE.'/search.'.$parameters['search'].'.'
               .$parameters['page'].'.'.$parameters['mode'].'.dat';
  }
  if($type == 'asin') {
    $filename = CACHE.'/asin.'.$parameters['asin'].'.'
               .$parameters['mode'].'.dat';
  }
  $data = serialize($data);
  $fp = fopen($filename, 'wb'),
  if(!$fp || (fwrite($fp, $data)==-1)) {
    echo ('<p>Error, could not store cache file'),
  }
  fclose($fp);


Looking through this code, you can see that cache files are stored under a filename that consists of the type of query followed by the query parameters. The cache() function stores results by serializing them, and the cached() function deserializes them. The cached() function will also overwrite any data more than an hour old, as per the terms and conditions.

The function serialize() turns stored program data into a string that can be stored. In this case, you create a storable representation of an AmazonResultSet object. Calling unserialize() does the opposite, turning the stored version back into a data structure in memory. Note that unserializing an object like this means you need to have the class definition in the file so that the class is comprehendible and usable once reloaded.

In this application, retrieving a resultset from the cache takes a fraction of a second. Making a new live query takes up to 10 seconds.

Building the Shopping Cart

So, given all these amazing Amazon querying abilities, what can you do with them? The most obvious thing you can build is a shopping cart. Because we already covered this topic extensively in Chapter 28, we do not go into deep detail here.

The shopping cart functions are shown in Listing 33.13.

Listing 33.13  cartfunctions.php—Implementing the Shopping Cart


<?php
require_once('AmazonResultSet.php'),
// Using the function showSummary() in the file bookdisplay.php display
// the current contents of the shopping cart
function showCart($cart, $mode) {
  // build an array to pass
  $products = array();
  foreach($cart as $ASIN=>$product) {
    $ars = getARS('asin', array('asin'=>$ASIN, 'mode'=>$mode));
    if($ars) {
      $products[] = $ars->getProduct(0);
    }
  }
  // build the form to link to an Amazon.com shopping cart
  echo "<form method="POST"
         action="http://www.amazon.com/gp/aws/cart/add.html">";
  foreach($cart as $ASIN=>$product) {
    $quantity = $cart[$ASIN]['quantity'];
    echo "<input type="hidden" name="ASIN.".$ASIN.""
          value="".$ASIN."">";
    echo "<input type="hidden" name="Quantity.".$ASIN.""
          value="".$quantity."">";
    }
    echo "<input type="hidden" name="SubscriptionId"
             value="".DEVTAG."">
          <input type="hidden" name="AssociateTag"
             value="".ASSOCIATEID."">
          <input type="image" src="images/checkout.gif"
                 name="submit.add-to-cart" value="Buy
                 From Amazon.com">
           When you have finished shopping press checkout to add all
           the items in your Tahuayo cart to your Amazon cart and
           complete your purchase.
           /form>
           <br/><a href="index.php?action=emptycart"><img
             src="images/emptycart.gif" alt="Empty Cart"
             border="0"></a>
           If you have finished with this cart, you can empty it
           of all items.
           </form>
           <br />
           <h1>Cart Contents</h1>";
    showSummary($products, 1, count($products), $mode, 0, true);
  }
  // show the small overview cart that is always on the screen
  // only shows the last three items added
  function showSmallCart() {
    global $_SESSION;
    echo "<table border="1" cellpadding="1" cellspacing="0">
          <tr><td class="cartheading">Your Cart $".
         number_format(cartPrice(), 2)."</td></tr>
         <tr><td class="cart">".cartContents()."</td></tr>";
    // form to link to an Amazon.com shopping cart
    echo "<form method="POST"
                action="http://www.amazon.com/gp/aws/cart/add.html">
          <tr><td class="cartheading"><a
                  href="index.php?action=showcart"><img
                  src="images/details.gif"border="0"></a>";
    foreach($_SESSION['cart'] as $ASIN=>$product) {
      $quantity = $_SESSION['cart'][$ASIN]['quantity'];
      echo "<input type="hidden" name="ASIN.".$ASIN.""
               value="".$ASIN."">";
      echo "<input type="hidden" name="Quantity.".$ASIN.""
               value="".$quantity."">";
    }
    echo "<input type="hidden" name="SubscriptionId"
               value="".DEVTAG."">
          <input type="hidden" name="AssociateTag"
               value="".ASSOCIATEID."">
          <input type="image" src="images/checkout.gif"
               name="submit.add-to-cart" value="Buy From
               Amazon.com">
          </td></tr>
          </form>
          </table>";
  }
  // show last three items added to cart
  function cartContents() {
    global $_SESSION;
    $display = array_slice($_SESSION['cart'], -3, 3);
    // we want them in reverse chronological order
    $display = array_reverse($display, true);
    $result = ' ';
    $counter = 0;
    // abbreviate the names if they are long
    foreach($display as $product) {
      if(strlen($product['name'])<=40) {
        $result .= $product['name']."<br />";
      } else {
        $result .= substr($product['name'], 0, 37)."…<br />";
      }
      $counter++;
    }
    // add blank lines if the cart is nearly empty to keep the
    // display the same
    for(;$counter<3; $counter++) {
      $result .= "<br />";
    }
    return $result;
  }
  // calculate total price of items in cart
  function cartPrice() {
    global $_SESSION;
    $total = 0.0;
    foreach($_SESSION['cart'] as $product) {
    $price = str_replace('$', ' ', $product['price']);
    $total += $price*$product['quantity'];
  }
  return $total;
}
// add a single item to cart
// there is currently no facility to add more than one at a time
function addToCart(&$cart, $ASIN, $mode) {
  if(isset($cart[$ASIN] )) {
    $cart[$ASIN]['quantity'] +=1;
  } else {
    // check that the ASIN is valid and look up the price
    $ars = new AmazonResultSet;
    $product = $ars->ASINSearch($ASIN, $mode);
    if($product->valid()) {
      $cart[$ASIN] = array('price'=>$product->ourPrice(),
            'name' => $product->productName(), 'quantity' => 1);
      }
  }
}
// delete all of a particular item from cart
function deleteFromCart(&$cart, $ASIN) {
  unset ($cart[$ASIN]);
}


There are some differences about the way you do things with this cart. For example, look at the addToCart() function. When you try to add an item to the cart, you can check that it has a valid ASIN and look up the current (or at least, cached) price.

The really interesting issue here is this question: When customers check out, how do you get their data to Amazon?

Checking Out to Amazon

Look closely at the showCart() function in Listing 33.13. Here’s the relevant part:

// build the form to link to an Amazon.com shopping cart
echo "<form method="POST"
       action="http://www.amazon.com/gp/aws/cart/add.html">";
foreach($cart as $ASIN=>$product) {
  $quantity = $cart[$ASIN]['quantity'];
  echo "<input type="hidden" name="ASIN.".$ASIN.""
        value="".$ASIN."">";
  echo "<input type="hidden" name="Quantity.".$ASIN.""
        value="".$quantity."">";
}
echo "<input type="hidden" name="SubscriptionId"
         value="".DEVTAG."">
      <input type="hidden" name="AssociateTag"
         value="".ASSOCIATEID."">
      <input type="image" src="images/checkout.gif"
             name="submit.add-to-cart" value="Buy
             From Amazon.com">
      When you have finished shopping press checkout to add all
      the items in your Tahuayo cart to your Amazon cart and
      complete your purchase.
      </form>";

The checkout button is a form button that connects the cart to a customer’s shopping cart on Amazon. You send ASINs, quantities, and your Associate ID through as POST variables. And hey, presto! You can see the result of clicking this button in Figure 33.5, earlier in this chapter.

One difficulty with this interface is that it is a one-way interaction. You can add items to the Amazon cart but cannot remove them. This means that people cannot browse back and forth between the sites easily without ending up with duplicate items in their carts.

Installing the Project Code

If you want to install the project code from this chapter, you will need to take a few steps beyond the norm. After you have the code in an appropriate location on your server, you need to do the following:

image  Create a cache directory.

image  Set the permissions on the cache directory so that the scripts will be able to write in it.

image  Edit constants.php to provide the location of the cache.

image  Sign up for an Amazon developer token.

image  Edit constants.php to include your developer token and, optionally, your Associate ID.

image  Make sure NuSOAP is installed. We included it inside the Tahuayo directory, but you could move it and change the code.

image  Check that you have PHP5 compiled with simpleXML support.

Extending the Project

You could easily extend this project by expanding the types of searches that are available via Tahuayo. For more ideas, check out the links to innovative sample applications in Amazon’s Web Services Resource Center. Look in the Articles and Tutorials section as well as the Community Code section for more information.

Shopping carts are the most obvious thing to build with this data, but they are not the only thing.

Further Reading

A million books and online resources are available on the topics of XML and Web Services. A great place to start is always at the W3C. You can look at the XML Working Group page at http://www.w3.org/XML/Core/ and the Web Services Activity page at http://www.w3.org/2002/ws/ just as a beginning.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.28.247