對Yii框架中行爲【behavior】的理解

什麼是行爲

所謂行爲,其實說白了,就是把A類綁定到B類上,可以在不修改B類的情況下,對B類進行功能和屬性的擴充,這樣B類就擁有了A類的屬性和方法,只是在這裏,我們習慣成A類爲行爲。

舉一個例子:

class MyClass
{
    public $hello = "我是類的自有屬性hello".PHP_EOL;

    public function hello()
    {
        echo "我是類的自有方法hello".PHP_EOL;
    }
}

class MyBehavior
{

    public $hi = "我是行爲的自有屬性hi".PHP_EOL;

    public function hi()
    {
        echo "我是行爲的自有方法hi".PHP_EOL;
    }
}

$class = new MyClass();
$behavior = new MyBehavior();

$class->attachBehavior('behavior1',$behavior);//將類和行爲進行綁定,這裏爲$behavior綁定了一個別名:behavior1

echo $class->hello;//輸出:我是類的自有屬性hello
$class->hello();//輸出:我是類的自有方法hello

echo $class->hi;//期望輸出:我是行爲的自有屬性hi
$class->hi();//期望輸出:我是行爲的自有方法hi

實現自己的行爲

實現行爲的關鍵知識點:__get(),__set(),__call(),call_user_func_array,前三個是PHP的魔術方法,這四個是非常重要的知識點

以上僞代碼就是行爲的實現,現在我們來動手實現自己的行爲:

//定義一個基類,後面的類都來繼承它
class Base
{
    //存放行爲類的容器數組
    private $_behaviors = [];

    //綁定行爲類:其實就是放入數組$_beheviors
    public function attachBehavior($name,$behavior)
    {
        //如果$_beheviors裏有這個別名的話,先unset掉,給新綁定的行爲讓路
        if(isset($this->_behaviors[$name])) {
            unset($this->_behaviors[$name]);
        }
        $this->_behaviors[$name] = $behavior;
    }

    
    public function __get($name)
    {
        foreach($this->_behaviors as $behavior) {
            if($behavior->hasProperty($name)) {
                return $behavior->$name;
            }
        }
    }

    public function __set($name,$value)
    {
        foreach($this->_behaviors as $behavior) {
            if($behavior->hasProperty($name)) {
                $behavior->$name = $value;
            }
        }
    }

    public function __call($method,$params)
    {
        foreach($this->_behaviors as $behavior) {
            if($behavior->hasMethod($method)) {
                call_user_func_array([$behavior,$method],$params);
            }
        }
    }
}

//定義行爲基類,所有的行爲類都繼承它
class Behavior
{
    //判斷該行爲類是否有$name屬性
    public function hasProperty($name)
    {
        return property_exists($this,$name);
    }

    //判斷該行爲類是否有$method方法
    public function hasMethod($method)
    {
        return method_exists($this,$method);
    }
}

定義好這兩個基類之後,我們還要修改一下MyClass和MyBehavior:


class MyClass extends Base
{
    // 讓Myclass繼承自Base,才能綁定行爲
    ···
}


class MyBehavior extends Behavior
{
    // 讓MyBehavior繼承自Behavior
    ···
}

然後我們繼續運行我們的代碼:

$class = new MyClass();
$behavior = new MyBehavior();
$class->attachBehavior('behavior1',$behavior);

echo $class->hello;// 輸出:我是類的自有屬性hello
$class->hello();// 輸出:我是類的自有方法hello

echo $class->hi;// 輸出:我是行爲的自有屬性hi
$class->hi();// 輸出:我是行爲的自有方法hi

// 這裏動態去設置行爲的屬性
$class->hi = "我是重新設置好的行爲的自有屬性hi".PHP_EOL;
echo $class->hi;// 輸出:我是重新設置好的行爲的自有屬性hi

看到了嗎,我們想要的期望輸出,這回變成現實了,整體還是很好理解的。

這裏沒有考慮行爲的方法和屬性的權限問題,例子裏都寫成了Public,只是方便舉例和測試,實際開發中肯定要進行權限控制判斷的。

Yii中的行爲

如何使用Yii中的行爲

  • 由於在Yii中,實現行爲的功能是yii\base\Component中的,所以想實現行爲,就必須從yii\base\Component繼承;
  • 從yii\base\Behavior派生自己的行爲類;
  • 將Component和Behavior綁定起來;
  • 像使用Component自己的屬性和方法一樣,使用綁定好的行爲中的屬性和方法;

行爲的綁定

在Yii中,一個行爲的綁定實現步驟是這樣的:

* yii\base\Component::behaviors();             //第一步
* yii\base\Component::ensureBehaviors();       //第二步
* yii\base\Behavior::attach();                 //第三步

在yii項目實際的開發中,我們很少手動去綁定行爲,一般都是定義一個數組,如下所示:

class MyClass
{
    // 定義好的要綁定的行爲數組
    public function behaviors()
    {
        return [
            // 匿名的行爲,僅給出行爲的類名
            MyBehavior::className(),

            // 名爲myBehavior2的行爲,也是僅給出行爲的類名
            'myBehavior2' => MyBehavior::className(),

            // 匿名行爲,給出了MyBehavior類的配置數組
            [
                'class' => MyBehavior::className(),
            ],
        ];
    }
}

那麼,yii是如何去綁定上述定義好的數組的呢?答案在第二步,yii\base\Component::ensureBehaviors():

public function ensureBehaviors()
{
    if ($this->_behaviors === null) {
        $this->_behaviors = [];
        // 遍歷 $this->behaviors()數組,並綁定
        foreach ($this->behaviors() as $name => $behavior) {
            $this->attachBehaviorInternal($name, $behavior);
        }
    }
}

ensureBehaviors()方法其實是調用了attachBehaviorInternal():

private function attachBehaviorInternal($name, $behavior)
{
    // 如果不是Behavior的實例,比如說只是一個類名的字符串,或者一個配置數組
    if (!($behavior instanceof Behavior)) {
        // 就把這個對象創建出來
        // createObject()人如其名,就是用來創建對象的,這個知識點在Yii的服務定位器中,其實就是去容器裏取出來對象
        $behavior = Yii::createObject($behavior);
    }

    // 如果是int,說明在配置$behaviors數組的時候,沒有對其人爲的設定一個key,那肯定就是默認的數字索引
    if (is_int($name)) {
        $behavior->attach($this); // 這裏是第四步
        $this->_behaviors[] = $behavior; //將行爲放入$this->_behaviors容器
    } else {

        // 命名行爲:

        // 已經有一個同名的行爲,要先解除,再將新的行爲綁定上去。
        if (isset($this->_behaviors[$name])) {
            $this->_behaviors[$name]->detach();
        }
        $behavior->attach($this); // 這裏是第四步
        $this->_behaviors[$name] = $behavior; //將行爲放入$this->_behaviors容器
    }

    return $behavior;
}

其實經過以上兩步,Yii已經將行爲綁定到Component的子類中,這個時候,已經可以使用行爲的屬性和方法了,那麼第三步是幹什麼的呢,我們先看一下源碼,代碼在yii\base\Behavior::attach():

public function attach($owner)
{
    // 設置該行爲的所有者
    this->owner = $owner;

    // 遍歷行爲類的事件,綁定到行爲類的所有者身上,讓所有者擁有觸發事件的功能
    foreach ($this->events() as $event => $handler) {
        $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
    }
}

這個attach方法主要是爲了把行爲中定義的事件都綁定到行爲的所有者身上,這樣所有者就有了觸發事件的能力

行爲的解綁

行爲的解綁的原理和綁定剛好相反,從行爲數組$_behaviors中,unset掉要解綁的行爲,代碼在yii\base\Component::detachBehavior():

public function detachBehavior($name)
{
    $this->ensureBehaviors();
    if (isset($this->_behaviors[$name])) {
        $behavior = $this->_behaviors[$name];
        unset($this->_behaviors[$name]);
        $behavior->detach(); // 這個方法主要是爲了解綁通過行爲綁定給Component的事件
        return $behavior;
    }

    return null;
}

$behavior->detach()方法在yii\base\Behavior::detach():

public function detach()
{
    if ($this->owner) {
        foreach ($this->events() as $event => $handler) {
            // 解除Component的事件
            $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
        }
        $this->owner = null;
    }
}

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

yii的行爲實現,其實和我們文章一開始自己寫的行爲類實現原理是一樣的,主要用到了__get(),__set(),__call()魔術方法:

屬性的獲取:

public function __get($name)
{
    // 由於在yii的基類Object裏爲屬性定義了getter()方法
    // 所以獲取屬性的時候,先判斷該屬性有沒有對應的getter()方法
    $getter = 'get' . $name;
    if (method_exists($this, $getter)) {
        // read property, e.g. getName()
        return $this->$getter();
    }

    // 如果沒有getter(),那麼該屬性就可能是行爲的屬性
    // 所以遍歷行爲數組,如果哪個行爲有這個屬性,就返回
    $this->ensureBehaviors();
    foreach ($this->_behaviors as $behavior) {
        if ($behavior->canGetProperty($name)) {
            return $behavior->$name;
        }
    }

    ···
}

屬性的設置:

public function __set($name, $value)
{
    // 設置一個屬性的時候,如果有setter()方法,那麼優先調用setter
    $setter = 'set' . $name;
    if (method_exists($this, $setter)) {
        $this->$setter($value);
        return;
    }

    
    $this->ensureBehaviors();
    // 遍歷行爲數組,如果行爲有$name屬性,那麼就設置爲$value
    foreach ($this->_behaviors as $behavior) {
        if ($behavior->canSetProperty($name)) {
            $behavior->$name = $value;
            return;
        }
    }

    ···
}

方法的獲取:

public function __call($name, $params)
{
    $this->ensureBehaviors();
    
    // 遍歷行爲數組
    foreach ($this->_behaviors as $object) {
        // 如果某個行爲有$name這個方法體,那麼就調用,參數爲$params
        if ($object->hasMethod($name)) {
            return call_user_func_array([$object, $name], $params);
        }
    }
    ···
}

引出:行爲和Traits的區別

php的traits是在5.4版本之後才引入的一個新特性,從實現效果上來看,行爲和traits都能把自身的屬性和方法注入到當前類中去,但是還是有一些區別:

  • 行爲從本質上來講,也是PHP的類,一個行爲可以繼承自另一個行爲,從而實現代碼的複用。而traits只是php的一種語法,效果上類似把tratis的代碼導入到一個類中,從而實現代碼的注入,特性是不支持繼承的。
  • 行爲可以支持動態的綁定,解綁,而不必對類進行修改。但是traits必須在類內使用use,才能導入,要解除traits時,則要刪除這個use語句,也就是說要對類進行修改。
  • traits在效率上要比行爲高一點,因爲traits是php內部實現的,行爲是我們後期自己實現的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章