PHP 特点:如何规避构造函数或强制调用它们?

PHP Traits: How to circumvenient constructors or force them to be called?

看看以下特征:

trait PrimaryModelRest {

    use RestController;

    protected $primaryModel;

    public function __construct() {
        $mc = $this->getPrimaryModelClass();

        try {
            $this->primaryModel = new $mc();
            if(!($this->primaryModel instanceof Model)) {
                throw new ClassNotFoundException("Primary Model fatal exception: The given Class is not an instance of Illuminate\Database\Eloquent\Model");
            }
        } catch (Exception $e) {
            throw new WrongImplementationException("Primary Model Exception: Class not found.");
        }
    }

    /**
     * @return string: Classname of the primary model.
     */
    public abstract function getPrimaryModelClass();

    // various functions here

}

如您所见,特征确保 using class 持有特定模型实例并实现特定方法。只要实现 class 不覆盖构造函数.

,此方法就有效

所以这是我的问题:我想确保调用构造函数或更好的解决方案,以便我可以在初始化时实例化此模型。

请回答多重继承以及多级继承.

使用基数 class,这样您就可以将特征作为 parent 来处理。

<?php

trait StorageTrait
{
    public function __construct()
    {
        echo "Storage Trait";
    }
}

class StorageAttempt
{
    use StorageTrait;

    public function __construct()
    {
        parent::__construct();
        echo " - Storage Attempt";
    }
}


abstract class StorageBase
{
    use StorageTrait;
}

class MyStorage extends StorageBase
{
    public function __construct()
    {
        parent::__construct();
        echo ' - My Storage';
    }
}


new StorageAttempt(); // won't work - will trigger error
new MyStorage(); // will display "Storage Trait - My Storage"

此外,如果您正在使用特征,您还可以使用属性和 getter 和 setter。

示例:存储特性涉及将使用 Storage Engine。您可以添加 storageEngine 属性 及其 getter 和 setter。 (有或没有类型提示)

interface StorageEngineInterface{}

trait StorageTrait
{
    /**
     * @var StorageEngineInterface
     */
    protected $storageEngine;

    /**
     * @return StorageEngineInterface
     */
    public function getStorageEngine(): StorageEngineInterface
    {
        return $this->storageEngine;
    }

    /**
     * @param StorageEngineInterface $storageEngine
     */
    public function setStorageEngine(StorageEngineInterface $storageEngine)
    {
        $this->storageEngine = $storageEngine;
        return $this;
    }
}

Note: this is just an explanation so you can better understand how Traits work

更新

为避免冲突,您可以为特征方法使用别名。这样你就可以使用两个构造函数(来自特征和扩展class)你可以执行以下操作

class DifferentStorage
{
    public function __construct()
    {
        echo ' diff ';
    }
}

class MyDifferentStorage extends DifferentStorage
{
    use StorageTrait {
        StorageTrait::__construct as otherConstructor;
    }

    public function __construct()
    {
        parent::__construct();
        self::otherConstructor();
    }
}

我认为您正在尝试让该特征完成其设计目的之外的工作。

特征不是多重继承的一种形式,而是 "horizontal reuse" - 它们通常被描述为 "compiler-assisted copy-and-paste"。因此,特征的工作是 提供一些代码 ,这样您就不必手动将其复制到 class 中。它唯一的关系是 class,其中出现 use 语句,代码为 "pasted"。为了帮助这个角色,它可以对该目标 class 提出一些基本要求 ,但在那之后,该特征不参与继承.

在您的示例中,您担心子 class 可能会在没有 运行 初始化它的构造函数代码的情况下尝试访问 $primaryModel,并且您正在尝试使用强制执行的特征;但这实际上不是特质的责任。

下面classSub的定义是完全等价的:

trait Test {
    public function foo() {
        echo 'Hello, World!';
    }
}
class ParentWithTrait {
    use Test;
}
class Sub inherits ParentWithTrait {
}

对比:

class ParentWithMethodDefinition {
    public function foo() {
        echo 'Hello, World!';
    }
}
class Sub inherits ParentWithMethodDefinition {
}

在任何一种情况下,class Sub 都可以有自己的 foo() 定义,并绕过您在父 class 中编写的逻辑。

唯一可以防止这种情况的合同是 final 关键字,在您的情况下,这意味着将您的构造函数标记为 final。然后你可以提供一个扩展点,can 被子class覆盖以添加他们自己的初始化:

class Base {
    final public function __construct() {
         important_things(); // Always run this!
         $this->onConstruct(); // Extension point
    }
    protected function onConstruct() {
         // empty default definition
    }
}
class Sub {
    protected function onConstruct() {
         stuff_for_sub(); // Runs after mandatory important_things()
    }
}

特征也可以将其构造函数标记为最终的,但是这是正在粘贴的代码的一部分,而不是对使用特征的class 的要求。你实际上可以使用带有构造函数的特征,但随后也编写一个新的构造函数,它会完全掩盖特征的版本:

trait Test {
    final public function __construct() {
        echo "Trait Constructor";
    }
}
class Noisy {
    use Test;
}
class Silent {
    use Test;
    public function __construct() {
        // Nothing
    }
}

就 trait 而言,这就像买了一瓶啤酒然后把它倒进水槽:你要了它的代码却没有使用它,但那是你的问题。

不过,至关重要的是,您还可以 别名 特征的方法,创建具有相同代码但名称不同的新方法 and/or 不同的可见性。这意味着您可以混合来自声明构造函数的特征的代码,并在更复杂的构造函数中使用该代码,或者完全在 class 中的其他地方使用该代码。

目标 class 也可能使用 "final + hook" 模式:

trait TestOne {
    final public function __construct() {
        echo "Trait TestOne Constructor\n";
    }
}
trait TestTwo {
    final public function __construct() {
        echo "Trait TestTwo Constructor\n";
    }
}
class Mixed {
    final public function __construct() {
        echo "Beginning\n";
        $this->testOneConstructor();
        echo "Middle\n";
        $this->testTwoConstructor();
        echo "After Traits\n";
        $this->onConstruct();
        echo "After Sub-Class Hook\n";
    }
    use TestOne { __construct as private testOneConstructor; }
    use TestTwo { __construct as private testTwoConstructor; }

    protected function onConstruct() {
        echo "Default hook\n";
    }
}
class ChildOfMixed extends Mixed {
    protected function onConstruct() {
        echo "Child hook\n";
    }
}

特征没有强制 Mixed class 实现此模式,但它启用 它符合促进代码重用的目的。

有趣的是,下面的代码没有工作,因为as关键字添加一个别名,而不是重命名正常的方法,所以这最终试图从 Mixed:

覆盖 final 构造函数
class ChildOfMixed extends Mixed {
    use TestTwo { __construct as private testTwoConstructor; }

    protected function onConstruct() {
        $this->testTwoConstructor();
        echo "Child hook\n";
    }
}

您可以使用接口注入模式:将接口 iPrimaryModelRest 实现到使用特性 PrimaryModelRest:

的相同 class
interface iPrimaryModelRest {
  public function init();
  public abstract function getPrimaryModelClass();
}

使用特征的 class 看起来像这样:

class cMyClass implements iPrimaryModelRest {
  use PrimaryModelRest;
}

然后,每当 class 被实例化(不仅是自动加载)时,您可以像这样调用一个特殊的类似工厂的初始化函数:

class cMyApp {
  public function start() {
    /** @var cMyClass $oClass */  // enlighten IDE
    $oClass = $this->init(new cMyClass);
  }
  public function init($oClass) {
    if ($oClass instanceof iPrimaryModelRest) {$oClass->init();}
    if ($oClass instanceof whateverinterface) {
      // pass optional stuff, like database connection
    }
  }
}

接口用于判断class的能力,设置data/runs对应的功能。如果我没记错的话,这个模式叫做 Service Locator.

我需要一个数据库连接特征。为了避免在特征中使用 __construct,我使用了魔法 getter 代替:

trait WithDatabaseConnection
{
    public function __get(string $name)
    {
        if ($name === 'pdo') {
            return App::make(\PDO::class);
        }

        trigger_error("Property $name does not exist.");

        return null;
    }
}
class Foo {
  use WithDatabaseConnection;

  public function save() {
    $this->pdo->query('...');
  }
}