Add array capabilities to DOMDocument class

Unfortunately the native PHP's DOMDocument class does not allow you to create an xml document from a multidimensional array or save it to one.

Here is a class, which adds these capabilities - by using DOMDocumentExt you can now call loadArray or saveArray functions to do the loading and saving tasks respectively.

As an added bonus you can also use function called getPathValue, which accepts one parameter - $path - a string in root/child1/child2/../childN format. It will convert the current document tree to an array and will return the contents of the specified path if such exists (or null if such path does not exist).

Here is the full source of the class:

  1. <?php
  2.  
  3. class DOMDocumentExt extends DOMDocument
  4. {
  5.  
  6.     /**
  7.      * Load content of the current document from an array
  8.      *
  9.      * @param array $array Array of key=>value pairs to load the contents from
  10.      * @return boolean
  11.      */
  12.     public function loadArray(array $array)
  13.     {
  14.         // clean the current document
  15.         parent::__construct($this->version, $this->encoding);
  16.  
  17.         // append the supplied array to the current class
  18.         $this->_loadArray($array);
  19.        
  20.         return true;
  21.     }
  22.    
  23.    
  24.     /**
  25.      * Append contents of a multidimensional array to the current document
  26.      *
  27.      * @param array $array     Multi-dimensional array containing the data
  28.      * @param DOMElement $node Parent node to attach children to (only for recursion)
  29.      * @return DOMElement
  30.      */
  31.     private function _loadArray(array $array, $node = null)
  32.     {
  33.         // loop through each entry of the supplied array
  34.         foreach ($array as $key => $value) {
  35.             // if the entry contains other entries
  36.             if (is_array($value)) {
  37.                 // create and append empty document node
  38.                 $new_node = $this->createElement($key);
  39.                 // append contents of the entry to the newly created node
  40.                 $child = $this->_loadArray($value, $new_node);
  41.            
  42.             // entry is the deepest level in the branch
  43.             } else {   
  44.                 // append text node
  45.                 $child = $this->createElement($key, $value);
  46.             }
  47.            
  48.             // if parent node is not specified
  49.             if ($node === null) {
  50.                 // append the newly created node to the document root
  51.                 $this->appendChild($child);
  52.             } else {
  53.                 // otherwise append the new node to the specified parent node
  54.                 $node->appendChild($child);
  55.             }
  56.         }
  57.        
  58.         // return the new node. Only for recursion
  59.         return $node;
  60.     }
  61.    
  62.    
  63.     /**
  64.      * Save current document tree as an array
  65.      *
  66.      * @return array
  67.      */
  68.     public function saveArray()
  69.     {
  70.         if ($this->documentElement !== null) {
  71.             return $this->_saveArray($this);
  72.         } else {
  73.             return null;
  74.         }
  75.     }
  76.    
  77.    
  78.     /**
  79.      * Convert current document to an array
  80.      *
  81.      * @param DOMElement $node XML document's node to convert
  82.      * @return array
  83.      */
  84.     private function _saveArray($node)
  85.     {
  86.         $result = null;
  87.        
  88.         // if the node is a text node, simply return its value
  89.         if ($node->nodeType == XML_TEXT_NODE) {
  90.             $result = $node->nodeValue;
  91.        
  92.         // otherwise there are other entries inside the current one
  93.         } else {
  94.             // start with a blank array
  95.             $result = array();
  96.            
  97.             if ($node->hasChildNodes()) {
  98.                 $children = $node->childNodes;
  99.                
  100.                 // loop through all childnodes of the node
  101.                 for ($i = 0; $i < $children->length; $i++) {
  102.                     $child = $children->item($i);
  103.                    
  104.                     // if this is not a text node
  105.                     if (strtolower($child->nodeName) != '#text') {
  106.                         // if we haven't encountered another node with the same name before
  107.                         if (!isset($result[$child->nodeName])) {
  108.                             $result[$child->nodeName] = $this->_saveArray($child);
  109.    
  110.                         } else {    // we have another node with the same name
  111.                             // save the existing node in a temporary variable
  112.                             $temp = $result[$child->nodeName];
  113.                            
  114.                             // reset the current value
  115.                             $result[$child->nodeName] = array();
  116.                            
  117.                             // append the previous value
  118.                             $result[$child->nodeName][] = $temp;
  119.                             // add the
  120.                             $result[$child->nodeName][] = $this->_saveArray($child);
  121.                         }
  122.                     } else {
  123.                         $result = $node->nodeValue;
  124.                     }
  125.                 }
  126.             } else {
  127.                 $result = "";
  128.             }
  129.         }
  130.        
  131.         return $result;
  132.     }
  133.    
  134.    
  135.     /**
  136.      * Get value of a node in the current document
  137.      *
  138.      * @param string $path Path to the node in root/child/../target_node format
  139.      * @return mixed
  140.      */
  141.     public function getPathValue($path)
  142.     {
  143.         $array = $this->saveArray();
  144.        
  145.         $pathlist = explode('/', $path);
  146.        
  147.         foreach ($pathlist as $key) {
  148.             if (is_array($array) && isset($array[$key])) {
  149.                 $array = $array[$key];
  150.             } else {
  151.                 return null;
  152.             }
  153.         }
  154.        
  155.         return $array;
  156.     }
  157.  
  158. }
  159.  
  160. ?>

IMPORTANT: You must have php-xml module installed on your server to use this class!

I will not try to explain every line here as the code is pretty much commented so please see inline comments if something is not clear.

Further follows an overview of all three extended functions available for your use.

loadArray(array $array) - empties the current document and creates a new one from the contents of the $array. The array must consist of key => value pairs, where the key will be the XML tag name and the value tags content. Of course the array can be multidimensional, which means that the value can as well be another array.

An example:

  1. $array = array(
  2.      "root" => array(
  3.          "node1" => array(
  4.              "node1-1" => "value 1-1",
  5.              "node1-2" => "value 1-2",
  6.          ),
  7.          "node" => "value 2-2",
  8.      ),
  9.  );
  10.  
  11.  $doc = new DOMDocumentExt("1.0", "utf-8");
  12.  $doc->loadArray($array);
  13.  print $doc->saveXML();

The example above will output the following xml string:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <root><node1><node1-1>value 1-1</node1-1><node1-2>value 1-2</node1-2></node1><node>value 2-2</node></root>

saveArray() - returns the current document in the form of an array. Returns null if the current document has not yet been created.

An example:

  1. $xml = '<root><node1><node1-1>value 1-1</node1-1><node1-2>value 1-2</node1-2></node1><node>value 2-2</node></root>';
  2.  
  3. $doc = new DOMDocumentExt("1.0", "utf-8");
  4. $doc->loadXML($xml);
  5.  
  6. var_dump($doc->saveArray());

The example above will output the following:

array(1) {
["root"]=>
array(2) {
["node1"]=>
array(2) {
["node1-1"]=>
string(9) "value 1-1"
["node1-2"]=>
string(9) "value 1-2"
}
["node"]=>
string(9) "value 2-2"
}
}

getPathValue(string $path) - converts the current document to an array and returns contents of the specified path. The path entries must be separated by forward slashes (/). It must start in the root.

Let's consider an example:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <config>
  3.     <users>
  4.         <user>
  5.             <name>john</name>
  6.         </user>
  7.         <user>
  8.             <name>matt</name>
  9.         </user>
  10.     </users>
  11. </config>

In order to get list of all users as an array, you can call do the following:

  1. <?php
  2. $doc = new DOMDocumentExt();
  3. $doc = $doc->loadXML($xml); // let's assume that the value of the $xml variable is the contents of the example xml document
  4.  
  5. $users = $doc->getPathValue('config/users');
  6. ?>

The output of the code would be:

array(1) {
["user"]=>
array(2) {
[0]=>
array(1) {
["name"]=>
string(4) "john"
}
[1]=>
array(1) {
["name"]=>
string(4) "matt"
}
}
}

As you can see the function takes multiple rows with the same name into account by automatically creating an indexed array. This is neccessary because a PHP array cannot contain two keys with the same name in the same level. For instance

  1. array('a' => 1, 'a' => 2);

is the same as

  1. array('a' => 2);

You can use the following code to print the name of each user:

  1. $doc = new DOMDocumentExt();
  2. $doc = $doc->loadXML($xml);
  3.  
  4. foreach ($doc->getPathValue('config/users/user') as $id => $user) {
  5.     print $user['name'] . "<br />";
  6. }

It will output

john
matt

In case you needed to access information about john, you should then pass "config/users/user/0" as the path.

NOTE: This class does not take tag attributes into account! I tried to find solution to it but it looks impossible so far. All my attempts have failed so if someone has a solution to it, please feel free to post them in your comments.

Comments
1
Hi

The _saveArray() method only works if there are not more than 2 child nodes with the same nodeName somewhere (for example the user tag). I modified this a bit and I also...:

- added a variable "$matchingSiblings = 0" over the for()-loop . 
- increment the variable right before the handling with the $temp variable (the case where a nodename was used before)
- only do all the temp stuff if $matchingSiblings is == 1.

Otherwise the first to same nodes work fine, but the third node isn't added to the same array. The old array with two nodes is instead added as a new array where the third node gets attached to.
Jan Brinkmann, May 20th 2010, 14:17
2
True, the functions are not perfect. I've spent a while trying to perfect it but it is pretty difficult. The major problem is that you can't have two keys with the same name in one PHP array however you can have multiple same level nodes with the same name in an XML document.

I did my best at the time to write these functions but they definitely need improvement. It would be highly appreciated if someone could post the code or a link to their blog with a solution.
Andris, May 28th 2010, 9:57
3
Why on earth would DOMDocument  not allow multidimensional arrays? Isn't that what an XML document is by its very design? 

1. If the following is not a multidimensional array...
2. ...and if no one will EVER have need to read through it with something like DOMDocument, then I'm the Queen of Sheba:

<xml>
<entries>
<entry>
<book></book>
<author></author>
</entry>
<entry>
<book></book>
<author></author>
</entry>
<entries>
</xml>
Jon, July 15th 2010, 1:14
4
The question in this case whether or not one can simulate arrays in xml but the fact that PHP's DOMDocument class does not allow loading and exporting its contents from/to an array.

But otherwise your example is correct except for the fact that it is a manually created XML string. What if you had it as an array and you needed to populate an instance of a DOMDocument class with its contents?
Andris, August 2nd 2010, 14:22
5
Hi,
This is pretty useful for my case.
can I use this code for commercial purpose ?
Talanika, August 28th 2012, 7:19
6
@Talanika - absolutely, if you find it useful!
Andris, September 12th 2012, 16:10
Name
Email (required)
will not be published
Website
Recaptcha
you will only be required to fill it in once in this session

You can use [code][/code] tags in your comments