Serving XML

In the previous section, "The "Hello World" of Ajax and PHP," data was served up as plain text. This is great when no post-processing is necessary. However, most applications require the data to be displayed a specific way. You could answer the request with formatted HTML instead of plain text, but then the web application is limited in how it can use the data. We want to separate data and display functionality. Instead, we use markup and data encoding to transmit the data in a format that both PHP and JavaScript can understand without dictating how it should be displayed. The first such format, and arguably the most common, is XML.

We will tackle the XML problem on two fronts: the server using PHP and the consumer (client) using JavaScript. We take on the server first. But before we do anything, we need to agree on a format for our data.

Designing an XML File

XML is a markup language that can be used to encapsulate any type of data that is transmitted over the network or stored on a disk. By definition, the format is generic (extensible) and does not automatically indicate the type of data that it contains. We have to create the format. In this sense XML is a self-descriptive language.

The document always starts with a directive telling the XML parser that the file is indeed XML. The directive also tells the parser the version and optionally the encoding type of the data. Encoding type is the format in which each character in the file is stored.

XML documents always consist of a root element called a node. The type of node is often referred to as a tag. The root element can have one or more children (also nodes), which, in turn, can have children. This is analogous to a tree where the root node is the trunk of the tree and each child node is a branch. Branches can have multiple sub branches but there can only ever be one trunk (root node). For this reason, we often refer to XML data as the XML tree.

Each tag can also have a set of attributes. Typically, an attribute is closely related to the tag. For example, a book is hardcover as opposed to a subchild where a book has a title. Attributes are defined the same way and are similar to attributes in HTML tags. For example, a book with a cover type may look like <book cover="hard"> where "cover" is the attribute and "hard" is the value of that attribute. Unlike in HTML all attribute values must be enclosed in double quotes to be valid XML.

Tags are case sensitive. <book> is different than <Book>. By convention, tags are usually all lower case. Tags, once opened, must be closed. They must also be closed in the inverse order of how they were defined. The last tag opened must be the first one to be closed. Closing tags bear the same name as the opening tag only with a forward slash in front of the tag name, and they never have attributes. To borrow a few tags from HTML, the first example in Table 1 is correct (valid XML) while the second is incorrect.

Table 1. Valid versus invalid XML

Valid

Invalid

<u><em>Text</em></u>

<u><em>Text</u></em>

In the event that a tag doesn't have any children, we can close it immediately by ending it with a forward slash. For example: <br></br> is reduced to <br/> instead.

Since the standardization of XML, XHTML has arisen. XHTML is similar to HTML and is compatible with HTML web browsers. However, it conforms completely to the XML standard. There are two implementations of XHTML one is strict and the other is transitional. The main difference between the two is the level of standards conformance with the XHTML standard. In this Short Cut, we use all transitional XHTML for the examples. Another main difference is that all tag and attribute names should both be entirely lowercase. We have already seen one example of XHTML in the "Hello World" example.

You should now have a good idea of what XML and XHTML are and how they work. You know enough for just about all XML-based Ajax applications.

We will now encapsulate information about books. We want our XML data to reflect that. We also want to include multiple books in the response to a single request. This format meets all our requirements:

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

It starts with the XML directive, which tells the parser the XML version. Then we can add multiple children nodes (books) inside the root element (catalog) and each book will have four child nodes. Books have title, author, year, and publisher. We're now ready to write some code.

The PHP Scripts

This example follows the same format as the previous example. First, let's examine the PHP, then explore the JavaScript. This script is significantly more complicated than the "Hello World" example so we'll break it up into several parts.

First we use a class to format the XML data.

The XMLNode Class

In order to handle the XML data, we need to have a defined method of storing the data. An array would be sufficient, however, to keep everything uniform we will be creating a class to store the XML data. This class needs to store a tag name, the attributes of the tag, and any children the tag may have. In addition, we would like some built-in behavior, such as converting to a string and traversing the child nodes. Given that definition, this is the class:

<?php
class XMLNode
{
       public $tag, $attributes, $children=array(), $text="";
       private $iteration=array();

       public function getChild($x) {
              if ( !isset($this->iteration[$x]) ) $this->iteration[$x]=0;
              for ( ; $this->iteration[$x] < count($this->children);
                      $this->iteration[$x]++ ) {
                     if ( $this->children[$this->iteration[$x]]->tag == $x ) {
                           $tmp = &$this->children[$this->iteration[$x]++];
                           return $tmp;
                     }
              }
              return null;
       }
       public function createChild($tag, array $attributes=array()) {
              $this->children[] = new XMLNode($tag, $attributes);
              $tmp = &$this->children[count($this->children)-1];
              return $tmp;
       }
       function __construct($t, array $a=array()) {
              $this->tag = $t;
              $this->attributes = $a;
       }
       public function buildChildrenFromArray($tag, array $nodes)
       {
              foreach ( $nodes as $key => $value ) {
                     $newNode = $this->createChild(is_numeric($key) ? $tag : $key);
                     if ( is_array( $value ) ) {
                           $newNode->buildChildrenFromArray($newNode->tag, $value);
                     } else $newNode->text = $value;
              }
       }
       function __toString($depth=0) {
              $pad = str_pad("",$depth,"	");
              $str  = "$pad<".$this->tag;
              foreach ( $this->attributes as $attr => $value)
                     $str .= " $attr="$value"";
              $str .= ">".$this->text;
              if ( count($this->children) > 0 ) $str .= "
";
              foreach ( $this->children as $child )
                     $str .= $child->__toString($depth+1);
              if ( count($this->children) > 0 ) $str .= "$pad";
              return "$str</".$this->tag.">
";
       }
}
?>

This class may look complicated but, if taken piece by piece, it is relatively easy to digest. Remember, you don't always have to know exactly how a class or function works internally—you just need to know how to use it effectively.

This class has five public methods for use. The simplest of them is __construct, which is the class constructor that sets the tag and attributes (see the Appendix for more information on constructors). Another useful method is createChild(), which will create a new child node and return a pointer to it. A pointer is used so that a reference to the data is returned instead of a copy of it. To create a pointer you can prefix the variable with an ampersand (&).

There are two methods that facilitate retrieving the data.

The first method is getChild(). This method takes the tag name of the child as a parameter and returns the next child of that type. It then increments an internal counter much like the each construct in PHP. If there aren't any children found, it will return null. Using this method, loop through all the books and print their titles using two lines of code:

while ( $book = $data->getChild("book") )
       echo $book->getChild("title")->text."<br>";

The second data retrieval method is the magic method __toString(). It can be called automatically when an instance of the class is referenced as a string or it can be used manually. In this class, it is a recursive function (a function that calls itself) that will print the value of the node and all its child nodes. Using this method, we can print out the XML tree using the current node as the root.

There is a second recursive method in the class. It is the buildChildrenFromArray(), which takes a default tag name and an array of nodes and then uses them to create child nodes (branches). One child node is created for each item in the array. If the array key is numeric, then the default name is used as a tag name, otherwise the array key itself is used. Then one of two things can happen. If the array value is another array (as is the case in a multidimensional array), then it calls buildChildrenFromArray() again for the new node. If the value is not an array, then it just sets the text of the new node to that value and continues to the next item in the array.

This seems like a lot of work for a simple XML node. However, we soon see that the XMLNode class makes other potentially complex actions easy. And we also see the class used again later.

The XMLPrinter Class

Rather than creating specialized code to print out the XML-formatted book data, we are going to make a general utility class that contains static methods to print out the XML for us. The class has only two public methods. The one we use here takes the name of the root node, the name of the first level or children, and an array of values. The class handles any level of depth (arrays of any dimension). Arrays can also be associative (an array using strings as keys), numerical indexed, or any combination of the two. Depending on the type of index/key the output of the method will be different:

<?php
include("XMLNode.class.php");
class XMLPrinter {
       public static function PrintFromNode( XMLNode $tree )
       {
              echo "<?xml version="1.0" ?>
";
              echo $tree->__toString();
       }
       public static function PrintFromArray( $root, $tag, array $tree )
       {
              $rootNode = new XMLNode($root);
              $rootNode->buildChildrenFromArray($tag,$tree);
              XMLPrinter::PrintFromNode( $rootNode );
       }
}
?>

We now have two classes that can be freely used in any web application where we have an array that we would like to format as XML. The printer class is extremely simple thanks to most of the work being done in our XMLNode class.

You can copy and paste this code into a file called XMLPrinter.class.php and copy the XMLNode class into a file called XMLNode.class.php.

Warning

Headers must be sent prior to any output. Since this class will most likely be used with the header() function, make sure there is no extra white space (spaces, new lines, tabs, etc) before the start of the code "<?php" or after the ending "?>" as whitespace is also considered output.

The Main PHP Script

Now that we have a method of generating the XML, we can write our main PHP file. Copy the code below into a file of your choosing and put that file in your web server's path.

<?php
include("XMLPrinter.class.php");
include("BookLoader.class.php");

if ( count( $_GET ) > 0 ) {
       header("Cache-Control: no-cache, must-revalidate");
       header("Content-type: text/xml");

       $results = BookLoader::Search($_GET["title"],$_GET["author"]);
       XMLPrinter::PrintFromArray( "catalog", "book", $results );
} else { ?>
<html>
<head>
       <title>Book search with XML</title>
       <script src="searchXML1.js"></script>
</head>
<body>
       <form method="post" onSubmit="return SubmitQuery()">
       Title: <input type="text" name="title" id="title"><br>
       Author: <input type="text" name="author" id="author"><br>
       <input type="Submit" value="Search" name="Submit" id="Submit">
       </form>
       <table id="data" border="1">
       <tr>
              <th>Title</th>
              <th>Author</th>
              <th>Year</th>
              <th>Publisher</th>
       </tr>
       <?php
       if ( count($_POST) > 0 )
       {
              $results = BookLoader::Search($_POST["title"],$_POST["author"]);
              foreach ( $results as $book )
              { ?>
       <tr>
              <td><?= $book["Title"] ?></td>
              <td><?= $book["Author"] ?></td>
              <td><?= $book["Year"] ?></td>
              <td><?= $book["Publisher"] ?></td>
       </tr>
              <?
              }
       }
       ?>
       </table>
</body>
</html>
<?php } ?>

Before running the script make sure that BookLoader.class.php, XMLPrinter.class.php, and books.csv (from earlier) are in the same directory as your file. Now run your script. You should have a search form and a table underneath it. Try typing "PHP" in the title field and pressing the "Search" button. When the page reloads, you will have a list of search results. There isn't any JavaScript code being executed on this page yet. Everything is pure PHP.

Take a second to examine the flow of the program in Figure 2.

Flow chart for PHP file

Figure 2. Flow chart for PHP file

First, look at what happens if there isn't any variable in $_GET.

If count($_GET) == 0 (the else block)

If there isn't any data in $_GET, then we can safely assume that the page was requested by either the POST method or GET without any query string (e.g., the page was just loaded for the first time). If that happens, then we display the HTML page (the search form). If there are POST variables present, we search using those variables then loop through and display the results. This loop ensures that users without JavaScript can still use our search form (remember unobtrusive JavaScript?).

This part should be fairly straight forward. We have a single form on the page. This form submits its data via the POST method. Our Ajax request, which we create later, will submit the search data via the GET method. This is how we differentiate in this case. There are other ways to do this, including putting the XML server in a separate PHP file or having a specific query tag that tells us it is an Ajax request. (In future applications, you can experiment with other methods.)

Warning

Using count() to determine if data was sent over HTTP can be an easy and efficient method. However, in your applications be sure to check that not only was data sent but also that the data is correct and does not pose any security risks. Be especially careful when using input retrieved from a form or Ajax in SQL queries. Input should always be sanitized prior to use.

If count($_GET) > 0

If there are variables in $_GET, we can assume that an Ajax request has been made. For this example, we assume the data is valid and intact. Then we search based on the query data.

Once our BookLoader class returns with the results we have a nicely formatted array to pass onto our XMLPrinter class. We call the PrintXML() method and we're done. We just served XML formatted book search results to the user.

The headers need a little bit of explaining. You saw the Cache-Control in the "Hello World" example but may have not known exactly what it does. Without this line of code the browser will not request a page if it is in its cache. Instead it loads an old (and potentially obsolete) version. To remedy this, tell the browser not to cache the XML results.

If you are still not convinced, go ahead, load the "Hello World" in Internet Explorer, and try pressing "Go" a few times. Now edit the PHP file and comment out the Cache-Control line. Refresh the page and try pressing "Go" a few more times. If caching is enabled, you may notice that the time no longer updates after the first click. That is the problem that the cache control fixes.

The second header is the content type. In "Hello World" it was "text/plain" because the data returned was unformatted text. In this, we need to change it to return a content type of "text/xml" instead. This is an especially important line of code. Without it, the browser does not recognize the returned data as XML and you may not be able to use it properly with the XMLHTTPRequest object.

If you know PHP well, you may have noticed that the code in the if statement is never executed as it stands, only the else block is. That is because we haven't added our JavaScript yet.

The JavaScript

If you ran the example, you probably noticed a JavaScript error. This is because the PHP file tries to include a JavaScript file (searchXML1.js) that doesn't exist. You may also notice the number one at the end of the file name. We make a searchXML2.js later, but for now, copy all the following JavaScript into a new file "searchXML1.js" in your web server's path:

var httpObj=null;
function NewHTTP()
{
       try { return new XMLHttpRequest(); }
       catch (e) { return new ActiveXObject("Microsoft.XMLHTTP"); }
}
function FetchValue(book,field)
{
       try { return book.getElementsByTagName(field).item(0).firstChild.nodeValue; }
       catch (error) { return "[error]"; }
}
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 books =
                     httpObj.responseXML.documentElement.getElementsByTagName("book");

                     if ( books.length == 0 ) {
                           errorText = "Sorry, no results were found.";
                     } else for ( var i=0; i<books.length; i++ ) {
                           var book = books.item(i);
                           var row = table.insertRow(i+1);
                           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;
              if ( errorText != null ) {
                     var row = table.insertRow(1);
                     var cell = row.insertCell(0);
                     cell.colSpan = 4;
                     cell.innerHTML = errorText;
              }
       }
}
function SubmitQuery()
{
       if ( httpObj != null ) return;
       document.getElementById("Submit").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();
       var author = escape(document.getElementById("author").value);
       var title  = escape(document.getElementById("title").value);
       httpObj.open("GET","?author="+author+"&title="+title,true);
       httpObj.onreadystatechange = OnData;
       httpObj.send(null);
       return false;
}

This script is much more complicated than the ones we saw previously. First let's see what it does. There is one more step before we can see it in action. Find the <form> tag in the PHP file. Now add an event handler to it. Inside the tag put the code:

onSubmit="return SubmitQuery();"

Now before submitting the form to the server a JavaScript enabled web browser will call the SubmitQuery function. Non-JavaScript browsers ignore this event so the page continues to function properly even if JavaScript is not enabled. Save your page and try loading it and searching for "PHP" again. If all went well, you should see the same search results as before but without ever leaving the page that you are on. Your Ajax application is now complete. Now let's look at what the SubmitQuery function actually does.

Most of it should be familiar to you but we added a few new things. First the immediate feedback is different. Instead of just setting the inner HTML of a div to "Loading," we first cleared the table of any rows. Then we inserted a new row that says "Loading..." and finally the "Search" button was disabled to avoid multiple simultaneous searches. Don't worry, it will be enabled again later. Then we build the query string.

The "Hello World" example had a very simple query string. In this example, it is a bit more complex. We need to pass data onto to the server. First, get the data to be passed. Run the date through the "escape" function to URL encode it. The encoding will make the data safe to be sent via a URL. Then concatenate the new data into the query string and send the request as usual. All that is left to do is to wait for a reply.

Once we get our reply, the callback function is called just like before, only now the data is encapsulated in XML. We must decode the XML and display it appropriately. There are four possible cases that we can encounter when the request finished loading.

  • The page does not exist or could not be reached (perhaps the server went down since the page was initially loaded)

  • There are no search results to display (the XML file has a root node but no children nodes)

  • There are one or more search results

  • The data returned is not in a valid format (unlikely in our situation but more probable in other real-world sceneries)

Regardless of the results, when the request finishes loading we re-enable the "Submit" button. However, depending on the results we take different actions afterward.

If the page does not exist or could not load, we display an alert window with an error message and set the "Loading..." cell to display "Error loading search results" instead. If there are no results or the XML is mal-formatted then we display "No results found" instead of the loading text. Finally, if there are results to display we display them one per row by iterating through the XML data.

Parsing XML with JavaScript

Now that the browser has the XML data it needs we can begin to parse it and loop through the results to add rows to the table. This requires a bit of DHTML. A page is said to be using DHTML when JavaScript is used to modify the page in real-time after it is loaded. You've seen DHTML several times already this text but this is the most prominent example of it thus far.

Remember that specifying a header telling the browser that the content following it is text/xml data? Because of that, there's a new tool at our disposal: the responseXML property. Like the responseText property used in previous examples, the responseXML property contains the data returned from the server. The only difference is that it is ready to be traversed via the DOM tree (the Document Object Model for the XML data).

Because the XML document uses the document object model, it is accessible via all the available DOM methods. The one we will use the most is getElementByTageName(), which gets all the tags with a specific name. First get all the books:

var books = httpObj.responseXML.documentElement.getElementsByTagName("book");

We now have an array of every book returned. The document element is the root node. In this case, it is "catalog" but it could be anything we want. Remember, you design the XML document format.

The data is returned as an array of elements. As such, the first thing we do is check to make sure there were elements returned. If the length is zero then there weren't any books the matched the query. Otherwise we loop through all the results.

The results can be queried much like the books were. In this example, we already know the format that we expect from the XML file so we have the benefit of knowing exactly what to look for. As we loop through the books, we create a table row for each one and create table cells for the title, author, year and publisher.

To get the title of a specific book the getElementByTagName() method is used again. This time we are expecting only one result so we use the array subscript [0] to access it directly:

book.getElementsById('Title')[0].firstChild.data;

This method poses a small problem. It relies heavily on the fact that the XML file is formatted exactly the way we expect it to be. An ideal JavaScript should not leave any warnings or errors in the user's log no matter what its input is.

Handling Mal-formatted XML

To solve this problem, we need to do some checking to ensure the element actually exists in the XML tree. There are a few ways to do this. One way is to check to see if the length of the array returned by getElementsByTageName is greater than or equal to one. Then check to make sure "firstChild" is valid. The first child is the text inside the node in our case. Another way is to use exception handling.

We use exception handling. A try/catch block is used to attempt to fetch the value and if an error was encountered then we use the "[error]" string. Since this has to be done for the title, author, year, and publisher we move it to a separate function so that we can reuse the code for each item.

Let's try our error handling code to see how it works. Change the JavaScript to try to fetch the non-existent ISBN field.

alert(FetchValue(book,"ISBN"));

Make sure to insert the code into the "for loop" that iterates through all the books. Then try fetching some search results. For each result you should now be presented with an alert windows that says "[error]" inside of it.

..................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