Get values of multi-dimensional arrays using xpath notation

Sometimes we have a value burried deep within a multi-dimensional array. In order to access it we usually end up doing something like this:

  1. $value = $array['a']['b']['c'];

hoping that all the keys are set. However, we have a problem if 'b' is not set or any other of the "path" keys are missing.

Wouldn't it be nice if we could get the value using something similar to xpath, e.g. "a/b/c" and specify the default value if the chain is broken? I thought it would and wrote this little function or/and extension to ArrayObject.

Object oriented approach

If you are a fan of OOP and SPL then you will probably prefer the following extension to ArrayObject:

  1. <?php
  2.  
  3. /**
  4.  * Class adding the functionality to get array values from a path
  5.  */
  6. class ArrayObjectExt extends ArrayObject
  7. {
  8.  
  9.     // specify the delimiter
  10.     protected $_pathDelimiter = '/';
  11.  
  12.  
  13.     /**
  14.      * Get value an array by using "root/branch/leaf" notation
  15.      *
  16.      * @param string $path   Path to a specific option to extract
  17.      * @param mixed $default Value to use if the path was not found
  18.      * @return mixed
  19.      */
  20.     public function getPathValue($path, $default = null)
  21.     {
  22.         // fail if the path is empty
  23.         if (empty($path)) {
  24.             throw new Exception('Path cannot be empty');
  25.         }
  26.  
  27.         // remove all leading and trailing slashes
  28.         $path = trim($path, $this->_pathDelimiter);
  29.  
  30.         // use current array as the initial value
  31.         $value = $this;
  32.  
  33.         // extract parts of the path
  34.         $parts = explode($this->_pathDelimiter, $path);
  35.  
  36.         // loop through each part and extract its value
  37.         foreach ($parts as $part) {
  38.             if (isset($value[$part])) {
  39.                 // replace current value with the child
  40.                 $value = $value[$part];
  41.             } else {
  42.                 // key doesn't exist, fail
  43.                 return $default;
  44.             }
  45.         }
  46.  
  47.         return $value;
  48.     }
  49.  
  50.  
  51.     /**
  52.      * Set path delimiter
  53.      *
  54.      * @param string $delimiter Delimiter used to split the path
  55.      * @return ArrayObjectExt
  56.      */
  57.     public function setPathDelimiter($delimiter)
  58.     {
  59.         $this->_pathDelimiter = (string) $delimiter;
  60.         return $this;
  61.     }
  62.  
  63. }

This class allows you to fetch the nested value using getPathValue and define your custom separator using the setPathDelimiter method. The latter one is not really needed unless you want to change the delimiter dynamically (e.g. to a dot and access values using Zend_Config_Ini notation of path.to.value instead of path/to/value). You can simply change the value of ArrayObjectExt->_pathDelimiter to whatever you want and remove the setPathDelimiter method altogether if you know you will never need to use a different delimiter.

Let's look at an eample on how to use the method:

  1. // create a test array
  2. $array = array(
  3.     'a' => array(
  4.         'b' => array(
  5.             'c' => array(
  6.                 'd' => 1,
  7.             ),
  8.             'value of index 0',
  9.             'value of index 1' ,
  10.             'value of index 2' ,
  11.         ),
  12.     ),
  13. );
  14.  
  15. // ... and instantiate the ArrayObject extension with it
  16. $ext = new ArrayObjectExt($array);
  17.  
  18. // you might already have your own classes extending ArrayObject
  19. // in that case just copy and paste contents of this class into your code
  20.  
  21. // in order to get value of "d" we can do this:
  22. $d = $ext->getPathValue('a/b/c/d');

As expected the function returns us "1". If the path does not exist however, the value of $default will be returned:

  1. $result = $ext->getPathValue('this/path/does/not/exist');

Value of $result will be null as that is the initial value of $default. You can specify our own value by passing it into the second parameter of the method:

  1. $result = $ext->getPathValue('this/path/does/not/exist', 'return_me');

The value of $result after this call will be "return_me".

You can also access numeric indices. This can easily be achieved by passing the indice as part of the path like this:

  1. $result = $ext->getPathValue('a/b/2');

This is equivalent to $ext['a']['b'][2] and will return "value of index 2".

This approach could be useful in loops where you could do something like:

  1. $i = 0;
  2. do {
  3.     $path = sprintf('a/b/%d', $i++);
  4.     $value = $ext->getPathValue($path);
  5.     // do something with the value
  6. } while ($value !== null);

Procedural approach

Probably most of the people will not want any custom extensions to the native SPL ArrayObject and would rather have some simple drop-in function.

It is very easy to modify the method defined in the previous section. All we need to do is prepend a parameter to the function allowing us to pass the array in and move the dellimiter declaration into the function body. The resulting function looks like this:

  1. <?php
  2.  
  3. /**
  4.  * Get value of an array by using "root/branch/leaf" notation
  5.  *
  6.  * @param array $array   Array to traverse
  7.  * @param string $path   Path to a specific option to extract
  8.  * @param mixed $default Value to use if the path was not found
  9.  * @return mixed
  10.  */
  11. function array_path_value(array $array, $path, $default = null)
  12. {
  13.     // specify the delimiter
  14.     $delimiter = '/';
  15.  
  16.     // fail if the path is empty
  17.     if (empty($path)) {
  18.         throw new Exception('Path cannot be empty');
  19.     }
  20.  
  21.     // remove all leading and trailing slashes
  22.     $path = trim($path, $delimiter);
  23.  
  24.     // use current array as the initial value
  25.     $value = $array;
  26.  
  27.     // extract parts of the path
  28.     $parts = explode($delimiter, $path);
  29.  
  30.     // loop through each part and extract its value
  31.     foreach ($parts as $part) {
  32.         if (isset($value[$part])) {
  33.             // replace current value with the child
  34.             $value = $value[$part];
  35.         } else {
  36.             // key doesn't exist, fail
  37.             return $default;
  38.         }
  39.     }
  40.  
  41.     return $value;
  42. }

As you can see it is exactly the same code with some minor modificaitons.

I suspect many people will ask what the point in all this is if we can access cells using the square bracket notation. My answer to that is

  1. these functions eliminate the hassle of checking if the key isset and
  2. they add the capability to define default values if the key is not found
  3. one has to type far less to access deeper cells. Compare something like $array['a']['b']['c']['d']['e'] to 'a/b/c/d/e' (or 'a.b.c.d.e', or 'a|b|c|d|e' depending on what you like).

This is more of an idea of what could be done to improve everyday things like array access. Obvioulsy, it won't be useful to everyone but hopefully someone will use it in their project or learn something from all this.

Having this code also makes it easy to add functionality of setters too. You could add a method setPathValue($path, $value) to the ArrayObjectExt class, which would allow you setting a nested value instead of getting it. Please leave your comments if you would be interested in having that function too.

Comments
1
very useful tuts, thanks!
I also interested to know how to set the path value as well :) thanks again.
bukak, October 15th 2011, 6:39
2
Andris, October 17th 2011, 14:05
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