PHP Reflection: How to know if a method/property/constant is inherited from trait?
Asked Answered
D

4

12

I want to exclude all inherited methods from trait(s) from the list that are not overriden in a class So how to know if a class member is inherited from trait?

Yes, I can check it like this:

    if ($trait->hasMethod($methodName)
        || $ref->getTraitAliases()[$methodName] !== null)
    {
        //
    }

But what if the trait method is overriden in a class? How to know it? One way is to check if method bodies are similar, if so, i may exclude it, but is there a better way to achieve this?

Dustpan answered 5/6, 2015 at 8:38 Comment(1)
Does this help: #9472383Anschluss
E
0

Important notes

This is only because of "academical" interest, in real situation you should not care about - from where method was derived as it contradicts the idea of traits, e.g. transparent substitution.

Also, because of how traits are working, any kind of such manipulations might be considered as "hacky", so behavior may differ across different PHP versions and I would not suggest to rely on that.

Distinction: difficulties

In reflection for PHP, there is getTraits() methods which will return ReflectionClass instance, pointing to reflection of trait. This may be used to fetch all methods, declared in traits, which are used in the class. However - no, it will not help in your question as there will be not possible to distinct which methods were then overridden in the class.

Imagine that there is trait X with methods foo() and bar() and there is class Z with method bar(). Then you will be able to know that methods foo() and bar() are declared in trait, but if you will try to use getMethods() on class Z - you will obviously get both foo() and bar() as well. Therefore, directly you can not distinct the case.

Distinction: work-aroud

However, yes, there is a way to still make it work. First way - is - like you've mentioned - try to investigate source code. It's quite ugly, but in the very end, this is the only 100% reliable way to resolve the matter.

But - no, there is another, "less ugly" way - to inspect instances on ReflectionMethod classes, that are created for class/traits methods. It happens that PHP will use same instance for trait method, but will override that one which is for the method, declared in class.

This "inspection" can be done with spl_object_hash(). Simple setup:

trait x
{
    public function foo()
    {
        echo 'Trait x foo()';
    }

    public function bar()
    {
        echo 'Trait x bar()';
    }
}

class z
{
    use x;

    public function foo()
    {
        echo 'Class foo()';
    }
}

And now, to fetch hashes for both cases:

function getTraitMethodsRefs(ReflectionClass $class)
{
    $traitMethods = call_user_func_array('array_merge', array_map(function(ReflectionClass $ref) {
        return $ref->getMethods();
    }, $class->getTraits()));
    $traitMethods = call_user_func_array('array_merge', array_map(function (ReflectionMethod $method) {
        return [spl_object_hash($method) => $method->getName()];
    }, $traitMethods));

    return $traitMethods;    
}

function getClassMethodsRefs(ReflectionClass $class)
{
    return call_user_func_array('array_merge', array_map(function (ReflectionMethod $method) {
        return [spl_object_hash($method) => $method->getName()];
    }, $class->getMethods()));
}

In short: it just fetches all methods from class trait (first function) or class itself (second function) and then merges results to get key=>value map where key is object hash and value is method name.

Then we need to use that on same instance like this:

$obj = new z;
$ref = new ReflectionClass($obj);

$traitRefs   = getTraitMethodsRefs($ref);
$classRefs   = getClassMethodsRefs($ref);

$traitOnlyHashes = array_diff(
    array_keys($traitRefs),
    array_keys($classRefs)
);

$traitOnlyMethods = array_intersect_key($traitRefs, array_flip($traitOnlyHashes));

So result, $traitOnlyMethods will contain only those methods, which are derived from trait.

The corresponding fiddle is here. But pay attention to results - they may be different from version to version, like in HHVM it just doesn't work (I assume because of how spl_object_hash is implemented - an either way, it is not safe to rely on it for object distinction - see documentation for the function).

So, TD;DR; - yes, it can be (somehow) done even without source code parsing - but I can not imagine any reason why it will be needed as traits are intended to be used to substitute code into the class.

Estate answered 12/6, 2015 at 14:28 Comment(2)
A situation where it is absolutely necessary to be able to answer the original question: annotation engines! A method in the trait can have docblock annotations and its override in the class can have docblock annotations too. The annotation engine must be able to combine annotations from the trait method and the override in the class.Rhetic
This solution is incorrect. It seemingly only worked by chance. If you swap the methods in the definition of trait x, i.e. define method bar() first and method foo() last, then this program erroneously reports foo() as the trait-only method. Oddly, there are reused values of spl_object_hash() between obviously distinct objects. Fiddle: 3v4l.org/X35VYRhetic
N
10

A simpler way to do this is ReflectionMethod::getFileName(). This will return the file name of the trait, not the class.

For the exotic case where trait and class are in the same file, one can use ReflectionMethod::getStartLine(), and compare this with start and end line of trait and class.

For the exotic case where trait and class and method are all on the same line.. oh please!

Numerable answered 28/8, 2017 at 6:27 Comment(0)
M
2

I am sorry but the accepted answer by Alma Do is completely wrong.

This solution cannot work even if you overcome the problem of spl_object_hash() values being recycled. This problem can be overcome by refactoring the get*MethodRefs() functions into one function that computes both results and ensures that the ReflectionMethod objects for the trait methods still exist when the analogous objects for the class methods are created. This prevents recycling of spl_object_hash() values.

The problem is, the assumption that "PHP will use same instance for trait method" is completely false, and the appearance of that happening resulted precisely from "lucky" spl_object_hash() recycling. The object returned by $traitRef->getMethod('someName') will always be distinct from the object returned by $classRef->getMethod('someName'), and so will be the corresponding instances of ReflectionMethod in collections returned by ->getMethods(), regardless of whether method someName() is overriden in the class or not. These objects will not only be distinct, they won't even be "equal": the ReflectionMethod instance obtained from $traitRef will have the name of the trait as the value of its class property, and the one obtained from $classRef will have the name of the class there.

Fiddle: https://3v4l.org/CqEW3

It would seem that only parser-based approaches are viable then.

Marceline answered 14/12, 2016 at 21:42 Comment(0)
E
0

Important notes

This is only because of "academical" interest, in real situation you should not care about - from where method was derived as it contradicts the idea of traits, e.g. transparent substitution.

Also, because of how traits are working, any kind of such manipulations might be considered as "hacky", so behavior may differ across different PHP versions and I would not suggest to rely on that.

Distinction: difficulties

In reflection for PHP, there is getTraits() methods which will return ReflectionClass instance, pointing to reflection of trait. This may be used to fetch all methods, declared in traits, which are used in the class. However - no, it will not help in your question as there will be not possible to distinct which methods were then overridden in the class.

Imagine that there is trait X with methods foo() and bar() and there is class Z with method bar(). Then you will be able to know that methods foo() and bar() are declared in trait, but if you will try to use getMethods() on class Z - you will obviously get both foo() and bar() as well. Therefore, directly you can not distinct the case.

Distinction: work-aroud

However, yes, there is a way to still make it work. First way - is - like you've mentioned - try to investigate source code. It's quite ugly, but in the very end, this is the only 100% reliable way to resolve the matter.

But - no, there is another, "less ugly" way - to inspect instances on ReflectionMethod classes, that are created for class/traits methods. It happens that PHP will use same instance for trait method, but will override that one which is for the method, declared in class.

This "inspection" can be done with spl_object_hash(). Simple setup:

trait x
{
    public function foo()
    {
        echo 'Trait x foo()';
    }

    public function bar()
    {
        echo 'Trait x bar()';
    }
}

class z
{
    use x;

    public function foo()
    {
        echo 'Class foo()';
    }
}

And now, to fetch hashes for both cases:

function getTraitMethodsRefs(ReflectionClass $class)
{
    $traitMethods = call_user_func_array('array_merge', array_map(function(ReflectionClass $ref) {
        return $ref->getMethods();
    }, $class->getTraits()));
    $traitMethods = call_user_func_array('array_merge', array_map(function (ReflectionMethod $method) {
        return [spl_object_hash($method) => $method->getName()];
    }, $traitMethods));

    return $traitMethods;    
}

function getClassMethodsRefs(ReflectionClass $class)
{
    return call_user_func_array('array_merge', array_map(function (ReflectionMethod $method) {
        return [spl_object_hash($method) => $method->getName()];
    }, $class->getMethods()));
}

In short: it just fetches all methods from class trait (first function) or class itself (second function) and then merges results to get key=>value map where key is object hash and value is method name.

Then we need to use that on same instance like this:

$obj = new z;
$ref = new ReflectionClass($obj);

$traitRefs   = getTraitMethodsRefs($ref);
$classRefs   = getClassMethodsRefs($ref);

$traitOnlyHashes = array_diff(
    array_keys($traitRefs),
    array_keys($classRefs)
);

$traitOnlyMethods = array_intersect_key($traitRefs, array_flip($traitOnlyHashes));

So result, $traitOnlyMethods will contain only those methods, which are derived from trait.

The corresponding fiddle is here. But pay attention to results - they may be different from version to version, like in HHVM it just doesn't work (I assume because of how spl_object_hash is implemented - an either way, it is not safe to rely on it for object distinction - see documentation for the function).

So, TD;DR; - yes, it can be (somehow) done even without source code parsing - but I can not imagine any reason why it will be needed as traits are intended to be used to substitute code into the class.

Estate answered 12/6, 2015 at 14:28 Comment(2)
A situation where it is absolutely necessary to be able to answer the original question: annotation engines! A method in the trait can have docblock annotations and its override in the class can have docblock annotations too. The annotation engine must be able to combine annotations from the trait method and the override in the class.Rhetic
This solution is incorrect. It seemingly only worked by chance. If you swap the methods in the definition of trait x, i.e. define method bar() first and method foo() last, then this program erroneously reports foo() as the trait-only method. Oddly, there are reused values of spl_object_hash() between obviously distinct objects. Fiddle: 3v4l.org/X35VYRhetic
T
0

I had a similar problem and this is how I solved it:

function isFromTrait(string $methodName, $class): bool
{
    if (! $class instanceof ReflectionClass) {
        $class = new ReflectionClass($class);
    }

    $method = $class->getMethod($methodName);

    $classFilename = $class->getFileName();
    $classStartLine = $class->getStartLine();
    $classEndLine = $class->getEndLine();

    $methodFilename = $method->getFilename();
    $methodStartLine = $method->getStartLine();
    $methodEndLine = $method->getEndLine();

    if (
        empty($classFilename) || empty($methodFilename) ||
        empty($classStartLine) || empty($classEndLine) ||
        empty($methodStartLine) || empty($methodEndLine)
    ) {
        throw new Exception();
    }

    if (
        strcmp($classFilename, $methodFilename) === 0 &&
        $methodStartLine >= $classStartLine &&
        $methodEndLine <= $classEndLine
    ) {
        return false;
    }

    foreach ($class->getTraits() as $trait) {
        foreach ($trait->getMethods() as $traitMethod) {
            if ($traitMethod->name === $method->name) {
                return true;
            }
        }
    }

    return false;
}

We can use the getFilename method and then check the lines as mentioned by donquixote answer.

I added an iteration through the target class's traits just to be safe.

The exception throw in the middle of the function is because getStartLine, getEndLine and getFilename can return false.

The function will return true if the method is implemented in the trait and return false if the method is overridden by the target class.

Trigonometry answered 29/5 at 22:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.