如何在PHP >= 5.4中的特质中重载类构造函数

82

在 PHP 5 中,我可以重载构造函数(和其他任何方法)。但如果我得到像下面这样的代码:

class Base {

    public function __construct($a, $b) {
        echo $a+$b;
    }


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


trait SayWorld {

    public function __construct($a, $b, $c = 0) {
        echo (int)$c * ($a+$b);
    }

    public function sayHello($a = null) {
        parent::sayHello();
        echo 'World!'.$a;
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld(2, 3);
$o->sayHello(1);

我遇到了一个错误:

致命错误:MyHelloWorld有来自特性的冲突构造函数定义

我该如何解决?你可以在这里测试我的代码。


1
只是一个警告。Trait别名会导致PHP在5.4.7版本中崩溃,特别是与自动加载器一起使用时。已经在repo中添加了修复程序,希望它会出现在下一个版本中。 - Matthew
3个回答

156

我认为目前实现你想要的唯一方法是:

class MyHelloWorld extends Base {

    use SayWorld {
        SayWorld::__construct as private __swConstruct;
    }

    public function __construct($a, $b, $c = 0)
    {
        $this->__swConstruct($a, $b, $c);
    }
}

编辑2:

基于我在 PHP 中处理特性的一年多经验,我的建议是:尽量避免在特性中编写构造函数,或者如果必须这样做,至少使它们不包含参数。将它们放在特性中与构造函数的一般思想相违背,即:构造函数应该是特定于所属类的。其他进化较高的高级语言甚至不支持隐式构造函数继承。这是因为构造函数比其他方法有更强的与类的关系。事实上,它们的关系非常强,以至于甚至LSP也不适用于它们。Scala 语言中的特性(Java 的一个非常成熟且符合 SOLID 原则的后继者),不能具有带参数的构造函数

编辑1:

PHP 5.4.11 中存在一个错误,实际上允许别名化超类方法。但这被 PHP 开发人员认为是不好的,因此我们仍然被困在我上面提出的繁琐解决方案中。但是该错误引发了有关如何处理此问题的讨论,我希望它将在未来的版本中得到解决。

与此同时,我一遍又一遍地遇到同样的问题。随着参数数量和 docblock 的行数重复多次使用特性而愤怒程度呈指数级增长。因此,我想出了以下模式,以尽可能地遵循 DRY 原则:

不要像这样重复整个参数集:

trait SayWorld {

    /**
     * This is a valid docblock.
     *
     * @param int $a Doc comment.
     * @param int $b Doc comment.
     */
    public function __construct($a, $b) {
        echo (int)$c * ($a+$b);
    }
}

class MyHelloWorld extends Base {

    use SayWorld {
        SayWorld::__construct as private __swConstruct;
    }

    /**
     * Repeated and unnecessary docblock.
     *
     * @param int $a Doc comment.
     * @param int $b Doc comment.
     * @param int $c Doc comment.
     */
    public function __construct($a, $b, $c = 0)
    {
        $this->__swConstruct($a, $b);
    }
}

我编写了一个类,很像 tuple(在C#Python用户中很常见的概念),并且将其用作无尽参数列表的替代。

class SayWorldConstructTuple
{
    public $a;

    public $b;

    public function __construct($a, $b)
    {
        $this->a = $a;
        $this->b = $b;
    }
}

class MyHelloWorld extends Base {

    use SayWorld {
        SayWorld::__construct as private __swConstruct;
    }

    /**
     * New and valid docblock.
     *
     * @param SayWorldConstructTuple $Tuple
     * @param int $c Additional parameter.
     */
    public function __construct(SayWorldConstructTuple $Tuple, $c = 0)
    {
        $this->__swConstruct($Tuple->a, $Tuple->b);
        $this->c = $c;
    }
}

注意:当元组构造函数参数更多,以及更多类使用元组时,此模式显然更为有用。

通过使用PHP的动态特性,它可以进一步实现自动化。


36
赞同“在特质中完全避免编写构造函数”的说法,附上解释。 - Dennis

7

尝试:

use SayWorld {
  Base::__construct insteadof SayWorld;
}

Ref: PHP Docs


1
抱歉,我的错误是我自信地认为我已经弄对了;)...已修复! - Martin
1
修复后,您的答案将返回逻辑状态,就像没有使用任何特征一样。 - Guy Fawkes
这个怎么表现?我使用了一个旧的构造函数,因为它似乎没有引发冲突。http://pastebin.com/zHktB5C0 - Martin
如果Base是一个普通类(即不是特质),如问题所述,那么这将导致以下致命错误:“致命错误:Class Base不是特质,在'as'和'insteadof'语句中只能使用特质...” - MrWhite
"insteadof"是一个可恶的东西,永远不要使用它。PHP的创建者最终会恢复理智,移除"insteadof",并且破坏你的代码。 - Szczepan Hołyszewski
显示剩余5条评论

5

虽然这篇文章有些旧了,但如果对任何人有帮助的话:

我遇到了类似的情况,但决定采用稍微不同的方法。我正在编写一个 WordPress 插件,想要传递插件信息(版本、名称、文本域等),但又不想在重构或扩展另一个类时更改每个文件,因此我创建了一个 trait,其中包含一个构造函数,它只调用用于类特定操作的 init 函数。

trait HasPluginInfoTrait{
    public function __construct() { 

        $this->plugin_name        = PLUGIN_NAME;
        $this->version            = PLUGIN_VERSION;

        if ( method_exists( $this, 'init' ){
            $this->init();
        }
    }
}

class SampleClass {
    use HasPluginInfoTrait;

    private function init(){
        // Code specific to SampleClass
    }
}

2
不错的想法,但是当向一个类添加多个特征时,这种做法就不再合理了,而这种情况并不罕见。 - jlh
1
这样做会更有意义,将方法放在trait中,例如PluginInfoInit(),然后在类构造函数中添加调用,如:if ( method_exists( $this, 'PlubinInfoInit' ) ) { $this->PluginInfoInit(); } 这将适用于多个traits,并且可以在添加trait时直接放入构造函数中。 - LavaSlider

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