Capturing the calling class up the hierarchy in PH

2019-06-19 05:42发布

问题:

Preamble:

What I'm after is; if a method calls the get_typed_ancestor() method, the class name needed to perform the operations required in get_typed_ancestor() is the name of the class in which the calling method is defined.

Passing $this to extract the class name fails because it will resolve to the concrete class. If the calling method is defined in an abstract class higher up the hierarchy than the concrete class of which the instance is, we get the incorrect class name.

Looking for an instanceof static fails for the same reason as described above.

As described below, the purpose for capturing the class name in which the method is defined, is so that get_typed_ancestor() can find an instance of any class derived from the class in which the calling method is defined, not just another instance of the concrete class that initiated the call stack (hence $this and static being unsatisfactory)

So far, passing __CLASS__ to get_typed_ancestor() seems to the be the only solution thus far, as __CLASS__ will properly resolve to the class name in which the calling method is defined, not the class name of the instance invoking the calling method.


Note:

I've included at the end of this question an example showing the working __CLASS__ argument approach, and the failed static approach. If you wanna take a stab, perhaps use that as a start.


Question:

I've seen several "solutions" floating around that leverage debug_backtrace() to capture the calling class of a given method or function; these however are (as my quotation marks may suggest) not exactly solutions as far as I'm concerned, as debug_backtrace() used in this manner is a hack.

Rant aside, if this hack is the only answer, then hack I shall.

Anyways; I'm working on a set of classes that act as nodes in a bottom-to-top traversable tree. Here's a class hierarchy, simplified for brevity:

abstract class AbstractNode{}

abstract class AbstractComplexNode extends AbstractNode{}

class SimpleNode extends AbstractNode{}

class ComplexNodeOne extends AbstractComplexNode{}

class ComplexNodeTwo extends AbstractComplexNode{}

Nodes may have any concrete node (or null) as a parent. Looking at AbstractNode:

abstract class AbstractNode{

    protected $_parent;

    public function get_typed_ancestor(){
        // here's where I'm working
    }

    public function get_parent(){
        return $this->_parent;
    }

}

The method get_typed_ancestor() is where I'm at.

From other methods in the extending classes, get_typed_ancestor() is called to find the closest _parent of the class type to which that method belongs. This is better illustrated with an example; given the previous AbstractNode definition:

abstract class AbstractComplexNode extends AbstractNode{

    public function get_something(){
        if(something_exists()){
            return $something;
        }
        $node = $this->get_typed_ancestor();
        if(null !== $node){
            return $node->get_something();
        }
    }

}

The method get_typed_ancestor(), when called from the context of AbstractComplexNode::get_something(), will be looking for an object of type (or extending type) AbstractComplexNode -- in the case of this hierarchy, the possible concrete classes being ComplexNodeOne and ComplexNodeTwo.

Since AbstractComplexNode cannot be instantiated, a concrete instance such as ComplexNodeOne would be invoking get_something().

I need to highlight a point here; the search in this previous case must be for AbstractComplexNode in order to find the first instance of either ComplexNodeOne or ComplexNodeTwo. As will be explained in a moment, searching for and instanceof static will fail, as it may skip instances of sibling classes and/or their children.

The problem is, since there are situations where the calling class is abstract, and the calling method is inherited by (and thus is called from an instance of) a class such as ComplexNodeOne, searching for a parent that is an instanceofstatic doesn't work, as static is late-bound to the concrete ComplexNodeOne.

Now, I have a solution, but I don't like it:

abstract class AbstractNode{

    public function get_typed_ancestor($class){
        $node = $this;
        while(null !== $node->_parent){
            if($node->_parent instanceof $class){
                return $node->_parent;
            }
            $node = $node->_parent;
        }
        return null;
    }

}

abstract class AbstractComplexNode extends AbstractNode{

    public function get_something(){
        if(something_exists()){
            return $something;
        }
        $node = $this->get_typed_ancestor(__CLASS__);
        if(null !== $node){
            return $node->get_something();
        }
    }

}

This appears to work, since __CLASS__ resolves to the class name of definition. Unfortunately, I tried using __CLASS__ as a default argument to get_typed_ancestor() with no success (though that was expected)

I'm considering leaving the $class argument as an optional regardless, but if it is at all possible to "implicitly" pass this data along to the method (in absence of the optional argument) that would be great.


Solutions/Failures:

  • Passing __CLASS__ from the calling method as an argument to get_typed_ancestor().
            Works, but is not ideal as I'd like get_typed_ancestor() to resolve the calling class without being explicitly informed of it.

  • In the search loop, checking if($node->_parent instanceof static).
            Doesn't work when the calling class inherits the calling method. It resolves to the concrete class in which the method is called, not the one in which is defined. This failure of course applies also to self and parent.

  • Use debug_backtrace() to capture $trace[1]['class'] and use that for the check.
            Works, but is not ideal as it's a hack.

It's tricky discussing a hierarchical data structure and supporting class hierarchy without feeling like you're confusing your audience.


Example:

abstract class AbstractNode
{
    protected $_id;

    protected $_parent;

    public function __construct($id, self $parent = null)
    {
        $this->_id = $id;
        if(null !== $parent)
        {
            $this->set_parent($parent);
        }
    }

    protected function get_typed_ancestor_by_class($class)
    {
        $node = $this;
        while(null !== $node->_parent)
        {
            if($node->_parent instanceof $class)
            {
                return $node->_parent;
            }
            $node = $node->_parent;
        }
        return null;
    }

    public function get_typed_ancestor_with_static()
    {
        $node = $this;
        while(null !== $node->_parent)
        {
            if($node->_parent instanceof static)
            {
                return $node->_parent;
            }
            $node = $node->_parent;
        }
        return null;
    }

    public function set_parent(self $parent)
    {
        $this->_parent = $parent;
    }

}

class SimpleNode extends AbstractNode
{

}

abstract class AbstractComplexNode extends AbstractNode
{

    public function test_method_class()
    {
        var_dump($this->get_typed_ancestor_by_class(__CLASS__));
    }

    public function test_method_static()
    {
        var_dump($this->get_typed_ancestor_with_static());
    }

}

class ComplexNodeOne extends AbstractComplexNode
{

}

class ComplexNodeTwo extends AbstractComplexNode
{

}

$node_1 = new SimpleNode(1);
$node_2 = new ComplexNodeTwo(2, $node_1);
$node_3 = new SimpleNode(3, $node_2);
$node_4 = new ComplexNodeOne(4, $node_3);
$node_5 = new SimpleNode(5, $node_4);
$node_6 = new ComplexNodeTwo(6, $node_5);

// this call incorrectly finds ComplexNodeTwo ($node_2), skipping
// the instance of ComplexNodeOne ($node_4)
$node_6->test_method_static();
//    object(ComplexNodeTwo)#2 (2) {
//      ["_id":protected]=>
//      int(2)
//      ["_parent":protected]=>
//      object(SimpleNode)#1 (2) {
//        ["_id":protected]=>
//        int(1)
//        ["_parent":protected]=>
//        NULL
//      }
//    }

// this call correctly finds ComplexNodeOne ($node_4) since it's
// looking for an instance of AbstractComplexNode, resolved from
// the passed __CLASS__
$node_6->test_method_class();
//    object(ComplexNodeOne)#4 (2) {
//      ["_id":protected]=>
//      int(4)
//      ["_parent":protected]=>
//      object(SimpleNode)#3 (2) {
//        ["_id":protected]=>
//        int(3)
//        ["_parent":protected]=>
//        object(ComplexNodeTwo)#2 (2) {
//          ["_id":protected]=>
//          int(2)
//          ["_parent":protected]=>
//          object(SimpleNode)#1 (2) {
//            ["_id":protected]=>
//            int(1)
//            ["_parent":protected]=>
//            NULL
//          }
//        }
//      }
//    }

回答1:

For solving the problem "to capture the calling class of a given method or function", simply pass the object that creates the instance in the constructor.

<?php

class A {
  public function caller() {
    $b = new B ($this);
    $b->bar();
  }
}

class B {
  $whoClass = '';
  public function __construct($who)
  {
    $this->whoClass = get_class($who);
  }
  public function bar($who) {
    echo get_class($this->whoClass);
  }
}