PHP 反思:如何知道 method/property/constant 是否继承自 trait?

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

我想从列表 中排除特征的所有继承方法,这些方法在 class 中没有被覆盖 那么如何知道一个 class 成员是否继承自 trait?

是的,我可以这样检查:

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

但是如果 trait 方法在 class 中被覆盖怎么办?怎么知道呢? 一种方法是检查方法体是否相似,如果是,我可能会排除它, 但是有没有更好的方法来实现这一点?

重要提示

这只是因为 "academical" 兴趣,在实际情况下你不应该关心 - 方法是从哪里派生的,因为它与特征的概念相矛盾,例如透明替代。

此外,由于特征的工作方式,任何类型的此类操作都可能被视为 "hacky",因此不同 PHP 版本的行为可能会有所不同,我不建议依赖它.

区别:困难

在PHP的反射中,有getTraits() methods which will return ReflectionClass个实例,指向trait的反射。这可用于获取在特征中声明的所有方法,这些方法在 class 中使用。但是-不,这对您的问题没有帮助,因为无法区分 class.

中哪些方法被覆盖了

假设特征 X 与方法 foo()bar() 以及 class Z 与方法 bar()。然后您将能够知道方法 foo()bar() 是在特征中声明的,但是如果您尝试在 class Z 上使用 getMethods() - 你显然也会得到 foo()bar()。因此,直接你不能区分大小写。

区别:work-around

但是,是的,仍然有办法让它发挥作用。第一种方法 - 就像你提到的那样 - 尝试调查源代码。挺丑的,不过归根结底,这是唯一100%靠谱的解决方法了。

但是 - 不,还有另一种 "less ugly" 方法 - 检查 实例 ReflectionMethod classes,是为 class/traits 方法创建的。碰巧 PHP 将对 trait 方法使用相同的实例,但会覆盖在 class.

中声明的用于该方法的实例

这个"inspection"可以用spl_object_hash()来完成。简单设置:

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()';
    }
}

现在,获取两种情况的哈希值:

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()));
}

简而言之:它只是从 class 特征(第一个函数)或 class 本身(第二个函数)中获取所有方法,然后合并结果以获得 key=>value 映射,其中键是对象哈希和值是方法名称。

然后我们需要像这样在同一个实例上使用它:

$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));

所以结果,$traitOnlyMethods 将只包含那些派生自特征的方法。

对应的fiddle为here。但要注意结果 - 它们可能因版本而异,就像在 HHVM 中它不起作用(我假设因为 spl_object_hash 是如何实现的 - 无论哪种方式,依赖它都是不安全的对于对象区分 - 请参阅函数文档)。

所以,TD;DR; - 是的,即使没有源代码解析也可以(以某种方式)完成 - 但我无法想象为什么需要它作为特征旨在用于将代码替换为 class.

很抱歉,Alma Do 接受的答案是完全错误的

即使您克服了 spl_object_hash() 值被回收的问题,此解决方案也无法工作。这个问题可以通过将 get*MethodRefs() 函数重构为一个函数来解决,该函数计算两个结果并确保当 class 方法的类似对象是时,特征方法的 ReflectionMethod 对象仍然存在创建。这会阻止 spl_object_hash() 值的回收。

问题是,"PHP will use same instance for trait method" 的假设是完全错误的,而这种情况的出现恰恰是 "lucky" spl_object_hash() 回收的结果。 $traitRef->getMethod('someName') 返回的对象将 始终$classRef->getMethod('someName') 返回的对象不同,返回的集合中 ReflectionMethod 的相应实例也是如此通过 ->getMethods() 不管 方法 someName() 是否在 class 中被覆盖。这些对象不仅是不同的,它们甚至不会是 "equal":从 $traitRef 获得的 ReflectionMethod 实例将具有特征的名称作为其 [=19= 的值] 属性,而从$classRef得到的那个,会有class那里的名字

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

那时似乎只有基于解析器的方法才可行。

一个更简单的方法是 ReflectionMethod::getFileName()。这将 return 特征的文件名,而不是 class.

对于特征和class在同一个文件中的特殊情况,可以使用ReflectionMethod::getStartLine(),并将其与特征和class的开始和结束行进行比较。

对于特征和 class 以及方法都在同一行的特殊情况..哦拜托!