在 Yii 框架中,Behavior(行為)的設計初衷是將自身的屬性與方法「注入」到所依附的 Component(組件)類別中。這項機制究竟如何運作?Yii 透過 PHP 的魔術方法來實現:屬性注入依賴 __get() 與 __set(),方法注入則透過 __call()。
當我們嘗試讀取 $component->property1 時,若組件本身未定義該屬性,Yii 會在幕後執行 yii\base\Component::__get()。以下是關鍵程式碼片段:
public function __get($name) {
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
return $this->$getter();
} else {
// 這裡是與 yii\base\Object::__get() 最大的不同處
$this->ensureBehaviors();
foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) {
// 行為中的屬性必須為 public 才能直接存取
return $behavior->$name;
}
}
}
// 後續異常處理略...
}
與 yii\base\Object::__get() 直接拋出異常不同,Component::__get() 在找不到 getter 後,會繼續檢查已綁定的 Behavior。流程如下:
ensureBehaviors() 確保行為已綁定(此方法會初始化 _behaviors 陣列並掛載所有宣告的行為)。$this->_behaviors 陣列。canGetProperty() 判斷該屬性是否為可讀屬性,若是則直接返回 $behavior->$name。寫入屬性(__set())的邏輯與此類似,此處不再贅述。
方法注入同樣利用魔術方法 __call() 實現:
public function __call($name, $params) {
$this->ensureBehaviors();
foreach ($this->_behaviors as $object) {
if ($object->hasMethod($name)) {
return call_user_func_array([$object, $name], $params);
}
}
throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
}
流程同樣先確保行為已綁定,再遍歷 _behaviors,透過 hasMethod() 判斷方法是否存在,最後以 call_user_func_array() 執行呼叫。
為何 Behavior 中的 private、protected 成員雖通過檢查,實際卻無法存取?關鍵在於 yii\base\Object 中的三個判斷方法:
public function canGetProperty($name, $checkVars = true) {
return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name);
}
public function canSetProperty($name, $checkVars = true) {
return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name);
}
public function hasMethod($name) {
return method_exists($this, $name);
}
結論:
public/private/protected),canGetProperty()/canSetProperty() 皆會返回 true。但實際存取時,PHP 僅允許存取 public 成員,故非公有屬性雖通過檢查,最終仍會失敗。hasMethod() 僅檢查方法是否存在(含私有/保護方法),但 call_user_func_array() 僅能呼叫 public 方法。依賴注入容器是一個知道如何實例化並配置物件及其所有依賴物件的物件。Yii 透過 yii\di\Container 提供 DI 容器功能,支援三種類型的注入:
利用 PHP 型別提示,容器自動解析依賴並透過建構子注入:
class Foo {
public function __construct(Bar $bar) { }
}
$foo = $container->get('Foo');
// 等同於:
$bar = new Bar;
$foo = new Foo($bar);
透過配置陣列,在建立物件時注入依賴:
use yii\base\Object;
class Foo extends Object {
public $bar;
private $_qux;
public function getQux() { return $this->_qux; }
public function setQux(Qux $qux) { $this->_qux = $qux; }
}
$container->get('Foo', [], [
'bar' => $container->get('Bar'),
'qux' => $container->get('Qux'),
]);
使用註冊的回呼函數建立實例:
$container->set('Foo', function () {
return new Foo(new Bar);
});
$foo = $container->get('Foo');
使用 yii\di\Container::set() 註冊依賴,支援類名、介面名、別名、配置陣列或回呼:
$container = new \yii\di\Container;
// 註冊介面綁定
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
// 註冊別名與配置
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);
// 註冊單例
$container->setSingleton('yii\db\Connection', [ ... ]);
呼叫 $container->get('userLister') 時,容器會遞迴解析所有建構子型別提示的依賴,自動建立完整的物件圖。Yii 在入口腳本載入 Yii.php 時會建立全域 DI 容器(Yii::$container),Yii::createObject() 內部即委派給該容器。開發者可全域自訂核心類別預設值,例如:
\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);
亦可於控制器建構子宣告介面依賴,由容器自動注入實作類別:
class HotelController extends Controller {
protected $bookingService;
public function __construct($id, $module, BookingInterface $bookingService, $config = []) {
$this->bookingService = $bookingService;
parent::__construct($id, $module, $config);
}
}
// 於啟動階段註冊:
\Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');
Yii 的 Behavior 機制以魔術方法為核心,實現了屬性與方法的動態注入;DI 容器則提供了強大的依賴管理與自動解析能力。兩者結合,使 Yii 成為高度解耦、易於測試與擴充的框架。建議深入閱讀 Martin Fowler 關於 DI 與 Service Locator 的文章,以建立更完整的架構觀念。