對Yii框架中事件【event】的理解

什麼是事件

怎麼去理解事件呢,通俗一點來講,好比你去淘寶網買東西,下了一個訂單,下訂單之後會觸發和訂單相關的一些事情,比如系統會將訂單信息推入隊列、通知賣家備貨,這就是一系列的事情集合。

引用上述下訂單的例子,一般按照傳統的開發流程,實現上述功能,應該這樣寫(僞代碼):

class OrderController
{

    public function actionCreateOrder()
    {
        // 1.創建訂單
        $orderModel = new Order();
        $orderModel->create($order_id,$buyer_id);
        OrderModel::create();

        // 2.推入隊列
        $orderQueue = new OrderQueue();
        $orderQueue->push($queue_name,$order_id);

        // 3.短信通知商家
        $msg = new Msg();
        $msg->send($seller_id,$content);
    }
}

這種寫法在功能實現上是沒有問題,但是有一個弊端就是,代碼耦合在一起,假如現在產品規劃,要在下單之後,給買家發送一封郵件,那是不是就要在這段代碼裏新添加代碼邏輯:

public function actionCreateOrder()
{
    // 1.創建訂單
    $orderModel = new Order();
    $orderModel->create($order_id,$buyer_id);
    OrderModel::create();

    // 2.推入隊列
    $orderQueue = new OrderQueue();
    $orderQueue->push($queue_name,$order_id);

    // 3.短信通知商家
    $msg = new Msg();
    $msg->send($seller_id,$content);

    // 4.郵件通知買家
    $mail = new Mail();
    $mail->send($buyer_id,$content);
}

如果後期又要對下訂單進行功能擴充和修改,那麼就要頻繁的去修改這段代碼,時間長了,這段代碼會越來越長,越來越臃腫,越來越難維護。分析其中原因,就是從功能上說,下單和下單之後的一系列事件,諸如發短信,發郵件,其實是鬆耦合的關係,但是在actionCreateOrder()方里,把這幾個功能捆綁的死死的,絲毫分不開,這就是一種糟糕的設計。所以,這裏我們要引入事件,事件在這裏就是爲了解耦,讓代碼邏輯變的鬆散,和易於維護。

動手實現一個我們自己的事件類

先來簡單實現一個我們自己的事件類:

class Event
{
    protected $_events = [];

    public function on($name, $handler, $append = true)
    {
        //這裏用$append來干涉同一個事件下的handler執行順序
        if ($append || empty($this->_events[$name])) {
            $this->_events[$name][] = [$handler];
        } else {
            //如果$append爲false,則會優先執行該事件下的這個handler
            //即將這個handler放入該事件數組的第一位
            array_unshift($this->_events[$name], [$handler]);
        }
    }

    public function trigger($name)
    {
        if (!empty($this->_events[$name])) {
            foreach ($this->_events[$name] as $handlerBox) {
                call_user_func($handlerBox[0]);
            }
        }
    }
}


class TestEvent extends Event
{

    public function run()
    {
        $this->on("EVENT_1", [$this, "event1"]);//一個對象的方法
        $this->on("EVENT_1", [$this, "event2"], false);//$append爲false,該handler將第一個被執行
        $this->on("EVENT_1", ['TestEvent', "event3"]);//類成員靜態方法
        $this->on("EVENT_1", "event4");//全局函數
        $this->on("EVENT_1", function () {
            echo "我是回調函數event5,我被觸發了!" . PHP_EOL;
        });//匿名函數
        $this->trigger("EVENT_1");
    }

    public function event1()
    {
        echo "我是event1,我被觸發了!" . PHP_EOL;
    }

    public function event2()
    {
        echo "我是event2,我被提前觸發了!" . PHP_EOL;
    }

    public static function event3()
    {
        echo "我是靜態方法event3,我被觸發了!" . PHP_EOL;
    }

}

function event4()
{
    echo "我是外部全局方法event4,我被觸發了!" . PHP_EOL;
}

$test = new TestEvent();
$test->run();
//執行run()方法會輸出:
//我是event2,我被提前觸發了!
//我是event1,我被觸發了!
//我是靜態方法event3,我被觸發了!
//我是外部全局方法event4,我被觸發了!
//我是回調函數event5,我被觸發了!

這段代碼還是很好理解的,不光是這段代碼,包括稍後我要講到的Yii的事件,其代碼都是很好理解的,重要的是思想,是如何規劃出好的程序邏輯。

按照上述事件的思維來重新開發這個下訂單的程序(仍然是僞代碼):

Class OrderController extends Event
{
    public function actionCreateOrder()
    {
        // 創建訂單
        $orderModel = new Order();
        $orderModel->create($order_id,$buyer_id);
        
        //綁定創建訂單之後的一系列事件
        $this->on("AFTER_CREATE_ORDER",['OrderQueue','push']);//綁定推入隊列事件
        $this->on("AFTER_CREATE_ORDER",['Msg','send']);       //綁定發送短信事件
        $this->on("AFTER_CREATE_ORDER",['Mail','send']);      //綁定發送郵件事件

        //觸發一系列和訂單有關的事件
        $this->trigger("AFTER_CREATE_ORDER");
    }
}

事件其實是某個業務流程上的某個特定的點,上述例子裏的流程就是用戶下單後,AFTER_CREATE_ORDER是下單完成後的一個節點,程序走到這裏,就觸發事件。該事件上有處理器就執行,沒有就繼續往後執行。
我們綁定了一個推入隊列的處理器,綁定了一個發送短信的處理器,綁定了一個發送郵件的處理器,還可以繼續綁定其他的處理器。這種思想,已經解耦了多種功能。在實現表現上,將“下單”看作是流程本身,而推入隊列,發送短信、郵件,看作是“附加”的,在需要的時候綁定下,不需要就不綁定。

事件處理器OrderQueue::push(),Msg::send(),Mail::send()分佈在屬於自己的獨立的類中,並沒有出現在上面actionCreateOrder()方法裏,而且可增可減,完全視需要而定,因此真正滿足了“開閉原則”——面對擴展開放,面對修改關閉。

引出Yii中的事件

Yii中與事件相關的類

在Yii中,事件是在yii\base\Component中引入的,同時,Yii中還有一個與事件緊密相關的yii\base\Event,它裝在了與事件相關的有關數據,並提供一些功能函數作爲輔助:

class Event extends Object
{
    public $name; //事件的名字
    public $sender; //事件發佈者,通常是調用了trigger()的對象或類
    public $handled = false; //是否終止事件的後續處理
    public $data; //事件相關數據

    private static $_events = [];

    public static function on($class, $name, $handler, $data = null, $append = true)
    {
        // 用於綁定事件 handler
    }

    public static function off($class, $name, $handler = null)
    {
        // 用於取消事件 handler 綁定
    }
    
    public static function trigger($class, $name, $event = null)
     {
        // 用於觸發事件
    }
 }

Yii的事件handler說明

所謂事件handler就是事件綁定好的一段可執行的程序而已,最終是通過php的call_user_func()來調用的,所以可以是以下的任意一種形式:

  • 一個 PHP全局函數的函數名
  • 一個對象的方法
  • 一個類的靜態方法
  • 一個匿名函數,如 function($event) {}

事件的綁定

所謂事件的綁定,就是把事件名字和事件handler綁定在一起,當觸發該事件名的時候,事件名對應的handler程序被執行。

事件的綁定表現形式

關於事件綁定的代碼,我們在文章開始部分,實現我們自己的event事件類的時候,已經寫過類似代碼了,只是yii的事件handler裏需要傳入Event $event對象參數:

$object = new TestEvent();
$object->on("EVENT_1", [$object, "event1"]);//一個對象的方法
$object->on("EVENT_1", [$object, "event2"], null, false);//$append爲false,該handler將第一個被執行
$object->on("EVENT_1", ['TestEvent', "event3"]);//類成員靜態方法
$object->on("EVENT_1", "event4");//全局函數
$object->on("EVENT_1", function ($event) {
    echo "我是回調函數event5,我被觸發了!" . PHP_EOL;
});//匿名函數

事件綁定的參數分析

上面的例子只是簡單的綁定了事件與事件handler,如果有額外的數據傳遞給handler,可以使用yii\base\Component::on()的第三個參數datatrigger()Eventdata,這個參數將會在trigger()觸發的時候,寫進Event類的屬性data裏(不太理解的同學,可以再去具體看源代碼):

$person->on("EVENT_1", 'event5', 'Hello World');
function event5($event) {
    echo $event->data; //輸出:Hello World
}

yii\base\Component::on()的第四個參數$append,是用來控制所綁定的事件handler的執行優先級:

  • 參數appendtruehandlerappend爲true 表示:所要綁定的事件handler追加在_event[$name]數組的後面,這是默認的綁定方式,依次執行。
  • 參數appendfalsehandlerappend爲false表示:所要綁定的事件handler放在_event[$name]數組的第一個索引位置,也就是說在觸發這個事件的時候,這個handler會被第一個執行。
  • 如果所有綁定的事件還沒有已經綁定好的handler,那麼無論appendtruefalsehandlerappend是否是true或者false,該handler都是第一個,因爲這個時候,在_event[$name]裏只有一個handler。

事件的解綁

事件可以綁定,自然也可以解綁,yii\base\Component::off()方法就是用來解綁事件的,它的原理是很簡單的,找到對應的事件和handler,然後unset掉event[_event[name]中的數據即可,如下所示:

public function off($name, $handler = null)
{
    $this->ensureBehaviors();
    if (empty($this->_events[$name])) {
        return false;
    }

    //$handler===null表明解除所有的handler
    if ($handler === null) {
        unset($this->_events[$name]);
        return true;
    } else {
        $removed = false;

        //遍歷所有的$handler
        foreach ($this->_events[$name] as $i => $event) {
            if ($event[0] === $handler) {
                unset($this->_events[$name][$i]);
                $removed = true;
            }
        }
        if ($removed) {
            //重新整理$this->_events[$name]的數組索引
            $this->_events[$name] = array_values($this->_events[$name]);
        }
        return $removed;
    }
}

關於事件解綁,需要注意以下幾點:

  • handlernullhandler爲null時,表示解除_events[$name]事件下所有的handler。
  • 在解除handlerhandler時,如果handler是匿名函數,需要在綁定事件的時候,提前將這個匿名函數賦值給一個變量,將事件和匿名函數對應的變量綁定,解除的時候,也傳入這個變量才能解除。

事件的觸發

事件的觸發,需要調用yii\base\Component::trigger()方法,代碼如下:

public function trigger($name, Event $event = null)
{
    $this->ensureBehaviors();
    if (!empty($this->_events[$name])) {
        if ($event === null) {
            $event = new Event;
        }
        if ($event->sender === null) {
            $event->sender = $this;
        }
        $event->handled = false;
        $event->name = $name;

        //遍歷$_events[$name]數組,依次執行該事件下綁定的handler
        foreach ($this->_events[$name] as $handler) {
            $event->data = $handler[1];

            //調用handler,參數是Event $event
            call_user_func($handler[0], $event);

            //如果在某個handler中,將$evnet->handled設爲 true,就停止調用該事件下後續的handler
            if ($event->handled) {
                return;
            }
        }
    }
    Event::trigger($this, $name, $event); //觸發類一級的事件
}

關於triggere()方法的實現是比較簡單的,核心就是通過php的內置函數call_user_func,以event調handlerhandlerevent對象爲參數,調用了handler處理器,這裏想說的一點是,如果在某個handler的執行中,將evnet->handled設爲true,就停止調用該事件下後續的handler,這一點要特別注意。

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