Consuming XML

Let's return to the topic of XML. This time, the XML is both served and consumed by PHP. When programming web services, it is common to produce XML output as well as get XML as input. One common example of such an implementation is the SOAP protocol, which isn't covered in this text. Instead of taking a normal query string, we take a full XML document as input. This is a reversal of what we have done thus far. In addition to generating the XML in PHP and reading it in JavaScript, we will do the exact opposite as well.

Another important topic to take into account is handling multiple query terms simultaneously and joining them together. First we need to go back to the design phase and determine how we to format our requests.

Designing the XML Request

We have two search fields to keep in mind when designing our XML request: the title and the author.

As promised, multiple searches can be executed simultaneously. This isn't particularly useful in our application; however, it could be useful in future web application development.

<request>
       <query>
              <title>PHP</title>
              <author>Andrew</author>
       </query>
 </request>

If multiple query nodes are present then the searches will each be executed sequentially. However, now we also need to redesign our XML response because the old response was suitable only for one set of results:

<?xml version="1.0" ?>
<catalog>
       <query title="PHP" author="Andrew">
              <book>
                     <Title>Ajax with PHP 5</Title>
                     <Author>Andrew Curioso</Author>
                     <Year>2007</Year>
                     <Publisher>O'Reilly Media</Publisher>
              </book>
       </query>
</catalog>

The new response format has all these listed abilities. In addition, it also echoes back the request so that the client can then sort the results appropriately. To transmit the query terms we use a method that hasn't been seen here yet. They are each included as attributes in the query tag. We can only have one attribute with each name inside a tag. However, that is not a problem because each query in our system can have one and only one title and author.

The PHP File

This example changes our situation around a little. For the first time we have to parse and interpret XML information using PHP and not just JavaScript. Fortunately for us, PHP has a library of XML-related functions that helps.

Parsing XML the easy way with SimpleXML

As of PHP 5, there is a new extension that makes XML much easier to parse called SimpleXML. This extension is enabled by default in PHP 5. There is only one line of code that we really need to worry about in this example:

$xml = new SimpleXMLElement($xmldata);

This line will construct a new SimpleXML object that allows us to loop through the document. Each document node will be present in an array in the object.

Parsing XML the harder way with XMLParser class

Since we already have an XMLPrinter class it would be nice if we could create a class that reverses the process. In this case, we can use the output of XMLPrinter as the input for XMLParser and vice versa. This feature is not necessary for the example; however, it lessens development time and reduces confusion at the same time. With this scheme the data can be constantly translated to produce other representations of itself in different formats without losing any information (see Figure 3).

Lifecycle of data using XMLParser and XMLPrinter

Figure 3. Lifecycle of data using XMLParser and XMLPrinter

The output of each class can be used as the input of the other.

PHP handles XML via a series of callback functions (functions that are called automatically when a part of the document has been read). This makes it relatively easy to parse the data but it is also fundamentally different than the way JavaScript handles XML. There are four functions that matter here:

  • xml_parser_create()

    Creates the XML parser object that we use to read and interpret the XML data.

  • xml_set_element_handler()

    Sets two callback functions. One handles the start of an XML tag (element) and the other handles the ending tag of a node.

  • xml_set_character_data_handler()

    Sets a callback function that is called when plain text (character) data is found in-between tags.

  • xml_parse()

    Does the actual parsing of the XML data. It calls the various callback functions we define.

  • xml_parser_free()

    Frees the memory allocated by the xml_parser_create() method.

All four functions are available in PHP 4 and 5. The class we build can be considered a wrapper class for these functions and can take generic XML data. But first, make a simple class to illustrate the functions. This class prints out a report of the XML query data.

<?php
class XMLBookQueryParser
{
       private $depth, $curtag, $queries;
       private function OnStartElement($parser, $tag, $attrs)
       {
              if ( $this->depth <= 0 && $tag != "request" )
                     throw new Exception("Invalid root node. Expected 'request'");
              else if ( $this->depth == 1 && $tag != "query" )
                     throw new Exception("Invalid tag. Expected 'query'");
              else if ( $this->depth == 1 )
                     $this->queries[] = array();
              else if ( $this->depth == 2 && $tag == "title" || $tag == "author" )
                     $this->curtag = $tag;
              else if ( $this->depth == 2 )
                     throw new Exception("Invalid tag. Expected 'title' or 'author'");
              else if ( $this->depth == 3 )
                     throw new Exception("Invalid request. XML tree is too deep");
              $this->depth++;
       }
       function OnEndElement($parser, $tag) {
                     $this->depth--;
                     $curtag = "";
       }
       function OnCharacterData($parser, $data) {
              if ( $this->curtag != "title" && $this->curtag != "author" ) return;
              $i = count($this->queries)-1;
              $this->queries[$i][$this->curtag] .= trim($data);
       }
       public static function ParseXML($xmlData)
       {
              $eventHandler = new XMLBookQueryParser();
              $eventHandler->queries = array();
              $parser = xml_parser_create();
              xml_set_object($parser, $eventHandler);
              xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
              xml_set_element_handler( $parser, "OnStartElement", "OnEndElement" );
              xml_set_character_data_handler($parser, "OnCharacterData");
              xml_parse($parser,$xmlData);
              xml_parser_free($parser);
              return $eventHandler->queries;
       }

}
?>

The script takes an XML file that encapsulates a valid book query (like the one we designed earlier) and returns the query as an array. There are a few parts that we should pay specific attention to.

The ParseXML() static method is entirely for initialization and cleanup of the parser. Because we can not directly use a static method as a callback function, we must create a dynamic instance of the class. That's happening on the first line of the method. We then initialize variables, set the callback functions, and fire the xml_parse() function. This function will iterate through the document and call the appropriate callback functions where needed. Once the parser finishes, we free the parser and return the results.

Although this class works well, it has a few drawbacks. One is that it is extremely specific. Any file that deviates from the design won't be handled properly. In this case, any invalid data produces an exception that must be caught. This can be a good thing because we ensure that absolutely no data that isn't part of our design makes its way onto the server. The problem arises if we later add to our XML design. Let's say we wanted to add the ability to query by publication year. This small change requires the class to be modified. Another problem is that if someone else is providing the data we may have elements that are not entirely in our control. For instance, some sites providing RSS (a form of content syndication based on XML) feeds append their own custom data to the feeds.

Instead of leaving the messy work for future implementers of the class, let's create a new XML parser class to handle the work for us:

<?php
include_once("XMLNode.class.php");

class XMLParser
{
       private $rootNode, $curNode, $stack;

       private function OnStartElement($parser, $tag, $attrs)
       {
              if ( !isset($this->curNode) ) {
                     $node = new XMLNode($tag,$attrs);
                     $this->rootNode = &$node;
                     $this->curNode = &$node;
                     echo "eek<br>";
              } else {
                     $this->stack[] = $this->curNode;
                     $newNode = $this->curNode->createChild($tag,$attrs);
                     unset($this->curNode);
                     $this->curNode = $newNode;
                     }
       }
       private function OnEndElement($parser, $tag) {
              unset($this->curNode);
              $this->curNode = array_pop($this->stack);
       }
       private function OnCharacterData($parser, $string)
       {
              $string = trim($string);
              if ( strlen($string) != 0 )
                     $this->curNode->text .= $string;
       }
       public static function ParseXML($xmlData)
       {
              $eventHandler = new XMLParser();
              $eventHandler->stack = array();
              $parser = xml_parser_create();
              xml_set_object($parser,$eventHandler);
              xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
              xml_set_element_handler( $parser, "OnStartElement", "OnEndElement" );
              xml_set_character_data_handler($parser, "OnCharacterData");
              xml_parse($parser,$xmlData);
              xml_parser_free($parser);
              return $eventHandler->rootNode;
       }
}
?>

This class is the most complex class in this Short Cut due to the use of references. Don't be intimidated by it, because, like all the helper classes here, it is not necessary to understand all the details of how it works. (If you understand how this class works, you can decipher almost any PHP class.) Designing a generic class like this is more often than not much more difficult than writing a specialized class to parse just books. However, we can now use this class in any of our scripts without having to reinvent it every time.

Side Note

References are the closest thing PHP has to pointers. Using references you can bind one variable to another. A common pitfall is that assigning a value to a variable that is already referencing another variable will change the value of the reference variable. This behavior is often not what a programmer who is accustomed to pointers in other languages would expect. For this reason, when setting curNode in the earlier code we first call unset($this->curNode).

The class builds on the concept of a stack (a last-in first-out data structure). A node is added to the tree when a new element is encountered. A pointer to that node (a reference to its location in memory) is then pushed onto the top of the stack. When the element is closed (a closing tag is encountered) the memory location is popped off the top. Stacks are particularly useful for examining XML trees because XML tags must always be closed in the opposite order from which they were opened.

The problem with custom classes like the one in this section is that they tend to be slower than the PHP native implementation and they obviously take more time to develop. However, they do have some advantages. Mainly, in this case, by using XMLParser instead of SimpleXML we gain operability with the other classes in this Short Cut. In addition, XMLPrinter degrades to PHP 4 much more gracefully. We'll show you both methods in action later. Which one you use is a matter of preference. You should do more research into the use of SimpleXML to gain a comprehensive knowledge of PHP.

The Search Form

Now that we have created a class to parse our XML data, we can create a search form to send the query display the results. Rather than create a simple form without any interactivity, we inject slightly more DHTML. We want a search box wherein you can type in your query (just like before), with two options this time. One option performs the search and the other adds the search term to a list so you can perform multiple queries at a time.

Note

PHP treats field names with [ and ] brackets as an array. Fields must follow the same naming conventions as arrays do in PHP.

Name our fields and table rows sequentially so that both PHP (for our unobtrusive design) and JavaScript can interpret the data. First, create the PHP file:

<?php
include_once("XMLPrinter.class.php");
include_once("XMLParser.class.php");
include_once("BookLoader.class.php");

if ( !isset($HTTP_RAW_POST_DATA) )
       $HTTP_RAW_POST_DATA = file_get_contents("php://input");

if ( strpos($HTTP_RAW_POST_DATA,"<request>") !== false ) {
       header("Cache-Control: no-cache, must-revalidate");
       header("Content-type: text/xml");

       $rootNode = new XMLNode("catalog");

       // START XML PARSING
       $request = XMLParser::ParseXML($HTTP_RAW_POST_DATA);
       while ( $query = $request->getChild("query") ) {
              $title = $query->getChild("title")->text;
              $author = $query->getChild("author")->text;

              $queryNode =
              $rootNode->createChild("query",array("title"=>$title,
"author"=>$author));


$queryNode->buildChildrenFromArray( "book",
                           BookLoader::Search($title,$author) );
       }
       // END XML PARSING

       XMLPrinter::PrintFromNode( $rootNode );
} else { ?>
<html>
<head>
       <title>Advanced Book search with XML</title>
       <script src="searchXML2.js"></script>
</head>
<body>
       <form method="post" onSubmit="return SubmitQuery()" id="searchForm">
       <table id="formTable">
         <tr><th>Title</th><th>Author</th></tr>
         <tr id="r1">
              <td><input type="text" name="query[r1][title]" value="<?=
 $_POST["query"]["r1"]["title"] ?>" id="r1title"></td>
           <td><input type="text" name="query[r1][author]" value="<?=
 $_POST["query"]["r1"]["author"] ?>" id="r1author"></td>
         </tr>
         <?php
         $i = 2;
         if ( count( $_POST["query"]) > 0 )
              foreach ( $_POST["query"] as $key => $query ) {
                if ( $key == "r1" && $_POST["Submit"] != "Add" ) continue;
              ?>
           <tr id="r<?= $i ?>">
                <td><input type="hidden" name="query[<?= $key ?>][title]"
 value="<?= $query["title"] ?>" id="<?= $key ?>title"> <?=
 $query["title"] ?></td>
             <td><input type="hidden" name="query[<?= $key ?>][author]"
 value="<?= $query["author"] ?>" id="<?= $key ?>author"> <?=
 $query["author"] ?></td>
           </tr>
         <?php
              $i++;
         } ?>
         <tr><td align="right" colspan="2">
           <input onClick="btnPressed=this.value" type="Submit" value="Add"
 name="Submit" id="Add">
              <input onClick="btnPressed=this.value" type="Submit"
 value="Search" name="Submit" id="Submit">
              </td></tr>
       </table>
       </form>
       <table id="data" border="1">
       <tr>
              <th>Title</th>
              <th>Author</th>
              <th>Year</th>
              <th>Publisher</th>
       </tr>
       <?php
       if ( count($_POST["query"]) > 0 && $_POST["Submit"] != "Add" )
       {
              foreach ( $_POST["query"] as $query ) {
              ?>
       <tr>
              <th colspan="4">Search for: <?= "$query[title] $query[author]" ?></td>
       </tr>
              <?php
                     $results = BookLoader::Search($query["title"],$query["author"]);
                     foreach ( $results as $book )
                     { ?>
       <tr>
              <td><?= $book["Title"] ?></td>
              <td><?= $book["Author"] ?></td>
              <td><?= $book["Year"] ?></td>
              <td><?= $book["Publisher"] ?></td>
       </tr>
              <?php
                 }
              }
       }
       ?>
       </table>
</body>
</html>
<?php } ?>

This file makes ample use of four of the classes that we built in previous sections (XMLParser, XMLPrinter, XMLNode, and BookLoader). Fortunately for us, we use this script for both this example and the JSON one (with minor modifications).

One feature of this new page is that we are no longer sending our request via the GET method. You may notice that there isn't any if ( count($_GET) > 0 ) line anymore. Instead we check to see if there was raw data posted. If that variable is present then we know that it is an Ajax request. Traditionally, you would get the posted data via $_POST. However, when making the Ajax request we can do something a little different.

PHP has several methods to get the raw data sent to the server. Because there are some discrepancies between PHP versions and different configurations, combine two of them to form a method that should work on any PHP configuration:

if ( !isset($HTTP_RAW_POST_DATA) )
       $HTTP_RAW_POST_DATA = file_get_contents("php://input");

The $HTTP_RAW_POST_DATA variable will hold our XML request.

The rest should look familiar. If there isn't any data posted or that data is in the wrong format we will display the normal page with the search form and any possible results. If there was an XML/Ajax request made, then we generate the appropriate XML response.

Also, the form has two submit buttons: one of which adds a new value to the list and the other performs the search. To see which button was pressed via JavaScript, we assign a variable using the OnClick handler of each button.

This script is entirely unobtrusive. There will be no loss of functionality if JavaScript isn't enabled. Save the script using a name of your choice and try the search. It is not the most eloquent solution but it works as you would expect. Now that the PHP and HTML is done, it's time to write the DHTML and Ajax to make the form interactive.

We briefly went over a built-in PHP class called SimpleXML. Let's take a look at how the same task can be accomplished using it.

Adapting the form to use SimpleXML

If you look closely at the PHP file we just created, you will see two comments that say "START XML PARSING" and "END XML PARSING." Now we will look at the alternative way of accomplishing our goal using SimpleXML. Replace that entire block of code with:

       // START XML PARSING
       $request = new SimpleXMLElement($HTTP_RAW_POST_DATA);
       foreach ( $request->query as $query ) {
              $title = $query->title;
              $author = $query->author;

              $queryNode =
              $rootNode->createChild("query",array("title"=>$title,
"author"=>$author));


              $queryNode->buildChildrenFromArray( "book",
                           BookLoader::Search($title,$author) );
       }
       // END XML PARSING

I changed only three lines of code in this example. You should add error checking to ensure that the nodes actually exists, otherwise you will get undesirable results. Without error checking there is a one-to-one relationship between the number of method calls needed with SimpleXML and the number needed with XMLParser. The largest difference in functionality is that XMLParser is more traditional—the loop structure needed to iterate through operators is similar to what you see in JavaScript—while SimpleXML allows the use of built-in PHP constructs and control structures such as foreach.

In addition, with XMLPrinter the PHP interpreter needs to compile the class file when necessary. This is an overhead that is avoided with SimpleXML. Incidentally, if you use the SimpleXML version, you don't have to include XMLPrinter.class.php at all.

SimpleXML can also be used to generate XML data. However, since XMLPrinter conveniently takes our search results from BookLoader as input, we'll stick with that for formatting our output. To learn more about SimpleXML see http://us.php.net/manual/en/ref.simplexml.php.

Making the Search Form Interactive

The first step to making our search form is to make our "Add" button insert a new search term without reloading the page. It would seem counterintuitive to have the ability to get search results without reloading but require a reload to add a query. To do this we use this code:

function AddToList()
{
       var authorField = document.getElementById("r1author");
       var titleField = document.getElementById("r1title");

       if ( authorField.value == "" && titleField.value == "" ) {
              alert("Please enter a value to search for.");
              return;
       }

       var table = document.getElementById("formTable");
       var newid= table.getElementsByTagName("tr").length-1;

       var row = table.insertRow(newid);
       row.id = "r"+newid;

       row.insertCell(0).innerHTML =
       "<input type="hidden" name="query[r"+newid+"][title]" value=""+
       titleField.value + "" id="r"+newid+"title">" + titleField.value;

       row.insertCell(1).innerHTML =
       "<input type="hidden" name="query[r"+newid+"][author]" value=""+
       authorField.value + "" id="r"+newid+"author">" + authorField.value;

       authorField.value = "";
       titleField.value = "";
}

We insert a call to this function into the OnSubmit handler for the form after we write some more code to go with. The function gets the relevant DOM elements from the page and then inserts a new row. In that row, we have two cells, each with a hidden field in it: one for the title and a second for the author. This JavaScript generates a page that is identical to the PHP version that would be generated server-side.

Generating XML Requests with JavaScript

Now we can concentrate on generating the XML and the Ajax code. First, generate the request. To do this, loop through every row of the search form table and fetch the values from it. We also fetch the values from the text form fields themselves. Using these values generate the query string:

function BuildQuery()
{
       var queryStr = "<request>";
       var table = document.getElementById("formTable");
       var rows= table.getElementsByTagName("tr");
       for ( var i=1; i<rows.length-1; i++ )
       {
              queryStr = queryStr +
                           "<query><title>" +
                           document.getElementById(rows[i].id+"title").value +
                           "</title><author>" +
                           document.getElementById(rows[i].id+"author").value +
                           "</author></query>";
       }
       queryStr = queryStr + "</request>";

       return queryStr;
}

To do this, each row is named sequentially and the IDs of the appropriate input fields consist of the ID of the row followed by "title" or "author" making them easy to find. Now that we have a query string, generate and make the Ajax request. As mentioned earlier, this time we make the request via the POST method instead of GET. This requires a little extra work:

function SubmitAjaxQuery()
{
       if ( httpObj != null ) return;
       document.getElementById("Submit").disabled = true;
       document.getElementById("Add").disabled = true;
       var table = document.getElementById("data");
       while ( table.rows.length > 1 ) table.deleteRow(1);
       var cell = table.insertRow(1).insertCell(0);
       cell.colSpan = 4;
       cell.innerHTML = "Loading";

       httpObj = NewHTTP();
       httpObj.open("POST","?",true);
       httpObj.setRequestHeader('Content-Type','text/xml'),
       httpObj.onreadystatechange = OnData;
       httpObj.send(BuildQuery());
}

There are three significant differences in this function than the one used before, we:

  • Used the POST method instead of GET in httpObj.open().

  • Sent (POST) the XML data instead of null when calling httpObj.send(): The data is generated via the BuildQuery() function from earlier.

  • Set the content-type: by doing this, we tell the web server to expect text/xml encoded data (which happens to be exactly what we are sending it).

We changed the structure of our response slightly to accommodate multiple results. Our old XML parsing code can't quite handle the new format, so it will have to be changed slightly. The code to loop through the books stays the same, but we must now handle the additional query tag that encapsulates the books. The query tag has attributes used to display the information about the query that produces every set of results. Our rewritten OnData handler is slightly longer than the old one, but most of the code should look familiar:

function OnData()
{
       if ( httpObj.readyState==4 ) {
              var errorText = null;
              var table = document.getElementById("data");
              table.deleteRow(1);

              if (httpObj.status==200 && httpObj.responseXML != null ) {
                     var queries =
                     httpObj.responseXML.documentElement.getElementsByTagName
("query");
                     var curRow = 1;
                     if ( queries.length == 0 ) {
                           errorText = "Error. No queries found.";
                     } else for ( var i=0; i<queries.length; i++ ) {
                           var query = queries.item(i);
                           var row = table.insertRow(curRow);
                           curRow++;
                           row.innerHTML =
                             "<td colspan=4>Search for: "+
                             query.getAttribute("title") + " " +
                             query.getAttribute("author") + "</td>";

                           var books = query.getElementsByTagName("book");
                           for ( var j=0; j<books.length; j++ ) {
                                  var book = books.item(j);
                                  var row = table.insertRow(curRow);
                                  curRow++;
                                  row.insertCell(0).innerHTML =
                                                       FetchValue(book,"Title");
                                  row.insertCell(1).innerHTML =
                                                       FetchValue(book,"Author");
                                  row.insertCell(2).innerHTML =
                                                       FetchValue(book,"Year");
                                  row.insertCell(3).innerHTML =
                                                       FetchValue(book,"Publisher");
                           }
                     }
              } else if (httpObj.responseXML == null ) {
                     errorText = "Invalid search results";
              } else {
                     alert("Error: Could not get search results.");
                     errorText = "Sorry, the search service is unavailable.";
              }
              httpObj = null;
              document.getElementById("Submit").disabled = false;
              document.getElementById("Add").disabled = false;
              if ( errorText != null ) {
                     var row = table.insertRow(1);
                     var cell = row.insertCell(0);
                     cell.colSpan = 4;
                     cell.innerHTML = errorText;
              }
       }
}

Note that the method getAttribute() is used on a few occasions. This method fetches the attribute associated with a DOM/XML node. Aside from that, the code is the same as the other XML code parsing in JavaScript we saw earlier; only this time with the addition of another loop to handle different queries.

Putting All the JavaScript Together

We now have all the functions necessary to generate our XML query and parse the results. Create a new file called searchXML2.js and paste all the JavaScript functions into it. Also, find the older XML JavaScript file (searchXML1.js) and copy the FetchValue() and NewHTTP() functions. Finally, write our OnSubmit() function, which is called when the form is submitted and has been mysteriously absent from our JavaScript file thus far. It looks like this:

function SubmitQuery()
{
       if ( btnPressed == "Add" )
              AddToList();
       else
              SubmitAjaxQuery();
       return false;
}

The new OnSubmit() checks to see what button was clicked and then proceeds to execute the appropriate function. If you are not sure how this is handled, take another look at the HTML embedded in the PHP file. Try running the search again. If all went well, then the results of the search are displayed entirely without the page reloading.

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

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