772 Chapter 31 Connecting to Web Services with XML and SOAP After all these parameters are set, we call $this->parseXML(); to actually do the work.The parseXML() method is shown in Listing 31.10. Listing 31.10 parseXML() Method—Parsing the XML Returned from a Query // Parse the XML into Product object(s) function parseXML() { $xml_parser = xml_parser_create(); xml_parser_set_option($xml_parser,XML_OPTION_SKIP_WHITE,1); xml_set_object($xml_parser, $this); xml_set_element_handler($xml_parser, "startElementHandler", "endElementHandler"); xml_set_character_data_handler($xml_parser, 'cdataHandler'); if (!($fp = fopen($this->_url, "r"))) { die("could not open XML input"); } while ($data = fread($fp, 4096)) { if (!xml_parse($xml_parser, $data, feof($fp))) { die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($xml_parser)), xml_get_current_line_number($xml_parser))); } } xml_parser_free($xml_parser); } In this method we are using PHP’s XML library, which is based on expat. We begin by calling the xml_parser_create() function.This creates an instance of the parser and returns a handle to this parser, which will need to be passed to other func- tions. We then need to set up a few things in this parser before we can actually parse the XML document, as follows: xml_parser_set_option($xml_parser,XML_OPTION_SKIP_WHITE,1); xml_set_object($xml_parser, $this); xml_set_element_handler($xml_parser, "startElementHandler", "endElementHandler"); xml_set_character_data_handler($xml_parser, 'cdataHandler'); 37 525x ch31 1/24/03 3:35 PM Page 772 773 Solution Overview The first line sets the parser to skip whitespace (which will save effort).There are a few other options that can be set with this function, but this is the only one relevant here. The next three lines are used to set up callback functions.We need to tell the parser what to do when it finds elements and character data inside the XML document.The call to xml_set_object() tells the parser that the callback functions will be found inside $this, the current AmazonResultSet object. (If we were just using regular functions rather than class methods, we would not need this function call. ) The call to xml_set_element_handler() tells the parser that whenever it finds the beginning of an element, it should call the method named startElementHandler(), and when it finds the end of an element, it should call the method named endElementHandler().These are yet more methods we have written inside the AmazonResultSet object.These methods are called callbacks, and their signature must fol- low a certain format. The startElementHandler() method must accept three parameters: a reference to the parser, the name of the element that has just started, and an array of the attributes of the element. The endElementHandler() method must accept two parameters: a reference to the parser, and the name of the element that is ending. The call to xml_set_character_data_handler() tells the parser what to do when it encounters character data in the XML document.This will occur when the parser tries to process the actual contents of an element. It should call the method called cDataHandler(), also located in the AmazonResultSet class. Again, this method must have a certain signature. In this case, it must accept two parameters: a reference to the parser, and the character data as a string.We’ll look at the content of these callbacks in a moment. After setting up all these parameters, we open the file at the URL we received a minute ago, and parse the XML we find there.The parser will work its way through the document, invoking the callback functions as it goes. Finally, we need to clean up after ourselves by deleting the parser: xml_parser_free($xml_parser); Now, let’s see what happens in those callback functions.The three functions are shown in Listing 31.11. Listing 31.11 Callback Functions // function to catch callbacks when the XML parser reaches the start of // a new element function startElementHandler($parser, $name, $attributes) { array_push($this->_names, $name); if($name=='DETAILS') { 37 525x ch31 1/24/03 3:35 PM Page 773 774 Chapter 31 Connecting to Web Services with XML and SOAP $this->_currentProduct = new Product(); } if($name == 'BROWSENODE') { $this->_currentProduct->_currentBrowseName++; } if($name == 'CUSTOMERREVIEW') { $this->_currentProduct->_currentReview++; } } // function to catch callbacks when the XML parser has data from // an element function cdataHandler($parser, $cdata) { $this->_currentName = array_slice($this->_names, -1, 1); $this->_currentName = $this->_currentName[0]; switch($this->_currentName) { case 'TOTALRESULTS' : $this->_totalResults = $cdata; break; case 'DETAILS' : break; case 'AUTHOR' : $this->_currentProduct->authors[] = $cdata; break; case 'RATING' : case 'SUMMARY' : case 'COMMENT' : @$this->_currentProduct->customerReviews[$this->_currentProduct->_ currentReview][$this->_currentName] .= $cdata; // fields that may contain returns and &s need to be concatenated // concatenation will give a notice if they are enabled - //hence the @ break; case 'LISTID' : $this->_currentProduct->listIDs[] = $cdata; break; Listing 31.11 Continued 37 525x ch31 1/24/03 3:35 PM Page 774 775 Solution Overview case 'BROWSENAME' : @$this->_currentProduct-> browseNames[$this->_currentProduct->_currentBrowseName] .= $cdata; // fields that may contain returns and &s need to be concatenated // concatenation will give a notice if they are enabled - // hence the @ break; case 'PRODUCT' : $this->_currentProduct->similarProducts[] = $cdata; break; // there are certain keys we are dealing with the children of // separately so can ignore case 'CUSTOMERREVIEW' : case 'AUTHORS' : case 'BROWSELIST' : case 'BROWSENODE' : case 'LISTS' : case 'REVIEWS' : case 'SIMILARPRODUCTS' : //do nothing break; default : @$this->_currentProduct->nodes[$this->_currentName] .= $cdata; break; } } // function to get callbacks when the XML parser reaches the end of an // element function endElementHandler($parser, $name) { if($name=='DETAILS') { //these are no longer required unset($this->_currentProduct->_currentReview); unset($this->_currentProduct->_currentBrowseName); array_push($this->_products, $this->_currentProduct); } array_pop($this->_names) ; } Listing 31.11 Continued 37 525x ch31 1/24/03 3:35 PM Page 775 776 Chapter 31 Connecting to Web Services with XML and SOAP By reading through this code, you will see that these three functions populate the class member array $_products with the information contained in the XML document.This array consists of instances of the Product class, which we have created specifically to hold the details of an item.The code for the Product class is shown in Listing 31.12. Listing 31.12 Part of Product.php—Storing Data on an Individual Item <?php // This class holds data about a single amazon product // scalar attributes are all stored in the array nodes // attributes from the XML document that require special // treatment have their own array // if the data came via XML/HTTP, most of the data will be in $this->nodes // if the data came via SOAP, _ALL_ of the data will be in $this->soap // This class' main purpose is to provide a common interface to the data // from these two sources so all the display code can be common class Product { var $nodes = array(); var $authors = array(); var $listIDs = array(); var $browseNames = array(); var $customerReviews = array(); var $similarProducts = array(); var $_currentReview = -1; var $_currentBrowseName = -1; var $soap; // array returned by SOAP calls // most methods in this class are similar // return the XML variable or the SOAP one // a single Product instance will only have one or the other function similarProductCount() { if($this->soap) return count($this->soap['SimilarProducts']); else return count($this->similarProducts); } This class consists almost entirely of accessor (get and set) functions, so we will not dwell on it here.The main reason this class exists is so that we can conveniently store data from two different sources and access it through the same interface.This means we can take the default output from NuSOAP and an easy-to-parse result from the XML and store 37 525x ch31 1/24/03 3:35 PM Page 776 . required unset($this->_currentProduct->_currentReview); unset($this->_currentProduct->_currentBrowseName); array_push($this->_products, $this->_currentProduct); } array_pop($this->_names). $cdata) { $this->_currentName = array_slice($this->_names, -1 , 1); $this->_currentName = $this->_currentName[0]; switch($this->_currentName) { case 'TOTALRESULTS' : $this->_totalResults = $cdata; break; case. $this); xml_set_element_handler($xml_parser, "startElementHandler", "endElementHandler"); xml_set_character_data_handler($xml_parser, 'cdataHandler'); if (!($fp = fopen($this->_url,