The following tutorial is the first of a two-part series on using Object-Oriented Programming (OOP) to improve your approach to manipulating xml structures.
A DOMIT! user recently asked to see some examples of modifying an xml document . How should one read in an xml file, change one or more of its values, and write it back to the filesystem?
He sent me the following snippet of XML to work with:
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE glossary [
<!ELEMENT staterotation (rotation+)>
<!ELEMENT rotation (state, banner)>
<!ELEMENT state (#PCDATA)>
<!ELEMENT banner (#PCDATA)>
]>
<staterotation>
<rotation>
<state>ca</state>
<banner>8</banner>
</rotation>
<rotation>
<state>hi</state>
<banner>1</banner>
</rotation>
<rotation>
<state>fl</state>
<banner>2</banner>
</rotation>
<rotation>
<state>tx</state>
<banner>4</banner>
</rotation>
<rotation>
<state>ny</state>
<banner>6</banner>
</rotation>
</staterotation>
The requirements for working with the string were as follows:
Here is an xml file that contains different state nodes. With each state node there is a banner node. The banner node keeps track of which banner number to display when someone wants to view a banner from that state. After a banner has been viewed for that state the banner node value should be incremented by one.
I was a bit unclear about the directions. I assumed that the banners were advertising of some sort, to be displayed on a web site. I also assumed that there existed more than one banner per state and that these banners should be rotated such that the same one was not always on display.
But how many banners per state were there? And in what context would someone want to view a banner for one state as opposed to another?
I had many other questions, and my first thoughts were to e-mail the contributor and ask for more detail. Then I stopped and thought:
Instead of fretting over the minute details of the proposed implementation, why not tackle the problem from a more abstract, reusable perspective? The xml file, after all, describes a set of data that is stable enough to warrant a Document Type Definition. It was not out of the question that the xml data might be used in a variety of different contexts.
With this in mind, I felt it would be wasteful to merely crank out a bunch of linear code that could only be used in a narrow set of circumstances. I should instead write a "wrapper" class that handled the data access and modification in a context-agnostic way. The user could be provided with a few simple but versatile methods that automated all the messy, redundant work of looping through nodes to find particular values. The xml data could thus be accessed in a safe and consistent manner, with methods that had already been tested and were known to work properly. It's a bit more up-front work, but the benefits are many.
So, I decided to build a class called StateRotation.
The first step is to create a class statement, and a class constructor whose role is to build an empty DOMIT_Document that can be used by the class methods. The document is contained in the class variable $xmlDoc. The contructor invokes an instance of the DOMIT! parser:
class StateRotation {
var $xmlDoc;
function StateRotation() {
require_once("xml_domit_parser.php");
$this->xmlDoc =& new DOMIT_Document();
} //StateRotation
Two methods are added for populating the document:
The method fromFile uses the DOMIT_Document method loadXML to read from a file. You pass in a path to the xml file ($filename) and it will construct a new DOMIT_Document.
The method fromString uses the DOMIT_Document method parseXML to read from an xml string. You pass in a raw xml string ($string) and it will construct a new DOMIT_Document.
function fromFile($filename) {
return $this->xmlDoc->loadXML($filename);
} //fromFile
function fromString($string) {
return $this->xmlDoc->parseXML($string);
} //fromString
Both methods return "true " if the parsing is successful.
A method for saving the modified xml file is also necessary. The toFile method uses the saveXML method of DOMIT_Document, which returns "true" of the document is successfully saved:
function toFile($filename) {
return $this->xmlDoc->saveXML($filename);
} //toFIle
Now that the class is able to input and output xml, we must write the accessor and mutator methods - methods which get and set the class data. The count function will tell us how many rotation nodes exist. We'll need this number to loop through each node and search for values.
function count() {
return count($this->xmlDoc->documentElement->childNodes);
} //count
getStatesList is a convenient method for obtaining a list of existing states in the list. It returns this list as an array reference - thus the ampersand in the method signature.
function &getStateList() {
$total = $this->count();
$states = array();
for ($i = 0; $i < $total; $i++) {
$currRotation =& $this->xmlDoc->documentElement->childNodes[$i];
$states[] = $currRotation->childNodes[0]->firstChild->nodeValue;
}
return $states;
} //getStateList
Note how getStatesList uses our count() function to loop through each child node of the documentElement. At each node, it will burrow a bit deeper and grab the nodeValue of the state node.
The same basic loop can be used for the remaining methods. The getBanner method iterates through each rotation node, performs a test to determine if the current state name equals the state name passed in by the programmer, and if the test is successful, the banner value is returned. Otherwise a blank string is returned.
function getBanner($state) {
$total = $this->count();
for ($i = 0; $i < $total; $i++) {
$currRotation =& $this->xmlDoc->documentElement->childNodes[$i];
if ($currRotation->childNodes[0]->firstChild->nodeValue == $state) {
return $currRotation->childNodes[1]->firstChild->nodeValue;
}
}
return "";
} //getBanner
The setBanner method is also searches for a particular state node. When it finds that node, however, it updates the node with the nodeValue supplied by the programmer. A break statement pops you out of the loop once the update has been made.
function setBanner($state, $num) {
$total = $this->count();
for ($i = 0; $i < $total; $i++) {
$currRotation =& $this->xmlDoc->documentElement->childNodes[$i];
if ($currRotation->childNodes[0]->firstChild->nodeValue == $state) {
$currRotation->childNodes[1]->firstChild->nodeValue = $num;
break;
}
}
} //setBanner
One of the requirements of the class is that the value of the banner node needs to be incremented each time it is viewed. We therefore add an incrementBanner method, which, like the setBanner method, loops through the rotation nodes until it finds the specified state. It then bumps up the value of the banner by one.
One caveat when using xml is that numerical data is prersented in string format. When incrementing the banner value, we must therefore:
convert the number from a string to an integer using the PHP intval function
perform the increment
convert back from integer to string by concatenating the integer with a "".
function incrementBanner($state) {
$total = $this->count();
for ($i = 0; $i < $total; $i++) {
$currRotation =& $this->xmlDoc->documentElement->childNodes[$i];
if ($currRotation->childNodes[0]->firstChild->nodeValue == $state) {
$currNum = intval($currRotation->childNodes[1]->firstChild->nodeValue);
$currNum++;
$currRotation->childNodes[1]->firstChild->nodeValue = ("" . $currNum);
break;
}
}
} //incrementBanner
Once the increment is performed, we can exit the loop using the break statement.
Now that we have encapsulated the functionality that we need within the StateRotation class, we should be able to safely query and update the xml structure, never again having to worry about the gory details of looping through nodes and testing each for certain conditions, while the inevitable typos creep in. If a method works properly once, it should work every time.
This makes for far cleaner, more readable, and more manageable code, that is insulated to a great extent from human error. It is in fact the whole point of an object oriented approach.
Let's try a simple test of our new class. We will:
open an xml file
echo out the banner value of the "ca" state
increment the banner value
echo out the new data
write the updated xml string to the filesystem
We instantiate the StateRotation class in the usual way:
require_once("state_rotation.php");
$sr =& new StateRotation();
The xml file statecount.xml is then loaded into the class with the fromFile method. Since this method returns true if the parsing is successful, it is prudent to trap for errors and print out an error in the event that something goes wrong:
if ($sr->fromFile("statecount.xml")) {
//code goes here!!!!
}
else {
echo ("Invalid xml file");
}
With out new class, echoing out the value of the "ca" banner is a one-line affair.
echo("The current banner value for 'ca' is: " . $sr->getBanner("ca"));
Incrementing its value is equally as simple:
$sr->incrementBanner("ca");
The new value can now be echoed:
echo("<br /><br />The new banner value for 'ca' is: " . $sr->getBanner("ca"));
The new xml string is then saved:
$sr->toFile("statecount.xml");
Executing the code returns the hoped for results:
The current banner value for 'ca' is: 8
The new banner value for 'ca' is: 9
We have thus achieved our objective, and have in our possession a completely reusable class that allows us to safely and concisely manipulate any xml string that conforms to the DTD specified!
You can download the source files in zip format here
In the second part of this tutorial, we will build upon the functionality of our StateRotation class by adding methods to add, insert, and replace rotation nodes.
If you have any questions about this tutorial, please email the author or post to the DOMIT! bulletin board.