【问题标题】:PHP Traits: How to circumvenient constructors or force them to be called?PHP Traits:如何绕过构造函数或强制调用它们?
【发布时间】:2018-04-26 11:10:10
【问题描述】:

看看以下特征:

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

}

如您所见,该 trait 确保 using 类拥有某个模型实例并实现某些方法。 只要实现类不覆盖构造函数,此方法就可以工作。

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

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

【问题讨论】:

  • final public function __construct() 防止实现类覆盖构造函数?
  • 很好,但是如果实现类无论如何都应该有一个构造函数,并且确保也调用了 trait 构造函数呢?
  • 那么你必须“相信”实现类调用特征构造函数;如果您允许实现类拥有自己的构造函数,则无法在代码中强制执行它
  • 为什么不在 getter 中实例化属性 primaryModel 呢? public function getPrimaryModel() { if ($this->primaryModel === null) { ...} return $this->primaryModel; }

标签: php inheritance constructor traits


【解决方案1】:

我认为您正在尝试使该 trait 完成它不适合的工作。

特征不是多重继承的一种形式,而是“水平重用”——它们通常被描述为“编译器辅助复制和粘贴”。因此,特征的工作是提供一些代码,这样您就不必手动将其复制到类中。它的唯一关系是与出现use 语句的类,其中代码被“粘贴”。为了帮助这个角色,它可以对目标类提出一些基本要求,但在那之后,特征不参与继承

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

Sub 类的以下定义完全等价:

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 {
}

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

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

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

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

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

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

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

目标类也可能使用“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 类实现此模式,但它已启用它,以符合其促进代码重用的目的。

有趣的是,下面的代码不起作用,因为as关键字添加一个别名,而不是重命名正常的方法,所以这最终试图覆盖 Mixed 中的 final 构造函数:

class ChildOfMixed extends Mixed {
    use TestTwo { __construct as private testTwoConstructor; }

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

【讨论】:

  • 肯定有一些有趣的点我会对此进行一些实验。
【解决方案2】:

使用基类,这将让您将 trait 作为父级处理。

<?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;
    }
}

注意:这只是一个解释,以便您更好地了解Traits 的工作原理

更新

为避免冲突,您可以为 trait 方法使用别名。这样您就可以同时使用构造函数(来自 trait 和来自扩展类),您可以执行以下操作

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

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

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

【讨论】:

  • 谢谢,但是一旦存储尝试扩展一个类也有一个初始化其他东西的构造函数,这将失败。
【解决方案3】:

您可以使用接口注入模式:将接口iPrimaryModelRest 实现到使用特征PrimaryModelRest 的同一类中:

interface iPrimaryModelRest {
  public function init();
  public abstract function getPrimaryModelClass();
}

使用 trait 的类如下所示:

class cMyClass implements iPrimaryModelRest {
  use PrimaryModelRest;
}

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

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
    }
  }
}

接口用于确定类的能力,设置数据/运行相应的功能。如果我没记错的话,这种模式称为Service Locator

【讨论】:

    【解决方案4】:

    我需要一个用于数据库连接的特征。为了避免在 trait 中使用 __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('...');
      }
    }
    

    【讨论】:

      猜你喜欢
      • 2012-06-29
      • 2015-07-19
      • 1970-01-01
      • 2017-02-08
      • 2012-06-17
      • 1970-01-01
      • 2010-09-24
      • 2022-12-08
      • 1970-01-01
      相关资源
      最近更新 更多