ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


PHP Web Services Without SOAP

by Adam Trachtenberg, coauthor of PHP Cookbook
10/30/2003

In an earlier article, I outlined the basics of Web services and how you can use PHP and SOAP to talk to Amazon.com. But SOAP isn't the only way a developer can query Amazon's database. The other method is known as REST, which stands for Representational State Transfer.

REST, unlike SOAP, doesn't require you to install a separate tool kit to send and receive data. Instead, the idea is that everything you need to use Web services is already available if you know where to look. HTTP lets you communicate your intentions through GET, POST, PUT, and DELETE requests. To access resources, you request URIs from Web servers. Further, what's wrong with the seemingly umpteen-gazillion already existing ways to process XML? Between SAX, DOM, XSLT, XSL-FO, XPath, XQuery, XPointer, and XWhatever, you should be able to harness at least one of them to turn XML into whatever form you desire.

Therefore, REST advocates claim, there's no need to layer the increasingly complicated SOAP specification on top of these fundamental tools. For more on REST, see the RESTwiki and Paul Prescod's pieces on XML.com.

There may be some community support for this philosophy. While SOAP gets all the press, there are signs REST is the Web service that people actually use. Since Amazon.com has both SOAP and REST APIs, they're a great way to measure usage trends. Sure enough, at OSCon, Jeff Barr, Amazon.com's Web Services Evangelist, revealed that Amazon handles more REST than SOAP requests. I forgot the exact percentage, but Tim O'Reilly blogged this number as 85%! The number I do remember from Jeff's talk, however, is 6, as in "querying Amazon using REST is 6 times faster than with SOAP".

In my previous article, I showed how to query Amazon using SOAP and then print HTML based on the data returned. In this article, I recreate the same results using REST instead of SOAP. This helps with a comparison between the two. If you haven't read the earlier article yet, now is a good time to skim it if you're unfamiliar with the basics of SOAP and the Amazon.Com Web services SOAP API.

The first task is finding the top selling O'Reilly books on Amazon.com. For that, you create an HTTP GET request to Amazon's Web Services URI. In REST, when a URI receives a GET request, it knows to process the request and return data to the client without altering the original information.

But before you can send the request, you need a way to let Amazon know what you're looking for. REST uses the query string to pass along the appropriate parameters, so you need to build it one argument at a time:

$base         = 'http://xml.amazon.com/onca/xml3';
$query_string = '';

$params = array( 'ManufacturerSearch' => "O'Reilly",
    'mode'  => 'books',
    'sort'  => '+salesrank',
    'page'  => 1,
    'type'  => 'lite',
    'f'     => 'xml',
    't'     => 'trachtenberg-20' ,
    'dev-t' => 'XXXXXXXXXXXXXX' ,
);

foreach ($params as $key => $value) { 
    $query_string .= "$key=" . urlencode($value) . "&";
}

$url = "$base?$query_string";

The $params array contains the details of the search: you want to find books published by O'Reilly with the results ordered by salesrank. These parameters are similar (differing in name) to the information provided to Amazon earlier for the SOAP request.

The only new item is f, for format. When you use SOAP, Amazon has to reply with a SOAP packet. When you use REST, Amazon is most likely going to reply with an XML document, but this is less a rule than a guideline. f is set to xml so that's what you get. Later we'll see how to instruct Amazon to reformat the data using XSLT by altering the value of f.

The foreach iterates through $params and constructs a properly formatted and encoded query string. Terms like O'Reilly with its apostrophe must be urlencoded into O%27Reilly or the request won't be valid.

Next you need to send the request and receive Amazon's reply. The file_get_contents() function is the easiest way to do this:

$xml = file_get_contents($url);

Unfortunately, file_get_contents() is only available on PHP 4.3.0 and above. Plus, fopen wrappers need to be enabled to let file_get_contents() access remote files. An alternative method is to use cURL:

$c   = curl_init($url);
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
$xml = curl_exec($c);
curl_close($c);

When CURLOPT_RETURNTRANSFER is set to 1, cURL stores the file as a variable instead of echoing it out.

The XML looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<ProductInfo 
  xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation=
    "http://xml.amazon.com/schemas3/dev-lite.xsd">

    <Request>
        <Args>
        omitted for clarity
        </Args>
    </Request>

   <TotalResults>828</TotalResults>
   <TotalPages>83</TotalPages>
   <Details url="http://www.amazon.com/...">
      <Asin>0596004508</Asin>
      <ProductName>Mac OS X: The Missing Manual...</ProductName>
      <Catalog>Book</Catalog>
      <Authors>
         <Author>David Pogue</Author>
      </Authors>
      <ReleaseDate>October, 2002</ReleaseDate>
      <Manufacturer>O'Reilly & Associates</Manufacturer>
      <ImageUrlSmall>http://images.amazon.com/...</ImageUrlSmall>
      <ImageUrlMedium>http://images.amazon.com/...</ImageUrlMedium>
      <ImageUrlLarge>http://images.amazon.com/...</ImageUrlLarge>
      <Availability>Usually ships within 24 hours</Availability>
      <ListPrice>$29.95</ListPrice>
      <OurPrice>$20.97</OurPrice>
      <UsedPrice>$14.98</UsedPrice>
    </Details>

    insert nine more books here

</ProductInfo>

In order to parse the XML, you need to know its data layout. At the top of the XML, there's a link to Amazon's lite schema. You can also eyeball this document to discover the book-related information is located inside of the <Details> elements below the top-level <ProductInfo> root element.

Now that you have the data stored in $xml, it's time to transform it into something useful. That's probably HTML, but it could be SQL for database storage or RSS for weblog readers. Since this article compares Amazon's SOAP and REST APIs, it's the same HTML from the SOAP piece.

You can use any of PHP's XML parsing utilities to accomplish this task, but I use XSLT here for two reasons. Why? First, Amazon offers XSLT support, so this guarantees everyone can use XSLT. (I'll show how to do this later.) Second, I feel SAX is a tad bulky just to turn XML into HTML. Besides, XSLT exists for exactly this reason; if you not going to use it here, you're never going to use it. (That said, I'm quite sure more than one person is strongly on the side of never using it.)

If you're not familiar with XSLT, Bob DuCharme has a introductory primer on XML.Com. Before getting into the nitty gritty of XSLT processing in PHP, here's the XSL to create the HTML:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="html"/>

  <xsl:template match="/">
    <xsl:apply-templates select="ProductInfo/Details"/>
  </xsl:template>
  
  <xsl:template match="Details">
    <div style="clear:left; width: 300px; padding:5px; 
      margin:5px; background:#ddd;">
    <a href="{@url}"><image src="{ImageUrlSmall}" 
      alt="{ProductName}" align="left"/></a>
    <b><xsl:value-of select="ProductName"/></b><br/>
    <xsl:apply-templates select="Authors"/><br/>
    Amazon.com Price: $<xsl:value-of 
      select="substring(OurPrice, 2)"/><br/>
    </div>    
  </xsl:template>

  <xsl:template match="Authors">
    By 
    <xsl:for-each select="Author">
      <xsl:choose>
        <xsl:when test="not(position() = 1)">
          <xsl:text> and </xsl:text>
        </xsl:when>
      </xsl:choose>
      <xsl:value-of select="."/>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

Without going into too much detail, this breaks down into a few pieces.

After the obligatory set of headers, you create a template that matches /, the root element. Within that template only ProductInfo and Details elements are handed off for processing. Everything else is ignored. This is analogous to the foreach loop on $hits->Details from last time.

The Details template does most of the reformatting. The HTML is embedded within the template. Variable values are pulled out to fill in the changing pieces of data. Things are a tad confusing because variables are accessed in three different ways, as {@url}, {ImageUrlSmall}, and <xsl:value-of select="ProductName">. In brief, the first grabs an attribute of the existing element. The second finds the value of a child of the current element. The last does the same thing, but it's used when you're not filling in a tag's entities.

Unfortunately, processing the authors is a bit more complex. You want to separate each author's name with and. However, since XSLT lacks PHP's join() function, you can't just do:

join(' and ', $hit->Authors)

Instead, you need to write your own version of join(). The Authors template iterates through each Author using a foreach loop. For every author but the first, it adds " and " and prints the author's name by selecting ".", the character data of the current element.

To make things easier for later, this XSLT is stored locally in a file named amazon.xsl and placed directly under the document root.

Now here's the code to use XSLT with PHP. This requires your copy of PHP to have XSLT support. If the XSLT extension isn't enabled, and you're unable to install it, just keep on reading, and you'll see how to solve this problem.

$xsl  = file_get_contents('amazon.xsl');
$xslt = xslt_create();

// map the strings to arguments
$args    = array('/_xml' => $xml, '/_xsl' => $xsl);
$results = xslt_process($xslt, 'arg:/_xml', 'arg:/_xsl', NULL, $args);

xslt_free($xslt);

Just as you loaded the XML file into $xml using file_get_contents(), you also store the XSLT file in $xsl. Then you instantiate an instance of the XSLT processor by calling xslt_create().

When you call xslt_process(), the first argument is the resource handle. The second through fourth arguments are the XML, XSLT, and output files, respectively. Passing in NULL as the output filename makes xslt_process() return the transformed document, where it's stored in $results. The final argument is the $args array. By default, xslt_process() loads documents from the file system; using $args instructs the processor to access data stored in the PHP variables $xml and $xsl instead.

Here are the results:

reformatted book data
Figure 1. Reformatted book data from the Amazon database

If your copy of PHP doesn't support XSLT, all is not lost. Amazon.com makes an XSLT service available, which you can use instead. It's easy to do.

First, place the stylesheet in a publicly accessible place on your Web server, such as http://www.example.com/amazon.xsl. Next, instead of assigning the f parameter in your REST request to xml, set it to the URL of your XSLT document. This causes Amazon to request the stylesheet, pass it to its XSLT processor, and return the transformed file to you instead of the XML.

$base         = 'http://xml.amazon.com/onca/xml3';
$query_string = '';

$params = array( 'ManufacturerSearch' => "O'Reilly",
    'mode' => 'books',
    'sort' => '+salesrank',
    'page' => 1,
    'type' => 'lite',
    'f'    => 'http://www.example.com/amazon.xsl',
    't'    => 'trachtenberg-20',
    'dev-t'=> 'XXXXXXXXXXXXXX',
); 

foreach ($params as $k => $v) { 
    $query_string .= "$k=" . urlencode($v) . "&";
}

$url = "$base?$query_string";

$results = file_get_contents($url);

Foisting your XSLT onto Amazon drastically simplifies your PHP. There's no need to manually invoke the XSLT program or even touch the XML. On the other hand, you're forced to use XSLT, which some might claim is a faustian bargain.

To make a more complex search, such as finding books published by O'Reilly on PHP, turn ManufacturerSearch into PowerSearch and update the search terms:

'PowerSearch' => "publisher:O'Reilly AND keywords:PHP"

This one line change filters our results down to four books:

A filtered search
Figure 2. A filtered search

The REST PowerSearch parameter accepts all the syntactical variations as its SOAP counterpart. That makes it just as powerful and easy for you to port your query logic from SOAP to REST and vice versa.

That's a quick tour of using Amazon.Com's REST Web services API. Unlike SOAP, an additional extension isn't necessary, nor is there all the associated overhead that comes from converting data structures from PHP to XML and back again. Instead, you have a leaner process that increases transparency and efficiency. However, that comes at the expense of requiring additional lines of code. Plus, you need to understand more about using HTTP and XML processing.

That can be a plus, not a minus. Once you have a good grasp of the basics, debugging REST is simple, since you can easily understand what's going on. In contrast, with SOAP, tracking down a bug can be a complicated mess, as you trace through SOAP packets and specifications.

Adam Trachtenberg is the manager of technical evangelism for eBay and is the author of two O'Reilly books, "Upgrading to PHP 5" and "PHP Cookbook." In February he will be speaking at Web Services Edge 2005 on "Developing E-Commerce Applications with Web Services" and at the O'Reilly booth at LinuxWorld on "Writing eBay Web Services Applications with PHP 5."

PHP Cookbook

Related Reading

PHP Cookbook
By David Sklar, Adam Trachtenberg


Return to the PHP DevCenter.

Copyright © 2009 O'Reilly Media, Inc.