PHP Reflection: How to know if a method/property/c

2019-03-26 14:10发布

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?

3条回答
▲ chillily
2楼-- · 2019-03-26 14:55

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!

查看更多
姐就是有狂的资本
3楼-- · 2019-03-26 14:57

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.

查看更多
乱世女痞
4楼-- · 2019-03-26 15:01

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.

查看更多
登录 后发表回答