1

Is it possible to use XPath syntax directly on PHP objects in order to navigate through the hierarchy of the object?

That is, can I use (2) instead of (1):

  1. $object->subObject1->subObject2
  2. $object['subObject1/subObject2'] (The expression in the brackets is the XPath.)

Additional question:

According to my current understanding, a conversion of an object into an ArrayObject doesn't make sense, because XPath cannot be used with ArrayObjects. Is this correct?

IMSoP
  • 89,526
  • 13
  • 117
  • 169
Anne Droid
  • 3,151
  • 4
  • 16
  • 15
  • Do you mean "does PHP have a built-in XPath-like shortcut for navigating objects?" (answer: no); or just "how can I implement an XPath-like shortcut for my own hierarchical objects?" – IMSoP Jul 29 '14 at 11:20
  • If the answer to "does PHP have a built-in XPath-like shortcut for navigating objects?" is no, then I am interested in "how can I implement an XPath-like shortcut for my own hierarchical objects?" – Anne Droid Jul 29 '14 at 11:26
  • You can implement the [`ArrayAccess`](http://php.net/manual/en/class.arrayaccess.php) interface for the objects you want to traverse. You could for example recursively use the provided methods. – Ulrich Thomas Gabor Jul 29 '14 at 11:55
  • I guess another question is how complex you want the "XPath" expressions to be. If it's just a case of specifying a path through child objects as a string (e.g. to allow dynamic access of some sort) then it would be relatively simple; but if you want to parse complex queries like `foo/bar[baz]/quux`, you're going to need something a lot more advanced. – IMSoP Jul 29 '14 at 12:00

2 Answers2

3

If all you need is basic traversal based on a /-separated path, then you can implement it with a simple loop like this:

  public function getDescendant($path) {
        // Separate the path into an array of components
        $path_parts = explode('/', $path);

        // Start by pointing at the current object
        $var = $this;

        // Loop over the parts of the path specified
        foreach($path_parts as $property)
        {
              // Check that it's a valid access
              if ( is_object($var) && isset($var->$property) )
              {
                    // Traverse to the specified property, 
                    // overwriting the same variable
                    $var = $var->$property;
              }
              else
              {
                    return null;
              }
        }

        // Our variable has now traversed the specified path
        return $var;
  }

To set a value is similar, but we need one extra trick: to make it possible to assign a value after the loop has exited, we need to assign the variable by reference each time:

  public function setDescendant($path, $value) {
        // Separate the path into an array of components
        $path_parts = explode('/', $path);

        // Start by pointing at the current object
        $var =& $this;

        // Loop over the parts of the path specified
        foreach($path_parts as $property)
        {
              // Traverse to the specified property, 
              // overwriting the same variable with a *reference*
              $var =& $var->$property;
        }

        // Our variable has now traversed the specified path,
        // and is a reference to the variable we want to overwrite
        $var = $value;
  }

Adding those to a class called Test, allows us to do something like the following:

$foo = new Test;
$foo->setDescendant('A/B', 42);

$bar = new Test;
$bar->setDescendant('One/Two', $foo);
echo $bar->getDescendant('One/Two/A/B'), ' is the same as ', $bar->One->Two->A->B;

To allow this using array access notation as in your question, you need to make a class that implements the ArrayAccess interface:

  • The above functions can be used directly as offsetGet and offsetSet
  • offsetExists would be similar to getDescendant/offsetGet, except returning false instead of null, and true instead of $var.
  • To implement offsetUnset properly is slightly trickier, as you can't use the assign-by-reference trick to actually delete a property from its parent object. Instead, you need to treat the last part of the specified path specially, e.g. by grabbing it with array_pop($path_parts)
  • With a bit of care, the 4 methods could probably use a common base.

One other thought is that this might be a good candidate for a Trait, which basically lets you copy-and-paste the functions into unrelated classes. Note that Traits can't implement Interfaces directly, so each class will need both implements ArrayAccess and the use statement for your Trait.

(I may come back and edit in a full example of ArrayAccess methods when I have time.)

IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • I think the whole point of the question is to use shortcuts like `One//B`. So, your implementation would need some tree searching (BFS/DFS). – Artjom B. Jul 29 '14 at 16:22
  • @ArtjomB. I did seek clarification on that point, but none was forthcoming, so I thought I'd write this out, in case it at least gave some ideas. – IMSoP Jul 29 '14 at 18:05
0

With some dependencies it should be (easily) possible supporting the complete set of XPath expressions. The only difficulty is to implement the walk over the object from the fully qualified XPath.

  1. Serialize the object to XML with something like XML_Serializer from PEAR.
  2. Load the created document as DOMDocument, run your arbitrary XPath expression and get the node path ($node->getNodePath()) from the selected elements as shown here
  3. Armed with a node path like /blah/example[2]/x[3] you can now implement a walk on the object recursively using object attribute iteration. This heavily depends on how the serializer from 1. actually works.

Note: I don't know if implementing the ArrayAccess interface is actually necessary, because you can access object attributes like $obj->$key with $key being some string that was sliced from the node path.

Community
  • 1
  • 1
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
  • Interesting approach; I guess the challenge in making it useful is coming up with an XML serialization that allows advanced XPath features to actually be meaningful. It's not clear to me how your example of `/blah/example[2]/x[3]` would translate to/from a graph of PHP objects (perhaps if some arrays are mixed in?). You also don't need to iterate the objects' properties once you've found your path, as shown in my answer. – IMSoP Jul 29 '14 at 18:24
  • I didn't have time to experiment with XML_Serializer yet, so I don't know how it will be useful or if it will be useful at all. – Artjom B. Jul 29 '14 at 19:10
  • I do not think this approach will perform well concerning memory and CPU consumption. Also this approach is literally immune to updates. Every update results in a new serialization and DOM parsing. – Ulrich Thomas Gabor Aug 05 '14 at 10:19
  • @GhostGambler This is true, but if implemented this would work on every object. Whereas [IMSoP's solution](http://stackoverflow.com/a/25016447/1816580) needs to implement those functions and build/instantiate the objects accordingly. Mine would be more flexible as a drop-in solution. – Artjom B. Aug 05 '14 at 10:24