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('...');
}
}
看看以下特征:
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
:
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('...');
}
}