PHP Trait:有没有一种适当的方法来确保使用Trait的类扩展了包含特定方法的超类?

21

来自 PHP 手册的示例 #2,http://php.net/manual/zh/language.oop5.traits.php 中提到:

<?php
class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayHello() {
        parent::sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
?>

这是正确的代码,但在该上下文中使用parent::是不安全的。假设我编写了自己的“hello world”类,该类不继承任何其他类:

<?php
class MyOwnHelloWorld
{
    use SayWorld;
}
?>

只有在调用sayHello()方法时,这段代码才会产生任何错误。这是不好的。

另一方面,如果Trait需要使用某个方法我可以将该方法编写为抽象方法,这是好的,因为它确保Trait在编译时正确使用。但是这并不适用于父类:

<?php
trait SayWorld
{
    public function sayHelloWorld()
    {
        $this->sayHello();
        echo 'World!';
    }

    public abstract function sayHello(); // compile-time safety

}

我想问的是:是否有一种方法能够确保(在编译时而不是运行时)使用某个特征的类将拥有parent::sayHello()方法?


你可以添加一个抽象方法来强制使用该特征的类具有所需的方法。这样它就变得独立于类层次结构(这就是特征背后的思想:P)。 - Jarry
5个回答

4
您可以检查$this是否扩展了特定的类或实现了特定的接口:
interface SayHelloInterface {
    public function sayHello();
}

trait SayWorldTrait {
    public function sayHello() {
        if (!in_array('SayHello', class_parents($this))) {
            throw new \LogicException('SayWorldTrait may be used only in classes that extends SayHello.');
        }
        if (!$this instanceof SayHelloInterface) {
            throw new \LogicException('SayWorldTrait may be used only in classes that implements SayHelloInterface.');
        }
        parent::sayHello();
        echo 'World!';
    }
}

class SayHello {
    public function sayHello() {
        echo 'Hello ';
    }
}

class First extends SayHello {
    use SayWorldTrait;
}

class Second implements SayHelloInterface {
    use SayWorldTrait;
}

try {
    $test = new First();
    $test->sayHello(); // throws logic exception because the First class does not implements SayHelloInterface
} catch(\Exception $e) {
    echo $e->getMessage();
}

try {
    $test = new Second();
    $test->sayHello(); // throws logic exception because the Second class does not extends SayHello
} catch(\Exception $e) {
    echo $e->getMessage();
}

4
不,没有这个功能。事实上,这个示例很糟糕,因为引入traits的目的是为了在不依赖继承的情况下为许多类引入相同的功能,并且使用“parent”不仅需要类具有父类,而且还应该具有特定的方法。
另外需要注意的是,“parent”调用不会在编译时进行检查,您可以定义一个简单的类,它不扩展任何内容并具有其中的方法,它将工作,直到其中一个这些方法被调用。

谢谢。我知道简单的类在编译时不会检查父调用,但这仍然是正确的代码。我的聪明IDE可以找到父调用,这意味着当我从PHP切换到强类型语言时它仍将是正确的代码。这使我可以安心入眠。但是,当涉及到多重继承时,我不知道还有哪种语言不能做到这一点。Scala中特质的概念允许双向继承。C++和Python类可以继承多个“父类”。 - Maciej Sz
所以我想寻找一种绕过PHP限制的方法,使我的代码更加类型安全一些。 - Maciej Sz

2
PHP编译阶段仅创建字节码。其他所有操作都在运行时完成,包括多态决策。因此,像下面这样的代码可以被编译:
class A {}
class B extends A {
    public function __construct() {
        parent::__construct();
    }
}

但是当运行时会爆炸:
$b = new B;

因此,你严格来说不能在编译时进行父级检查。你能做的最好的事情就是尽早将其推迟到运行时。正如其他答案所示,可以在 trait 方法内部使用 instanceof 来实现这一点。我个人更喜欢在我知道 trait 方法需要特定契约时使用类型提示。
所有这些都归结为 traits 的根本目的:编译时复制和粘贴。就是这样。Traits 什么也不知道关于契约。不知道多态性。它们只是提供了一种重用机制。当与接口配合使用时,它们非常强大,但有些冗长。
实际上,traits 的第一个学术讨论揭示了 traits 的想法,即 traits 应该是“纯”的,因为它们没有关于它们周围对象的任何知识:
引用: “Trait 本质上是一组纯方法,它作为类的构建块,是代码重用的原始单元。在这个模型中,类是由一组 Trait 组成的,通过指定连接 Trait 的粘合代码并访问必要的状态来组成的。”
“如果特性不邪恶,它们会很有趣”很好地总结了要点、陷阱和选项。我不像作者那样痛恨特性:当有意义时我会使用它们,并且始终与一个interface配对使用。PHP文档中的示例鼓励不良行为是不幸的。”

1
自从我提出这个问题以来,我实际上已经完全停止使用traits。并且在可能的情况下,我尝试重写旧代码以删除traits。你可以说我成长了。它们有时很有用,但只在极少数情况下。 - Maciej Sz

0

很遗憾,在PHP中没有一种强制使用特定类的trait的方法。但是在开发时,您可以使用注释注解来访问父类的方法或属性。为此,您可以使用@extends@implements@method@property注解。例如:

<?php

namespace App\Models\Traits;

use App\Models\User;
use Illuminate\Database\Concerns\BuildsQueries;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;


/**
 * @extends Model
 * @method Model|Builder setRole(string $role)
 *
 * // for example
 * @implements BuildsQueries
 * @method someExampleMethod()
 * @property $exampleProperty
 */
trait WithPermission
{
    public function scopeSetRole(Builder $query, string $role): void
    {
        $query->join('model_has_roles', function ($join) {
            $join
                ->on($this->getTable() . '.user_id', '=', 'model_has_roles.model_id')
                ->where('model_has_roles.model_type', User::class);
        })
            ->join('roles', 'model_has_roles.role_id', '=', 'roles.id')
            ->where('roles.name', $role);
    }

}


在此之后,您必须确信您已在适当的类中使用了该特性。在这个例子中,我从Model类中使用了getTable()方法,编辑器可以通过这种方式带出方法的自动完成。


0

我认为完全可以不使用traits的方法:

class BaseClass 
{
    public function sayHello() {
            echo 'Hello ';
    }
}

class SayWorld
{
    protected $parent = null;

    function __construct(BaseClass $base) {
        $this->parent = $base;
    }

    public function sayHelloWorld()
    {
        $this->parent->sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    protected $SayWorld = null;

    function __construct() {
        $this->SayWorld = new SayWorld($this);
    }

    public function __call ( string $name , array $arguments ) {
        if(method_exists($this->SayWorld, $name)) {
            $this->SayWorld->$name();
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接