Yii2基本概念之——行爲(Behavior)

使用行爲(behavior)可以在不修改現有類的情況下,對類的功能進行擴充。通過將行爲綁定到一個類,可以使得類具有行爲本身所具有的屬性和方法,就好像是類本來就具有的這些屬性和功能一樣。

好的代碼設計,必須要同時滿足可複用性、可維護性和可擴展性。設計原則中有一條非常重要的一條:類應該對擴展開放,對修改關閉。改變原有代碼往往會帶來潛在風險,因此我們儘量減少修改的行爲。我們的目標是允許類容易擴展,在不修改現有代碼的情況下,就可以搭配新的行爲。如果能實現這樣的目標,有什麼好處呢?這樣的設計具有彈性,可以應對改變,可以接收新的功能來應對改變的需求。

Yii的行爲就是這樣一類對象,當一個對象(繼承了Component的)想要擴展功能,又不想改變原有代碼時,那麼你完全可以用行爲去實現這些新功能,然後綁定到該對象上——完全是符合“開閉原則”的。
Yii的行爲都需要繼承自yii\base\Behavior,而能接受行爲綁定從而擴充自身功能的只能是yii\base\Component的子類,只繼承BaseObject基類沒有繼承Component的不能享受此“待遇”。因此,行爲是組件纔有的功能。行爲和事件結合起來使用,還可以定義組件在何種事件進行何種反饋。因此行爲有如下兩個作用:

  1. 將屬性和方法注入到一個component裏面,被訪問時和別的屬性或者方法訪問無異(行爲的附加)
  2. 響應component中觸發的事件,以便對某一事件作出反應(行爲的綁定和觸發,是對事件的應用)

定義行爲


行爲必須繼承自yii\base\Behavior,定義一個行爲仿照下面進行:

class MyBehavior extends \yii\base\Behavior
{
    public $prop1;
    private $_prop2;
    private $_prop3;

    //綁定事件和處理器,從而擴展類的功能表現,這裏體現了“行爲”字面意義
    public function events()
    {

    }

    //行爲的只讀屬性
    public function getProp2()
    {
        return $this->_prop2;
    }

    //行爲的只寫屬性
    public function setProp3($prop3)
    {
        $this->_prop3 = $prop3;
    }

    //行爲的方法
    public function foo()
    {
        return 'foo';
    }

    protected function bar()
    {
        return 'bar';
    }

}

接下來,將行爲附加到對象上,從而擴充對象的功能:

$user = new User();
//$user對像附加行爲,擴充功能
$user->attachBehavior('myBehavior', new MyBehavior());
//獲取prop2屬性
$user->prop2;
//給只讀屬性賦值會報錯
$user->prop2 = 3;
//給只寫屬性prop3賦值
$user->prop3 = 2;
//操作可讀-可寫屬性prop1
$user->prop1 = 1;
$var = $user->prop1;

// 使用方法foo
$user->foo();
// 不可訪問,這裏會拋出'Unknown Method'異常
$user->bar();

當然MyBehavior()完全可以支持依賴注入,從而在運行時決定這些屬性的值。

從上面可以看出,$user對象使用其MyBehavior的屬性和方法來幾乎毫不費勁,就像自己擁有這些屬性和方法一樣。但是,我們並沒有給User類中添加任何一行代碼,因此這個擴展做得真是悄無聲息啊!

行爲的附加


行爲的附加或者綁定,通常是由Component來發起。有兩種方式可以將一個Behavior綁定到一個 yii\base\Component 。 一種是靜態附加行爲,另一種是動態附加行爲。靜態附加在實踐中用得比較多一些,因爲一般情況下,在你的代碼沒跑起來之前,一個類應當具有何種行爲是確定的。 動態附加主要是提供了更靈活的方式,上面即是行爲的動態附加,但實際使用中並不多見。

靜態附加

class User extends ActiveRecord
{
    const MY_EVENT = 'my_event';
    public function behaviors()
    {
        return [           

            // 匿名行爲,只有行爲類名
            MyBehavior::className(),

            // 命名行爲,只有行爲類名
            'myBehavior2' => MyBehavior::className(),

            // 匿名行爲,配置數組
            [
                'class' => MyBehavior::className(),
                'prop1' => 'value1',
                'prop2' => 'value2',
            ],

            // 命名行爲,配置數組
            'myBehavior4' => [
                'class' => MyBehavior::className(),
                'prop1' => 'value1',
                'prop2' => 'value2',
            ]
        ];
    }
}

上面的數組響應的鍵就是行爲的名稱,這種行爲成爲命名行爲,沒有指定名稱的就成爲匿名行爲。

還有一個靜態的綁定辦法,就是通過配置文件來綁定:

[
    'class' => User::className(),
    'as myBehavior2' => MyBehavior::className(),
    'as myBehavior3' => [
        'class' => MyBehavior::className(),
        'prop1' => 'value1',
        'prop3' => 'value3',
    ],
]

通過這個配置文件獲取的User對象的實例,依然被附加了MyBehavior行爲。

動態附加

要動態附加行爲,在對應組件裏調用yii\base\Component::attachBehavior()方法即可,如:

use app\components\MyBehavior;
// 附加行爲——對象
$user->attachBehavior('myBehavior1', new MyBehavior);
// 附加行爲——類名
$user->attachBehavior('myBehavior2', MyBehavior::className());
// 附加行爲——配置數組
$user->attachBehavior('myBehavior3', [
    'class' => MyBehavior::className(),
    'prop1' => 'value1',
    'prop2' => 'value2',
]);

也可以通過yii\base\Component::attachBehaviors()同時附加多個行爲:

$myBehavior = new MyBehavior();
$user->attachBehaviors([
    'myBehavior1'=> $myBehavior,
    [
        'class' => MyBehavior2::className(),
        'prop1' => 'value1',
        'prop3' => 'value2',
    ],
    new MyBehavior3()
]);

附加多個行爲,那麼組件就獲得了所有這些行爲的屬性和方法。

不管是靜態附加還是動態附加,命名行爲都可以通過yii\base\Component::getBehavior($name)獲取出來,匿名行爲不可以單獨獲取出來,但是可以通過Component::getBehaviors()一次全部獲取出來。

行爲附加的原理

在Component內部,事件是通過私有屬性event _behavior來保存的:

private $_events = [];
private $_behaviors;

$_behaviors的數據結構:

$_behaviors 的數據結構

上圖中前面兩個是命名行爲,後面兩個是匿名行爲。數組的每個元素值都是Behavior的子類實例。

行爲附加涉及到四個方法:

Component::behaviors()
Component::ensureBehaviors()
Component::attachBehaviorInternal()
Behavior::attach()

Component::behaviors()用於供子類覆寫,比如:

public function behaviors()
{
    return [
        'timeStamp' => [
            'class' => TimeBehavior::className(),
            'create' => 'create_at',
            'update' => 'update_at',
        ],
    ];
}

yii\base\Component::ensureBehaviors()方法經常出現,它的作用是將各種動態的和靜態的方式附加的行爲變成標準格式(參看$_behaviors的數據結構):

 public function ensureBehaviors()
 {
     if ($this->_behaviors === null) {
         $this->_behaviors = [];
         // behaviors()方法由Component的子類重寫
         foreach ($this->behaviors() as $name => $behavior) {
             $this->attachBehaviorInternal($name, $behavior);
         }
     }
 }

接下來的第三個出場的attachBehaviorInternal(),我們看看是何方神聖:

 private function attachBehaviorInternal($name, $behavior)
 {
     //如果是配置數組,那就將其創建出來再說
     if (!($behavior instanceof Behavior)) {
         $behavior = Yii::createObject($behavior);
     }
     if (is_int($name)) { // 匿名行爲
         //先是行爲本身和component綁定
         $behavior->attach($this);
         //將行爲放進$_behaviors數組,沒有鍵值的是匿名行爲
         $this->_behaviors[] = $behavior;
     } else {  //命名行爲
         if (isset($this->_behaviors[$name])) {
             //命名行爲需要保證唯一性
             $this->_behaviors[$name]->detach();
         }         
         $behavior->attach($this);
         //命名行爲,鍵值就是行爲名稱
         $this->_behaviors[$name] = $behavior;
     }

     return $behavior;
 }

Yii中以Internal開頭或者結尾的,一般是私有方法,往往都是命門所在,如果要看源碼,這些都是核心邏輯實現的地方。

最後一個出場的是Behavior::attach(),Behavior有一個屬性$owner,指向是擁有它的組件,就是行爲的擁有者。組件和行爲是一個相互綁定、相互持有的過程。組件在$_behavior持有行爲的同時,行爲也在$owner中持有組件。因此,不管是行爲的附加還是解除都是雙方的事情,不是一方能說了算的。

public function attach($owner)
{
    //Behavior的$owner指向的是行爲的所有者
    $this->owner = $owner;
    //讓行爲的所有者$owner綁定用戶在Behavior::events()中所定義的事件和處理器
    foreach ($this->events() as $event => $handler) {
        $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
    }
}

行爲的解除
有附加當然有解除,命名行爲可以被單個解除,使用方法Component::detachBehavior($name),匿名行爲不可以單獨解除,但是可使用detachBehaviors()方法解除所有的行爲。

//解除命名行爲
$user->detachBehavior('myBehavior1');
//解除所有行爲
$user->detachBehaviors();

這上面兩種方法,都會調用到 yii\base\Behavior::detach() ,其代碼如下:

public function detachBehavior($name)
{
    $this->ensureBehaviors();
    if (isset($this->_behaviors[$name])) {
        $behavior = $this->_behaviors[$name];
        //1.將行爲從$owner的$_behaviors中刪除
        unset($this->_behaviors[$name]);
        //2.解除$owner的所有事件和其處理器
        $behavior->detach();
        return $behavior;
    }

    return null;
}

$behavior->detach()是這樣的:

public function detach()
{
    if ($this->owner) {
        //解綁$owner所有事件和其事件處理器
        foreach ($this->events() as $event => $handler) {
            $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
        }
        //$owner重新置爲null,表示沒有任何擁有者
        $this->owner = null;
    }
}

行爲所要響應的事件
行爲與事件結合後,可以在不對類作修改的情況下,補充類在事件觸發後的各種不同反應。因此,只需要重載 yii\base\Behavior::events()方法,表示這個行爲將對類的何種事件進行何種反饋即可:

class MyBehavior extends Behavior
{
    public $attr;

    public function events() //覆寫events方法
    {
        return [
            ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert', //將事件和事件處理器綁定
            User::MY_EVENT => [$object, 'methodName'],//自己定義的事件
        ];
    }

    //$event可以帶來三個信息,事件名,觸發此事件的對象(類或者實例),附加的數據
    public function beforeInsert($event) 
    { 
        $model = $this->owner;//訪問已附件的組件
        // Use $model->attr
    }

    public function methodName($event) 
    {
        $owner = $this->owner;//行爲的擁有者
        $sender = $event->sender//觸發此事件的類或者實例
        $data = $event->data;//觸發事件時傳遞的參數

        // Use $model->attr
    }
}

events()方法返回一個關聯數組,鍵是事件名,值是要響應的事件處理器。事件處理器可以是一下四種形式:

  • 此行爲中的方法methodName,等效爲[$this, 'methodName']
  • 對象的方法:[$object, 'methodName']
  • 類的靜態方法:['Page', 'methodName']
  • 閉包:function ($event) { ... }

這些方法中都會傳遞事件$event過來,通過$event你可以獲得事件名,觸發此事件的對象(類或者實例),附加的數據信息。詳見《Yii2基本概念之——事件(Event)》。

行爲響應事件的實例


Yii費了那麼大勁,主要就是爲了將行爲中的事件handler綁定到類中去。因爲在編程中用的最多的,也就是Component對各種事件的響應。通過行爲注入,可以在不修改現有類的代碼的情況下更改、擴展類對於事件的響應和支持。使用這個技巧,可以玩出很酷的花樣出來。
比如,Yii自帶的yii\behaviors\AttributeBehavior類,定義了在一個 ActiveRecord 對象的某些事件發生時, 自動對某些字段進行修改的行爲。它有一個很常用的子類 yii\behaviors\TimeStampBehavior 用於將指定的字段設置爲一個當前的時間戳。現在以它爲例子說明行爲的運用。
在 yii\behaviors\AttributeBehavior::event() 中,代碼如下:

 public function events()
 {
     return array_fill_keys(
         array_keys($this->attributes),
         'evaluateAttributes'
     );
 }
 ```
代碼很容易看懂,無需詳述。

而在`yii\behaviors\TimeStampBehavior::init()`中有代碼:
```PHP
public function init()
{
    parent::init();

    if (empty($this->attributes)) {
        //重點看這裏
        $this->attributes = [
            BaseActiveRecord::EVENT_BEFORE_INSERT => [$this->createdAtAttribute, $this->updatedAtAttribute],
            BaseActiveRecord::EVENT_BEFORE_UPDATE => $this->updatedAtAttribute,
        ];
    }
}




<div class="se-preview-section-delimiter"></div>

上面的這個方法是初始化$this->attributes這個數組。結合前面的兩個方法,返回的$event數組應該是這樣的:

return [
    BaseActiveRecord::EVENT_BEFORE_INSERT => 'evaluateAttributes',
    BaseActiveRecord::EVENT_BEFORE_UPDATE => 'evaluateAttributes',
];




<div class="se-preview-section-delimiter"></div>

這裏的意思是BaseActiveRecord::EVENT_BEFORE_INSERTBaseActiveRecord::EVENT_BEFORE_UPDATE都響應處理器evaluateAttributes。看看其關鍵部分:

public fun```PHPction evaluateAttributes($event)
{
    ...

    if (!empty($this->attributes[$event->name])) {       
        $attributes = (array) $this->attributes[$event->name];
        //這裏默認返回默認的時間戳time()
        $value = $this->getValue($event);
        foreach ($attributes as $attribute) {            
            if (is_string($attribute)) {
                if ($this->preserveNonEmptyValues && !empty($this->owner->$attribute)) {
                    continue;
                }
                //將其賦值給$owner的字段
                $this->owner->$attribute = $value;
            }
        }
    }
}




<div class="se-preview-section-delimiter"></div>

使用時,只需要在ActiveRecord裏面重載behaviors()方法:

public function behaviors()
{
    return [
        [
            'class' => TimestampBehavior::className(),
            'attributes' => [
                ActiveRecord::EVENT_BEFORE_INSERT => 'created_at',
                ActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at',
            ]
        ],
    ];
}




<div class="se-preview-section-delimiter"></div>

因此,當EVENT_BEFORE_INSERT事件觸發,
這樣,你在插入記錄時created_atupdated_at會自動更新,而在修改時updated_at會更新。

行爲的屬性和方法注入原理


通過以上各個例子,組件附加了行爲之後,就獲得了行爲的屬性和方法。那麼,這是如何實現的呢?歸根結底主要通過__set()__get()__call()這些魔術方法來實現的。屬性的注入靠的是__set()__get(),而方法的注入是靠__call()

屬性的注入

Component持有一個數組$_behavior,裏面都是Behavior子類,而Behavior繼承自Yii最基礎的BaseObject。在《Yii2基本概念之——屬性(property)》中我們介紹了屬性的概念,因此Behavior也是可以運用屬性的。

Component的可讀屬性,我們看看Component的getter函數:

public function __get($name)
{
    $getter = 'get' . $name;
    //這是自己的可寫屬性
    if (method_exists($this, $getter)) {        
        return $this->$getter();
    }

    /**下面是比BaseObject多出來的部分**/
    $this->ensureBehaviors();
    //依次檢查各個行爲中的可讀屬性
    foreach ($this->_behaviors as $behavior) {
        if ($behavior->canGetProperty($name)) {
            return $behavior->$name;
        }
    }
    ...
}




<div class="se-preview-section-delimiter"></div>

Component的可寫屬性,我們看看Component的setter函數:

public function __set($name, $value)
{
    $setter = 'set' . $name;
    //自己的可寫屬性
    if (method_exists($this, $setter)) {        
        $this->$setter($value);
        return;
    } elseif (strncmp($name, 'on ', 3) === 0) {         
        $this->on(trim(substr($name, 3)), $value);

        return;
    } elseif (strncmp($name, 'as ', 3) === 0) {        
        $name = trim(substr($name, 3));
        $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));

        return;
    }

    $this->ensureBehaviors();
    //依次檢查各個行爲中是否有可寫屬性$name
    foreach ($this->_behaviors as $behavior) {
        if ($behavior->canSetProperty($name)) {
            $behavior->$name = $value;
            return;
        }
    }
    ...
}




<div class="se-preview-section-delimiter"></div>

對於setter函數,略微複雜,檢查順序依次是:

  • 自己的setter函數,也即是自己的可寫屬性
  • 如果$name是’on xyz’形式,則xyz作爲事件,$value作爲handler,將其綁定
  • 如果$name是’as xyz’形式,則xyz作爲行爲名字,$value作爲行爲,將其附加
  • 依次檢查各個行爲中是否有可寫屬性$name,返回第一個;如果沒有則拋出異常

因此,Component的可讀屬性就是本身的可讀屬性加上所有行爲的可讀屬性;而可寫屬性就是本身的可寫屬性加上所有行爲的可寫屬性

方法的注入

同屬性的注入類似,方法的注入也是自身的方法加上所有行爲的方法:
PHP
public function __call($name, $params)
{
$this->ensureBehaviors();
//遍歷所有行爲的方法
foreach ($this->_behaviors as $object) {
if ($object->hasMethod($name)) {
return call_user_func_array([$object, $name], $params);
}
}
...

這裏以爲最終調用的是call_user_func_array()的函數,所以只有行爲的public 方法才能被注入組件中。
除了屬性的讀和寫,還有對屬性的判斷(isset)和註銷(unset),分別通過對魔術方法__isset__unset的重載來實現,這裏就不多贅述了。

結語
屬性,事件和行爲是Yii的基礎功能,它們使得Yii成爲一個變化無窮、魅力無窮的框架。然而,框架不能做PHP本身都做不到的事情,它酷炫的功能無非是PHP自身的面向對象特性(重載,魔術方法,成員變量/函數可見性)和一些數據結構,外加巧妙的算法來實現的。因此“解剖”的目的就在於,解開這次神祕面紗,搞清楚內在邏輯,最終使得自己的編程能力得到切實的提高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章