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 instanceof
static
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 toget_typed_ancestor()
.
Works, but is not ideal as I'd likeget_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 toself
andparent
.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
// }
// }
// }
// }