yii2 - Behavior 實例及源碼分析

Behavior 的簡述

        行爲簡單來講是組件的擴展,能夠對組件的屬性方法事件 (yii2組件的三大要點)進行擴展而無需改動組件現有的代碼邏輯。即此行爲所擁有的屬性,方法,事件,都會被綁定它的組件 "獲取" 到。因此 yii2 的行爲在必定程度上也是對 Event 的封裝,你能夠在行爲裏定義須要擴展的屬性,方法,也能夠註冊事件,讓組件能夠作到綁定此行爲,即註冊了某事件的功能。php

    咱們知道,框架在執行過程當中有不少系統級別執行節點,在這些節點 yii2 使用 Event 來進實現行鉤子機制。好比咱們調用一個 Action 有 beforeAction 和 afterAction 的執行節點,調用 Model 的 validate 方法有 beforeValidate 和 afterValidate 的執行節點,執行到相應的節點便會觸發相應的事件,事件去檢查有無註冊的鉤子,有的話即會觸發。而行爲則能夠爲某組件方便的實現此功能。html

yii\base\Model 中的 validate 方法的先後執行節點web

我若是在某行爲中註冊了model的這兩個事件,那麼任何繼承至 yii\base\Model的組件只要綁定了此行爲,都會被註冊這兩個事件。數組

yii\base\Behavior 基類

    yii\base\Behavior::$owner //行爲全部者 確定是某組件對象yii2

    yii\base\Behavior::events() // 行爲擴展的事件app

    yii\base\Behavior::attach($owner) //當組件綁定行爲時 行爲會爲其註冊 events 中定義的事件框架

    yii\base\Behavior::detach($owner) //此方法組要是用於組件綁定行爲名重複時進行事件的解綁yii

yii\base\Behavior 是行爲的基類,全部的行爲都繼承於此,好比咱們經常使用的 yii\filter\AccessControl 和 yii\filter\VerbFilter。函數

行爲實例

app\behaviors\CtrlBehaviorpost

<?php
/**
 * Created by sallency.
 * User: sallency
 * Date: 2016/5/31 0031
 * Time: 16:03
 */

namespace app\behaviors;

use yii\base\Behavior;
use yii\base\Event;
use yii\rest\Controller;

class CtrlBehavior extends Behavior
{
    const PHP_WEB_EOL = "<br>";

    public $param_1;
    public $param_2;

    /**
     * 行爲是爲 Controller 作的擴展 故能夠註冊 Controller 的事件
     * @return array events for component owner
     */
    public function events()
    {
        return [
            Controller::EVENT_BEFORE_ACTION => "handlerBeforeAction",
            Controller::EVENT_AFTER_ACTION => "handlerAfterAction"
        ];
    }

    /**
     * event handler
     * @param \yii\base\Event $event
     */
    public function handlerBeforeAction(Event $event)
    {
        echo __METHOD__ . self::PHP_WEB_EOL;
        echo '由行爲註冊的組件事件,傳遞的$event->sender屬性爲此組件對象' . self::PHP_WEB_EOL;
        echo "組件的控制器和動做:" . $event->sender->uniqueId . '/' . $event->sender->action->id . self::PHP_WEB_EOL;
        echo self::PHP_WEB_EOL;
    }

    /**
     * event handler
     * @param \yii\base\Event $event
     */
    public function handlerAfterAction(Event $event)
    {
        echo self::PHP_WEB_EOL;
        echo __METHOD__ . self::PHP_WEB_EOL;
        echo '由行爲註冊的組件事件,傳遞的$event->sender屬性爲此組件對象' . self::PHP_WEB_EOL;
        echo "組件的控制器和動做:" . $event->sender->uniqueId . '/' . $event->sender->action->id . self::PHP_WEB_EOL;
    }

    /**
     * 擴展方法 經過 __METHOD__ 我麼能夠看出這貨被組件調用時究竟是不是組件的一個方法
     */
    public function extendMethodForCtrl()
    {
        echo "在行爲中定義的方法:";
        echo __METHOD__ . self::PHP_WEB_EOL;
    }
}

app\controllers\BehaviorController

<?php
/**
 * Created by sallency.
 * User: sallency
 * Date: 2016/5/31 0031
 * Time: 16:23
 */

namespace app\controllers;

use app\behaviors\CtrlBehavior;
use yii\web\Controller;

class BehaviorController extends Controller
{
    const PHP_WEB_EOL = "<br>";

    public function init()
    {
        parent::init(); // TODO: Change the autogenerated stub
    }

    //綁定行爲 靜態綁定 還有 attachBehavior/attachBehaviors 動態綁定
    public function behaviors()
    {
        return [
            "ctrlBehavior" => [
                "class" => CtrlBehavior::className(),
                "param_1" => "hello",
                "param_2" => "world"
            ]
        ];
    }

    public function actionIndex()
    {
        echo "組件訪問行爲的屬性和方法:" . __METHOD__ . self::PHP_WEB_EOL;
        //使用 __set __get 方法遍歷訪問行爲隊列 $_behaviors 中是否有行爲對象包含如下屬性
        //有則經過此行爲對象訪問操做屬性
        echo "在行爲中定義的屬性:" . $this->param_1 . "\t" . $this->param_2 . self::PHP_WEB_EOL;
        //使用 __call 方法遍歷訪問行爲隊列 $_behaviors 中是否有行爲對象包含如下方法
        //有則經過此行爲對象訪問方法
        $this->extendMethodForCtrl();
    }
}

訪問 behavior/index

app\behaviors\CtrlBehavior::handlerBeforeAction
由行爲註冊的組件事件,傳遞的$event->sender屬性爲此組件對象
組件的控制器和動做:behavior/index

組件訪問行爲的屬性和方法:app\controllers\BehaviorController::actionIndex
在行爲中定義的屬性:hello	world
在行爲中定義的方法:app\behaviors\CtrlBehavior::extendMethodForCtrl

app\behaviors\CtrlBehavior::handlerAfterAction
由行爲註冊的組件事件,傳遞的$event->sender屬性爲此組件對象
組件的控制器和動做:behavior/index

能夠看到,咱們並無在控制器中定義 $param_1,$param_2屬性,沒有定義 extendMethodForCtrl 方法,沒有註冊 EVENT_BEFORE_ACTION 和 EVENT_AFTER_ACTION 事件

但 actionIndex 執行前/後觸發了 EVENT_BEFORE_ACTION/EVENT_AFTER_ACTION 事件,並且咱們能夠訪問在行爲中定義的 $param_1 $param_2 和 extendMethodForCtrl 方法

Behavior 實現機制

一、組件行爲隊列:$_behaviors

    你必需要明確的是,組件其實並無獲得行爲的屬性和方法,組件行爲隊列:$_behaviors,這裏面存放着你綁定到組件上的行爲實例。組件訪問這些看似本身獲得屬性和方法時,只不過是經過組件的 __set/__get 或者 __call 方法中的對 $_behaviors 中的行爲對象進行遍歷詢問是否有此屬性或方法,有的話則讓此行爲對象反饋給本身而已。

    就好像老闆僱傭了一批有技能的員工,對外看來這個老闆會不少技能,但其實他只不過是把外界的需求分發給他的員工,找到一個能解決此需求的員工去處理這個需求而已,幹活的仍是員工。

二、組件的 behaviors 方法

    咱們經常使用的組件靜態綁定行爲的方法(動態綁定: attachBehavior/attachBehaviors 方法)。此方法返回須要註冊的行爲的yii2標準的參數數組的數組

yii\base\Component::behaviors()
{
    return [
        "myBehavior_1" => [
            "class" => "app\behaviors\MyBehavior1"
            "param_1" => "this is my behavior_1",
            "param_2" => "hello world"
        ],
        "myBehavior_2" => [
            "class" => "app\behaviors\MyBehavior2"
            "param_1" => "this is my behavior_2",
            "param_2" => "hello world"
        ]
    ];
}

三、組件的ensureBehaviors方法

    此方法主要是將 behaviors 方法中註冊的行爲分配給 attachBehaviorInternal 方法進行行爲綁定

yii\base\Component::ensureBehaviors()
{
    if ($this->_behaviors === null) {
            $this->_behaviors = [];
            //獲得組件的行爲註冊數組
            foreach ($this->behaviors() as $name => $behavior) {
                //爲當前組件註冊行爲類
                $this->attachBehaviorInternal($name, $behavior);
            }
        }
}

四、組件的attachBehaviorInternal方法

attachBehaviorInternal 的主要功能是
    1 經過 Yii::createObject($behaviorConfig) 方法獲得一個行爲實例,將其存儲在組件的 $_behaviors 中,這樣結合 __set __get __call 方法便能直接訪問此行爲實例的屬性和方法。
    2 此行爲實例同時會調用自身的 attach 方法(yii\base\Behavior::attach()),檢測本身的 events 中註冊的事件,將其綁定到當前的組件對象中

/**
 * $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 中經過其 events 來爲當前組件註冊事件行爲
        $behavior->attach($this);
        //將這個行爲放入本身的行爲隊列
        $this->_behaviors[$name] = $behavior;
    }
    return $behavior;
}

能夠發現,每一個行爲實例在被放入組件的行爲隊列 $_behaviors 的同時,會去調用本身的事件註冊函數 attach($this)(見 5),爲當前組件註冊 events 方法中聲明的事件

五、行爲的 events attach detach 方法

//須要爲組件綁定的事件
public function events()
{
    return [];
}
//爲組件 $owner 綁定事件
public function attach($owner)
{
    $this->owner = $owner;
    foreach ($this->events() as $event => $handler) {
        $owner->on($event, is_string($handler) ? [$this, $handler] : $handler);
    }
}
//爲組件 $owner 解綁事件
public function detach()
{
    if ($this->owner) {
        foreach ($this->events() as $event => $handler) {
            $this->owner->off($event, is_string($handler) ? [$this, $handler] : $handler);
        }
        $this->owner = null;
    }
}

六、組件的 __set __get __call 方法

這裏我只放 __call 方法的實現,代碼一看便知,當組件訪問一個自身沒有定義的方法時會觸發__call方法,yii2這裏的處理邏輯即是去行爲隊列$_behaviors 中檢索是否存在某個含有此方法的行爲

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

yii\filter\AccessControl / yii\filter\VerbFilter

yii2最經常使用的兩個系統行爲,這兩個行爲做爲過濾器是給 yii\web\Controller 組件用的,綁定方法以下

public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['login', 'logout'], //只對此處聲明的Action生效
                'rules' => [
                    [
                        'actions' => ['logout'],
                        'allow' => true,
                        'roles' => ['@'],//認證用戶
                    ],
                    [
                        'actions' => ['login'],
                        'allow' => true,
                        'roles' => ['?'],//遊客
                        'denyCallback' => function($rule, $action) {//若是不是遊客則無權訪問
                            throw new \Exception("not allowed to access this page" . $action->id);
                        }
                    ],
                ],
            ],
            'verbs' => [
                'class' => VerbFilter::className(),
                'actions' => [
                    'logout' => ['post'],
                    'login'  => ['post'],
                    'index'  => ['get', 'post', 'put', 'delete', 'head', 'option'] 
                ],
            ],
        ];
    }

AccessControl 主要是對訪問控制,VerbFilter 則是對http的動詞進行訪問控制

簡單看一下 VerbFilter 的源碼

class VerbFilter extends Behavior
{    
    public $actions = []; //咱們綁定行爲時傳遞的參數

    public function events() //爲組件註冊的事件 能夠看到是控制器調用Action前節點的事件
    {
        return [Controller::EVENT_BEFORE_ACTION => 'beforeAction'];
    }

    //事件的handler
    public function beforeAction($event)
    {
        //獲得本次請求的action
        $action = $event->action->id;
        if (isset($this->actions[$action])) {
            $verbs = $this->actions[$action];
        } elseif (isset($this->actions['*'])) {
            $verbs = $this->actions['*'];
        } else {
            return $event->isValid;
        }
        //獲得本次請求的 http 動詞
        $verb = Yii::$app->getRequest()->getMethod();
        $allowed = array_map('strtoupper', $verbs);
        if (!in_array($verb, $allowed)) {
            $event->isValid = false;
            Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed));
            throw new MethodNotAllowedHttpException('Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed) . '.');
        }

        return $event->isValid;
    }
}

此行爲會爲 Controller 組件註冊 EVENT_BEFORE_ACTION 的事件,這樣便會在 Action 調用前去校驗本次的 http 動詞是否符合規則。

AccessControl 並無直接繼承 Behavior,而是經過繼承 yii\base\ActionFilter 間接繼承,同時 ActionFilter 對 Behavior 的 attach/detach 進行了重寫,並不去調用 events 中爲組件聲明的事件(其實他就沒聲明...),而是固定的在 attach 綁定 EVENT_BEFORE_ACTION,在 detach 中定 EVENT_AFTER_ACTION

/**
 * @inheritdoc
 */
public function attach($owner)
{
    $this->owner = $owner;
    $owner->on(Controller::EVENT_BEFORE_ACTION, [$this, 'beforeFilter']);
}

/**
 * @inheritdoc
 */
public function detach()
{
    if ($this->owner) {
        $this->owner->off(Controller::EVENT_BEFORE_ACTION, [$this, 'beforeFilter']);
        $this->owner->off(Controller::EVENT_AFTER_ACTION, [$this, 'afterFilter']);
        $this->owner = null;
    }
}

嗯,就這樣啦

梳理下要點

一、Component 將綁定的行爲實例存放在本身的 $_behaviors 隊列中,看似本身 拿到 了行爲的方法或屬性,其實也只是配合本身的 __set __get __call 方法在尋找不到時去遍歷 $_behaviors 中的行爲實例,看誰有此屬性或方法而已,是老闆和員工的關係

二、在綁定行爲的時候,Component 存放的是此行爲的一個實例(綁定時會進行實例類型檢測,故全部的行爲都是 Behavior或子類的實例),綁定時,此行爲實例會調用本身的 attach 方法,將行爲中爲組件定義的事件綁定至此組件,這樣便實現了事件綁定。

相關文章
相關標籤/搜索