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

来源:https://www.cnblogs.com/minirice/archive/2018/03/13/yii2_behavior.html
-Advertisement-
Play Games

使用行為(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()中有代碼:

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,
        ];
    }
}

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


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

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

public function 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;
            }
        }
    }
}

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

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

因此,當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;
        }
    }
    ...
}

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

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

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

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

方法的註入

同屬性的註入類似,方法的註入也是自身的方法加上所有行為的方法:

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自身的面向對象特性(重載,魔術方法,成員變數/函數可見性)和一些數據結構,外加巧妙的演算法來實現的。因此“解剖”的目的就在於,解開這次神秘面紗,搞清楚內在邏輯,最終使得自己的編程能力得到切實的提高。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這是很正常的video寫法,我們查看微信是什麼樣式 這樣種video控制項樣式並不是我們想要的。所有我們需要自己來寫視頻封面的樣式。 常用的方式:自己來設置視頻封面的樣式。 第一步:找一張視頻封面,設置大小。 第二步:video隱藏。 第三步: 用js來監聽用戶是否點擊圖片。用戶點擊圖片,則video ...
  • 請參考Mongoose的文檔 1、findOneAndUpdate([query], [doc], [options], [callback]) 有callback傳遞才執行。 2、exec是promise的寫法,代替callback,和使用callback作用一樣。 3、ts可以使用await/a ...
  • 1:中國人民通過對 ASCII 編碼的中文擴充改造,產生了 GB2312 編碼,可以表示6000多個常用漢字。 2:漢字實在是太多了,包括繁體和各種字元,於是產生了 GBK 編碼,它包括了 GB2312 中的編碼,同時擴充了很多。 3:中國是個多民族國家,各個民族幾乎都有自己獨立的語言系統,為了表示 ...
  • HTML基本知識 學習html首先我們先看看HTML本質: web框架本質 我們在學socket,我們創建一個socketserver,然後運行起來,有一個client客戶端要連接socket服務端,連接上之後,如果兩邊都沒有close,就一直不斷開,可以不斷的進行請求。 那我們說一個網站,我們在服 ...
  • 簡而言之: HTTPS = HTTP + SSL HTTP 的 URL 以 http:// 開頭,而 HTTPS 的 URL 以 https:// 開頭 HTTP 是不安全的,而 HTTPS 是安全的 HTTP 標準埠是 80 ,而 HTTPS 的標準埠是 443 在 OSI 網路模型中,HTT ...
  • //假設有個全局對象Person var Person = { 'name' : 'alice' } //通過某種配置,獲得了字元串形式的對象名 var thisPerson = 'Person'; //需要取得該對象的值 var PersonName; //巧用window對象 PersonNam ...
  • 說完了出身,即出身自dojo/_base/目錄下的config模塊,那就要好好講講這對象有什麼可以寫的屬性了。 1. has屬性 官方說是用於更好的特征檢測的,具體有什麼用現在還不得知。 例如: 和 其中,dojo-amd-factory-scan子屬性表示AMD工廠掃描; dojo-firebug ...
  • 完整版文章請訪問我的個人博客查看:http://damienzhong.com/2018/03/12/%E5%BE%AE%E6%9C%8D%E5%8A%A1%EF%BC%88microservices%EF%BC%89%E3%80%90%E7%BF%BB%E8%AF%91%E3%80%91/ 微服務 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...