Now that we know what XML documents are and how to write them, it is time to look at how to use them within PHP. This is complicated ever so slightly by there being two ways of accessing and manipulating an XML document, both of which are supported by PHP. The first is known as Simple API for XML (SAX). This is a small serial access parser for XML documents that calls functions implemented by you whenever it encounters content of a specific type (opening element tags, closing element tags, text data, and so forth). It is unidirectional, meaning that it works through your XML document telling you what it sees as it sees it (see Figure 23-1). This has the advantage of being fast and memory efficient, but it has the disadvantage of doing nothing other than giving you the tags and text as it sees themyou are responsible for interpreting any structure and hierarchy from the data. Figure 23-1. A SAX parser working on a document.The second method is known as the Document Object Model, or DOM. The XML DOM gives you the information in a given XML document in a hierarchical object-oriented fashion. As a document is loaded in the DOM, it forms a hierarchy of objects representing its structure, and you move through the document using these objects (see Figure 23-2). This is an intuitive way to access your data, because the data is specified hierarchically in the XML document. The DOM does, however, have the disadvantage of being somewhat slower and more memory intensive than SAX. Figure 23-2. An XML DOM representation of a document.Using SAX or DOMNow that we have two options for accessing our data, we must choose between them. We are fortunate in that we can identify clear situations when one would be better than the other. For less hierarchical data that could be considered more the results of an "information dump," we would want to use the SAX parser to reload the data. An example of this would be an object in PHP and its properties. If we want to dump a collection of these and their member data to disk in an XML file or even store them in a field in a database table for later depersistence, we would likely be working with straightforward files with a limited structure. Loading them back in with SAX would be fast and not require much extra work. For most other situations, especially for data that is fundamentally hierarchical or less predictable in nature (such as user data), we would want to use the DOM. Any decrease in speed the DOM incurs would be offset by the savings in the code we otherwise would have had to write to rebuild the structure of the document. One other advantage to the DOM is that it can be used to generate XML documents in addition to reading and parsing them. We can create and add new elements to our tree using the DOM and then resave the document to disk if we want. For systems based entirely on the SAX parser, XML generation will be done by hand (which is still not unreasonably challenging). Although both XML implementations in PHP5 are interesting and useful, we will find ourselves more often using the DOM, which is why we cover it further. Using the DOMAlthough the SAX parser is fast, easy to use, and permits us to load our data efficiently, it does not let us take full advantage of the XML documents, including truly appreciating their hierarchical structure. It also doesn't easily enable us to search through our documents looking for specific pieces of information. Fortunately, the designers of the XML Specifications, foreseeing the usefulness of such functionality, created a specification for a DOM. This specification defines a number of "levels" for DOMs, the second of which encourages an object-oriented implementation. The DOM is just a set of classes through which you can create, inspect, and manipulate XML documents. PHP5 ships with a new implementation of a DOM, known simply as "the DOM." Older versions of PHP did have a DOM implementation, but the system for PHP5 is vastly changed and improved, and we cover this one exclusively. Setting Up PHP for the DOMThe new DOM is enabled by default in PHP5. For those users compiling PHP themselves, no extra command-line options are usually required. No configuration options are required in php.ini for this extension. Getting Started in CodeThe DOM implementation in PHP5 is a robust system of object classes, the most important of which is usually the DOMDocument class. It is this class that you will use to load and save documents, get access to the elements in your document, and search for content within the document. Creating a DOMDocument object is as simple as follows: $dom = new DOMDocument(); After you have this, you need to load in the XML content you want to parse. You can either give it the name of a file to load with the load member function, or you can just give it a string containing the XML content through the loadXML function: $result = $dom->load('c:/webs/health/claims.xml'); if ($result === FALSE) { throw new CantLoadClaimsException(); } // continue The DOMDocument class contains a number of methods to create new XML documents, including some for creation of elements, attributes, and text content. There are also methods for searching within a document (see the section "Adding Search Capabilities") and for validating with DTDs and XSDs (see the section "Validating XML"). The first property with which we will work, however, is the documentElement property, which returns the root node of the element hierarchy in our document. The Element HierarchyAs shown before, XML documents are organized in a hierarchical format, with all content nodes originating from a single document element, or root node. This root node is accessed by querying the documentElement property on the DOMDocument object: $rootNode = $dom->documentElement; The $rootNode variable now contains an object of type DOMElement, which inherits directly from DOMNode. All nodes in the DOM are implemented as classes inheriting from the DOMNode class, which contains a number of basic properties and methods. You learn the type of the node by querying the nodeType property on a given node, which will typically have one of the values shown in Table 23-2. (A few other possible values exist, but we will not likely encounter those much.)
The node types with which we will work most of the time are elements, attributes, and text nodes. Element nodes correspond to the elements in our documents, and any attributes they contain are represented by an attribute node. Their contents are represented by a text node, as shown in Figure 23-3. Figure 23-3. A sample node hierarchy in PHP.One of the quirks to working with the DOM to which we will have to adjust initially is that there is a requirement in the XML Specification that the DOM preserve all text content in an XML document, including the whitespace between nodes. So, in fact, the diagram shown in Figure 23-3 is not quite correct. There will be text nodes in places that we would not otherwise expect them, as in Figure 23-4. Figure 23-4. A more accurate sample node hierarchy in PHP.Fortunately, if we do not care about whitespace and these extra newlines, spaces, and tabs, we can tell our DOMDocument object to cheat a little bit and collapse all extra whitespace, removing many of those unwanted text nodes. You can do this by setting the preserveWhiteSpace property on it to FALSE before the document is loaded. Changes to this member variable have no effect on already loaded documents: $dom->preserveWhiteSpace = FALSE; With this change, our document hierarchy truly would look like that shown in Figure 23-3. Nodes and ElementsBecause all nodes and elements inherit from the same base class, the DOMNode, there is a standard way of querying nodes for information and working our way through the document hierarchy without surprises. Standard pieces of information to query on an element or node are as follows:
To see these in action, look at the following example code, which takes a simple XML document and shows some properties being queried: <?php $xmldoc = <<<XMLDOC <?xml version="1.0" encoding="utf-8"?> <sh:Shoes xmlns:sh='http://localhost/shoestore'> <sh:Shoe> <sh:BrandName>Nyke</sh:BrandName> <sh:Model>Super Runner 150</sh:Model> <sh:Price>109.99</sh:Price> </sh:Shoe> </sh:Shoes> XMLDOC; $dom = new DOMDocument(); $dom->preserveWhiteSpace = FALSE; $result = $dom->loadXML($xmldoc); if ($result == FALSE) die('Unable to load in XML text'); $rootNode = $dom->documentElement; echo $rootNode->nodeName . "<br/>\n"; echo $rootNode->localName . "<br/>\n"; echo $rootNode->prefix . "<br/>\n"; echo $rootNode->namespaceURI . "<br/>\n"; ?> The output of this script will be as follows: sh:Shoes Shoes sh:Shoes sh http://commerceserver/shoestore Navigating through the hierarchy of elements is done through the following standard methods and properties available on all classes inheriting from DOMNode:
From this list of methods, we can see that we have the following three ways to iterate through the child nodes of a given node: // // if we don't preserve whitespace, then we can just get // the first node (it will be a DOMElement). Otherwise, // we have to skip over the DOMText node that will be there! // if ($dom->preserveWhiteSpace === FALSE) $shoe = $node->firstChild; // <sh:Shoe> else { $shoe = $node->firstChild; while ($shoe->nodeType !== XML_ELEMENT_NODE) $shoe = $shoe->nextSibling; } echo "<br/>Method 1:<br/>\n"; foreach ($shoe->childNodes as $child) { echo "Type: $child->nodeType, Name: $child->localName<br/>"; } echo "<br/>Method 2:<br/>\n"; $children = $shoe->childNodes; for ($x = 0; $x < $children->length; $x++) { $child = $children->item($x); echo "Type: $child->nodeType, Name: $child->localName<br/>"; } echo "<br/>Method 3:<br/>\n"; $child = $shoe->firstChild; while ($child !== NULL) { echo "Type: $child->nodeType, Name: $child->localName<br/>"; $child = $child->nextSibling; } All three result in the same nodes being visited. Note the extra code we had to include at the top. In those cases where we are preserving whitespace, the first child of an element node is very likely not to be an element node, but a text node containing the whitespace between that element and the next element node. The output of the preceding script with preserveWhiteSpace set to TRUE (the default) will be the same for all three loops, as follows. (Recall that nodeType 3 is XML_TEXT_NODE and 1 is XML_ELEMENT_NODE.) Type: 3, Name: Type: 1, Name: BrandName Type: 3, Name: Type: 1, Name: Model Type: 3, Name: Type: 1, Name: Price Type: 3, Name: AttributesTo access attributes on a given node, you have two options. The first, and by far most common, is to use the hasAttribute and getAttribute methods, as follows: if ($element->hasAttribute('name')) echo 'Name is: ' . $element->getAttribute('name'); else echo 'Element has no name!'; The other method for obtaining attributes is to use the attributes collection on the DOMNode class, which enables us to get at the actual DOMAttr classes representing these attributes, as follows: $attrs = $element->attributes; if ($attrs !== NULL) { foreach ($attrs as $attr) { if ($attr->name == 'name') echo 'Name is: ' . $attr->value; } } else echo 'Element has no attributes!'; Although slightly less convenient than the getAttribute method on the DOMElement class, this method enables us to view all attributes and their values when we are not absolutely certain as to which attributes will exist for a given element. An ExampleTo see all of this in action, we will continue the example of the health-care claims system. We will write a ClaimsDocument class, which we will use to return all the claims in a document, or search for claims given a user's name. We will declare some extremely simple classes (without interesting implementation) to hold the data we learn about claims and patients, as follows: class Patient { public $name; public $healthCareID; public $primaryPhysician; } class Claim { public $patient; public $code; public $amount; public $actingPhysicianID; public $treatment; } We will also create a new class called the ClaimsDocument, as follows: class ClaimsDocument { public $errorText; private $dom; } The first method we will add on this class is a public method that loads a given claim document and saves the DOMDocument representing it in a private member variable: // // loads in a claims XML document and saves the DOMDocument // object for it. // public function loadClaims($in_file) { // create a DOMDocument and load our XML data. $this->dom = new DOMDocument(); // by setting this to false, we will not have annoying // empty TEXT nodes in our hierarchy. $this->dom->preserveWhiteSpace = FALSE; $result = $this->dom->load($in_file); if ($result === FALSE) throw new CantLoadClaimsException(); return TRUE; } We will next write a method to return an array of Claim objects for all of the <Claim> elements in the document: // returns an array containing all of the claims we loaded public function getAllClaims(&$out_claimsList) { // 1. get the root node of the tree (Claims). $claimsNode = $this->dom->documentElement; // 2. now, for each child node, create a claim // object. $claimsList = array(); foreach ($claimsNode->childNodes as $childNode) { $claim = $this->loadClaim($childNode); $claimsList[] = $claim; } // set up the out param $out_claimsList = $claimsList; return TRUE; } As you can see, this method requires a new method called loadClaim: // // loads the data for a claim element. // private function loadClaim($in_claimNode) { $claim = new Claim(); foreach ($in_claimNode->childNodes as $childNode) { switch ($childNode->localName) { case 'Patient': $claim->patient = $this->loadPatient($childNode); break; case 'Code': $claim->code = $childNode->textContent; break; case 'Amount': $claim->amount = $childNode->textContent; break; case 'ActingPhysicianID': $claim->actingPhysicianID = $childNode->textContent; break; case 'Treatment': $claim->treatment = $childNode->textContent; break; } } return $claim; } This method, as it works, calls a function to load the patient data, called loadPatient: // // loads the data for a patient element. // private function loadPatient($in_patientNode) { $patient = new Patient(); $patient->name = $in_patientNode->getAttribute('name'); foreach ($in_patientNode->childNodes as $childNode) { switch ($childNode->localName) { case 'HealthCareID': $patient->healthCareID = $childNode->textContent; break; case 'PrimaryPhysician': $patient->primaryPhysician = $childNode->textContent; break; } } return $patient; } Adding Search CapabilitiesFinally, we will add a new public method that demonstrates how to use the facilities available via the DOMDocument to find nodes within our document. We will call this method findClaimsByName, and it will return all claims for the patient with the given name. This function works by using the getElementsByTagName method on the DOMDocument class. This method takes the name of an element to find as an argument and returns a list of all those nodes in the document with the given element (tag) name: public function findClaimsByName($in_name) { if ($in_name == '') { throw new InvalidArgumentException(); } $claims = array(); // 1. use the DOMDocument to do the searching for us. $found = $this->dom->getElementsByTagName('Patient'); foreach ($found as $patient) { // 2. for any found node, if the name is the one we // want, then load the data. these are in the parent // node of the Patient node. if (trim($patient->getAttribute('name')) == $in_name) { $claims[] = $this->loadClaim($patient->parentNode); } } return $claims; } Putting It All TogetherTo show the use of our new ClaimsDocument object, we can list some simple code to demonstrate loading the claims, listing the claims that were sent with it, and finding a claim by patient name. We start by creating a ClaimsDocument object and wrapping our code in a try/catch block in case there is an error with the XML: try { $cl = new ClaimsDocument(); // etc. } catch (Exception $e) { echo "¡Aiee! An Error occurred: " . $e->getMessage() . "<br/>\n"; } After creating the object, we load the claims document and have the it return a list of all those claims: $cl->loadClaims('claims.xml'); $claims = $cl->getAllClaims(); If we then want to summarize these claims, we can write code as follows: $count = count($claims); echo "<u>Successfully loaded $count claim(s)</u>. "; echo "Summarizing:"; echo "<br/><br/>\n"; foreach ($claims as $claim) { $name = $claim->patient->name; $id = $claim->patient->healthCareID; echo "Patient Name: <b>$name</b> (ID: <em>$id</em>)<br/>"; } Finally, to find a specific user and find out how much the user's claim was, we write this: echo <<<EOM <br/><br/><u>Searching for specific user:</u><br/><br/> EOM; $matching = $cl->findClaimsByName('Samuela Nortone'); echo "Patient Name: <b>$name</b> (ID: <em>$id</em>)<br/>\n"; echo "Claim Amount: $claim->amount<br/>\n"; When put together, the complete code looks like this: try { $cl = new ClaimsDocument(); $cl->loadClaims('claims.xml'); $claims = $cl->getAllClaims(); $count = count($claims); echo "<u>Successfully loaded $count claim(s)</u>. "; echo "Summarizing:"; echo "<br/><br/>\n"; foreach ($claims as $claim) { $name = $claim->patient->name; $id = $claim->patient->healthCareID; echo "Patient Name: <b>$name</b> (ID: <em>$id</em>)<br/>"; } echo <<<EOM <br/><br/><u>Searching for specific user:</u><br/><br/> EOM; $matching = $cl->findClaimsByName('Samuela Nortone'); echo "Patient Name: <b>$name</b> (ID: <em>$id</em>)<br/>\n"; echo "Claim Amount: $claim->amount<br/>\n"; } catch (Exception $e) { echo "¡Aiee! An Error occurred: " . $e->getMessage() . "<br/>\n"; } |