淺析Yii2.0的行爲Behavior

概念理解:使用行爲(behavior)能夠在不修改現有類的狀況下,對類的功能進行擴充。 經過將行爲綁定到一個類,可使類具備行爲自己所定義的屬性和方法,就好像類原本就有這些屬性和方法同樣。 而不須要寫一個新的類去繼承或包含現有類。在功能上相似於 Traits ,達到相似於多繼承的目的。

行爲的實現demo

<?php

namespace common\components;


use yii\base\Component;

// 待綁定行爲的類
class MyClass extends Component
{
    
}


<?php


namespace common\components;


use yii\base\Behavior;

// 定義一個行爲類
class BehaviorTest extends Behavior
{
    const EVENT_AFTER_SAVE = 'eventAfterAttach';

    public $_val = '我是BehaviorTest裏面的公有屬性_val';

    public function getOutput()
    {
        echo '我是BehaviorTest裏面的公有方法getOutput';
    }

    public function events()
    {
        return [
            self::EVENT_AFTER_SAVE => 'afterAttach'
        ];
    }

    public function afterAttach()
    {
        echo '事件已觸發';
    }
}

// 在控制器或者命令行下調用

$myClass = new MyClass();

$myBehavior = new BehaviorTest();

// 將行爲綁定到 MyClass 的實例
$myClass->attachBehavior('test', $myBehavior);

// MyClass 實例調用行爲類中的屬性
echo $myClass->_val;

// MyClass 實例調用行爲類中的方法
$myClass->getOutput();

// MyClass 實例觸發行爲類中定義的事件
$myClass->trigger(BehaviorTest::EVENT_AFTER_SAVE);

行爲的綁定原理

咱們先來看一下 $myClass->attachBehavior('test', $myBehavior); 行爲綁定的時候作了哪些事情?好的,又是咱們的老朋友yii\base\Componentphp

// yii\base\Component 的部分代碼

private $_behaviors;

public function behaviors()
{
    return [];
}

public function attachBehavior($name, $behavior)
{
        $this->ensureBehaviors();
        return $this->attachBehaviorInternal($name, $behavior);
}

public function ensureBehaviors()
{
        if ($this->_behaviors === null) {
            $this->_behaviors = [];
            foreach ($this->behaviors() as $name => $behavior) {
                $this->attachBehaviorInternal($name, $behavior);
            }
        }
}

private function attachBehaviorInternal($name, $behavior)
{
    if (!($behavior instanceof Behavior)) {
        $behavior = Yii::createObject($behavior);
    }
    if (is_int($name)) {
        $behavior->attach($this);
        $this->_behaviors[] = $behavior;
    } else {
        if (isset($this->_behaviors[$name])) {
            $this->_behaviors[$name]->detach();
        }
        $behavior->attach($this);

        $this->_behaviors[$name] = $behavior;
    }

    return $behavior;
}

$myClass 調用 attachBehavior() 傳入倆個參數,一個是行爲名稱,另外一個是行爲類的名稱或實例或者是數組,接着 attachBehavior() 調用了 ensureBehaviors(),這個函數咱們暫時用不到,由於咱們沒有在MyClass裏面重載behaviors(),不過也能大概猜到ensureBehaviors()的用途了。再往下調用的是私有函數attachBehaviorInternal(),這個函數先判斷傳進來的$behavior 是否已實例化,若是尚未則進行實例化,再經過$name判斷是匿名行爲仍是命名行爲,若是是命名行爲,須要查看是否已經綁定同名的行爲,若是綁定了同名的行爲,會將之前的行爲先解綁再調用$behavior->attach($this);[注:這裏$this指的MyClass實例,也就是$myClass],這樣咱們就來到了yii\base\Behaviorattach()方法,下面是attach()方法的源碼:html

public function attach($owner)
{
    $this->owner = $owner;
    foreach ($this->events() as $event => $handler) {
        $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
    }
}

$this->owner = $owner;[注:這裏$this 指的是$behavior也就是類BehaviorTest的實例],這句代碼指定了行爲類的主人是誰,後面的代碼看着彷佛似曾相識?是的,就是將行爲類events()方法裏面的事件也綁定的宿主身上,這裏不具體展開,有興趣的小夥伴能夠看一下淺析Yii2.0的事件Event。最後將行爲名稱和行爲實例放到$myClass的屬性_behavior中,至此,行爲的綁定就結束了。好像也沒幹什麼啊,咱們如今能夠打印一下$myClass的數據結構是怎樣的?segmentfault

common\components\MyClass Object
(
    [_events:yii\base\Component:private] => Array
        (
            [eventAfterAttach] => Array
                (
                    [0] => Array
                        (
                            [0] => Array
                                (
                                    [0] => common\components\BehaviorTest Object
                                        (
                                            [_val] => 我是BehaviorTest裏面的公有屬性_val
                                            [owner] => common\components\MyClass Object
 *RECURSION*
                                        )

                                    [1] => afterAttach
                                )

                            [1] => 
                        )

                )

        )

    [_eventWildcards:yii\base\Component:private] => Array
        (
        )

    [_behaviors:yii\base\Component:private] => Array
        (
            [test] => common\components\BehaviorTest Object
                (
                    [_val] => 我是BehaviorTest裏面的公有屬性_val                   
                    [owner] => common\components\MyClass Object
 *RECURSION*
                )

        )

)

能夠看到$myClass已經綁定了一個行爲test,綁定了一個事件eventAfterAttach,那麼綁定行爲之後,是怎麼調用行爲類裏面的屬性和方法呢?數組

行爲的使用原理

仍是看一下demo裏面$myClass->_val這句代碼是怎麼執行的?根據上面$myClass的數據結構能夠看出並無_val這個屬性,可是yii\base\Component裏面實現了__get()這個魔術方法,咱們看一下源碼。數據結構

public function __get($name)
{
    $getter = 'get' . $name;
    if (method_exists($this, $getter)) {
        // read property, e.g. getName()
        return $this->$getter();
    }

    // behavior property
    $this->ensureBehaviors();
    foreach ($this->_behaviors as $behavior) {
        if ($behavior->canGetProperty($name)) {
            return $behavior->$name;
        }
    }

    if (method_exists($this, 'set' . $name)) {
        throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
    }

    throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
}

又似曾相識?是的,跟yii\base\BaseObject裏面屬性的實現相似,有興趣的小夥伴能夠看一下淺析Yii2.0的屬性Property。而後直接看註釋behavior property部分,又去調用了ensureBehaviors(),先無論,接着又去遍歷_behaviors這個屬性,根據上面$myClass的數據結構得知,此時foreach裏面的$behavior就是行爲類common\components\BehaviorTest實例,先經過canGetProperty判斷_val是否可讀或者存在,你們能夠去看yii\base\BaseObject裏面該方法的實現。咱們這裏返回的是true,而後就直接經過common\components\BehaviorTest的實例$behavior返回_val的值。yii

根據上面獲取行爲類裏面屬性的流程咱們注意到:函數

  1. 由於是經過實例化行爲類去調用的屬性,因此屬性是protected或者是private是獲取不到的。
  2. 若是Component綁定了多個行爲,而且多個行爲中有同名的屬性,那麼該Component獲取的是第一個行爲類裏面的該屬性。

那麼行爲類裏面的方法是怎麼被調用的呢?屬性的調用是經過__get()來實現的,很容易聯想到方法的調用是經過__call()來實現的,咱們查看一下yii\base\BaseObject源碼,果真裏面實現__call()這個魔術方法,下面是源碼,而後對照上面$myClass的數據結構一看就明白了。須要注意的是,跟上面屬性的調用同樣,方法也必須是public的,protected 、private方法是調用不了的。this

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

注意到__call()裏面又有一個老朋友ensureBehaviors()這個函數彷佛無處不在?是的,查看一下yii\base\Component裏面的源碼咱們能夠發現全部的公有方法都調用了這個函數,那麼這個函數究竟是幹嗎的呢,其實demo裏面綁定行爲的方式能夠稱爲主動綁定,就是咱們主動調用函數attachBehavior()去綁定行爲的,對應的就是被動綁定了,實現方式就是在待綁定行爲的類裏面重載behaviors()這個函數就能夠實現綁定了,至關於一個行爲的配置項。倆種綁定方式看我的喜愛了,若是一個類須要綁定的行爲很明確,推薦使用配置項的方法去綁定,也就是被動綁定。下面是將demo裏面的綁定方式改爲被動綁定。spa

<?php

namespace common\components;


use yii\base\Component;

class MyClass extends Component
{

    public function behaviors()
    {
        return [
            'test' => new BehaviorTest()
        ];
    }
}

此時能夠將demo中 $myClass->attachBehavior('test', $myBehavior); 這句代碼去掉,$myClass也是一樣能夠調用類 BehaviorTest 裏面的屬性和方法

小結

這倆天查看了Yii2.0事件、行爲的實現方式,以爲有不少類似的地方,都是經過yii\base\Component來實現的,經過打印的數據結構也能夠看到,Component主要就是圍繞_events _eventWildcards _behaviors這三個屬性展開的,其中第二個屬性是 事件的通配符模式,也能夠歸屬到 事件中,那麼這樣Component的主要功能就是就是實現了 事件和行爲。而且實現原理上也是類似的,都是往Component裏面綁定事件和行爲的handle,而後觸發事件或者行爲的時候,再去回調相應的handle。不過在解除的時候雖然都是刪掉相應的handle,可是解除行爲還須要解除在綁定行爲的時候綁定的事件,這點不太同樣。命令行

以上總結參考了深刻理解Yii2.0,其實之前就看過,可是也只是侷限於看過,沒有本身跑demo調試、查看源代碼,而後就誤覺得本身明白了,其實過倆天什麼都不記得了。因此如今經過寫博客來加深本身的理解,因爲水平有限,歡迎小夥伴交流和指正。

相關文章
相關標籤/搜索