深入解析PHP的Yii框架中的event事件機(jī)制
事件
事件可以將自定義代碼“注入”到現(xiàn)有代碼中的特定執(zhí)行點(diǎn)。附加自定義代碼到某個(gè)事件,當(dāng)這個(gè)事件被觸發(fā)時(shí),這些代碼就會(huì)自動(dòng)執(zhí)行。例如,郵件程序?qū)ο蟪晒Πl(fā)出消息時(shí)可觸發(fā) messageSent 事件。如想追蹤成功發(fā)送的消息,可以附加相應(yīng)追蹤代碼到messageSent 事件。
Yii 引入了名為 yii\base\Component 的基類以支持事件。如果一個(gè)類需要觸發(fā)事件就應(yīng)該繼承 yii\base\Component 或其子類。
Yii的event機(jī)制
YII的事件機(jī)制,是其比較獨(dú)特之處,合理使用好事件機(jī)制,會(huì)使各個(gè)組件之間的耦合更為松散,利于團(tuán)體協(xié)作開發(fā)。
何時(shí)需要使用事件,如何給事件綁定事件處理函數(shù),以及如何觸發(fā)事件,與其它語(yǔ)言是有較大的差別的。例如Javascript中,可以使用
$(‘#id').on("click",function() {});
方式給DOM元素綁定處理函數(shù),當(dāng)DOM元素上發(fā)生指定的事件(如click)時(shí),將自動(dòng)執(zhí)行設(shè)定的函數(shù)。
但是PHP是服務(wù)器端的腳本語(yǔ)言,就不存在自動(dòng)觸發(fā)事件之說(shuō),所以和Javascript對(duì)比,YII中的事件是需要手動(dòng)觸發(fā)的。一般來(lái)說(shuō),要實(shí)現(xiàn)YII組件的事件機(jī)制,需要以下幾步:
定義事件名稱,其實(shí)就是級(jí)組件定義一個(gè)on開頭的方法,其中的代碼是固定的,如:
public function onBeginRequest($event){ $this->raiseEvent('onBeginRequest',$event); }
即函數(shù)名與事件名是一致的。此步的作用就是將綁定在此事件上的處理函數(shù)逐個(gè)執(zhí)行。寫這一系列的播客,算是一個(gè)整理,所以我寫細(xì)一點(diǎn),現(xiàn)在把raiseEvent方法的代碼貼出來(lái)。
/** * Raises an event. * This method represents the happening of an event. It invokes * all attached handlers for the event. * @param string $name the event name * @param CEvent $event the event parameter * @throws CException if the event is undefined or an event handler is invalid. */ public function raiseEvent($name,$event){ $name=strtolower($name); //_e這個(gè)數(shù)組用來(lái)存所有事件信息 if(isset($this->_e[$name])) { foreach($this->_e[$name] as $handler) { if(is_string($handler)) call_user_func($handler,$event); elseif(is_callable($handler,true)){ if(is_array($handler)){ // an array: 0 - object, 1 - method name list($object,$method)=$handler; if(is_string($object)) // static method call call_user_func($handler,$event); elseif(method_exists($object,$method)) $object->$method($event); else throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".', array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>$handler[1]))); } else // PHP 5.3: anonymous function call_user_func($handler,$event); } else throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".', array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>gettype($handler)))); // stop further handling if param.handled is set true if(($event instanceof CEvent) && $event->handled) return; } } elseif(YII_DEBUG && !$this->hasEvent($name)) throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.', array('{class}'=>get_class($this), '{event}'=>$name))); }
事件處理器(Event Handlers)
事件處理器是一個(gè)PHP 回調(diào)函數(shù),當(dāng)它所附加到的事件被觸發(fā)時(shí)它就會(huì)執(zhí)行??梢允褂靡韵禄卣{(diào)函數(shù)之一:
- 字符串形式指定的 PHP 全局函數(shù),如 'trim' ;
- 對(duì)象名和方法名數(shù)組形式指定的對(duì)象方法,如 [$object, $method] ;
- 類名和方法名數(shù)組形式指定的靜態(tài)類方法,如 [$class, $method] ;
- 匿名函數(shù),如 function ($event) { ... } 。
事件處理器的格式是:
function ($event) { // $event 是 yii\base\Event 或其子類的對(duì)象 }
通過(guò) $event 參數(shù),事件處理器就獲得了以下有關(guān)事件的信息:
- yii\base\Event::name:事件名
- yii\base\Event::sender:調(diào)用 trigger() 方法的對(duì)象
- yii\base\Event::data:附加事件處理器時(shí)傳入的數(shù)據(jù),默認(rèn)為空,后文詳述
附加事件處理器
調(diào)用 yii\base\Component::on() 方法來(lái)附加處理器到事件上。如:
$foo = new Foo; // 處理器是全局函數(shù) $foo->on(Foo::EVENT_HELLO, 'function_name'); // 處理器是對(duì)象方法 $foo->on(Foo::EVENT_HELLO, [$object, 'methodName']); // 處理器是靜態(tài)類方法 $foo->on(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']); // 處理器是匿名函數(shù) $foo->on(Foo::EVENT_HELLO, function ($event) { //事件處理邏輯 }); 附加事件處理器時(shí)可以提供額外數(shù)據(jù)作為 yii\base\Component::on() 方法的第三個(gè)參數(shù)。數(shù)據(jù)在事件被觸發(fā)和處理器被調(diào)用時(shí)能被處理器使用。如: // 當(dāng)事件被觸發(fā)時(shí)以下代碼顯示 "abc" // 因?yàn)?$event->data 包括被傳遞到 "on" 方法的數(shù)據(jù) $foo->on(Foo::EVENT_HELLO, function ($event) { echo $event->data; }, 'abc');
事件處理器順序
可以附加一個(gè)或多個(gè)處理器到一個(gè)事件。當(dāng)事件被觸發(fā),已附加的處理器將按附加次序依次調(diào)用。如果某個(gè)處理器需要停止其后的處理器調(diào)用,可以設(shè)置 $event 參數(shù)的 [yii\base\Event::handled]] 屬性為真,如下:
$foo->on(Foo::EVENT_HELLO, function ($event) { $event->handled = true; });
默認(rèn)新附加的事件處理器排在已存在處理器隊(duì)列的最后。因此,這個(gè)處理器將在事件被觸發(fā)時(shí)最后一個(gè)調(diào)用。在處理器隊(duì)列最前面插入新處理器將使該處理器最先調(diào)用,可以傳遞第四個(gè)參數(shù) $append 為假并調(diào)用 yii\base\Component::on() 方法實(shí)現(xiàn):
$foo->on(Foo::EVENT_HELLO, function ($event) { // 這個(gè)處理器將被插入到處理器隊(duì)列的第一位... }, $data, false);
觸發(fā)事件
事件通過(guò)調(diào)用 yii\base\Component::trigger() 方法觸發(fā),此方法須傳遞事件名,還可以傳遞一個(gè)事件對(duì)象,用來(lái)傳遞參數(shù)到事件處理器。如:
namespace app\components; use yii\base\Component; use yii\base\Event; class Foo extends Component { const EVENT_HELLO = 'hello'; public function bar() { $this->trigger(self::EVENT_HELLO); } }
以上代碼當(dāng)調(diào)用 bar() ,它將觸發(fā)名為 hello 的事件。
提示:推薦使用類常量來(lái)表示事件名。上例中,常量 EVENT_HELLO 用來(lái)表示 hello 。這有兩個(gè)好處。第一,它可以防止拼寫錯(cuò)誤并支持 IDE 的自動(dòng)完成。第二,只要簡(jiǎn)單檢查常量聲明就能了解一個(gè)類支持哪些事件。
有時(shí)想要在觸發(fā)事件時(shí)同時(shí)傳遞一些額外信息到事件處理器。例如,郵件程序要傳遞消息信息到 messageSent 事件的處理器以便處理器了解哪些消息被發(fā)送了。為此,可以提供一個(gè)事件對(duì)象作為 yii\base\Component::trigger() 方法的第二個(gè)參數(shù)。這個(gè)事件對(duì)象必須是 yii\base\Event 類或其子類的實(shí)例。如:
namespace app\components; use yii\base\Component; use yii\base\Event; class MessageEvent extends Event { public $message; } class Mailer extends Component { const EVENT_MESSAGE_SENT = 'messageSent'; public function send($message) { // ...發(fā)送 $message 的邏輯... $event = new MessageEvent; $event->message = $message; $this->trigger(self::EVENT_MESSAGE_SENT, $event); } }
當(dāng) yii\base\Component::trigger() 方法被調(diào)用時(shí),它將調(diào)用所有附加到命名事件(trigger 方法第一個(gè)參數(shù))的事件處理器。
移除事件處理器
從事件移除處理器,調(diào)用 yii\base\Component::off() 方法。如:
// 處理器是全局函數(shù) $foo->off(Foo::EVENT_HELLO, 'function_name'); // 處理器是對(duì)象方法 $foo->off(Foo::EVENT_HELLO, [$object, 'methodName']); // 處理器是靜態(tài)類方法 $foo->off(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']); // 處理器是匿名函數(shù) $foo->off(Foo::EVENT_HELLO, $anonymousFunction);
注意當(dāng)匿名函數(shù)附加到事件后一般不要嘗試移除匿名函數(shù),除非你在某處存儲(chǔ)了它。以上示例中,假設(shè)匿名函數(shù)存儲(chǔ)為變量$anonymousFunction 。
移除事件的全部處理器,簡(jiǎn)單調(diào)用 yii\base\Component::off() 即可,不需要第二個(gè)參數(shù):
$foo->off(Foo::EVENT_HELLO);
類級(jí)別的事件處理器
以上部分,我們敘述了在實(shí)例級(jí)別如何附加處理器到事件。有時(shí)想要一個(gè)類的所有實(shí)例而不是一個(gè)指定的實(shí)例都響應(yīng)一個(gè)被觸發(fā)的事件,并不是一個(gè)個(gè)附加事件處理器到每個(gè)實(shí)例,而是通過(guò)調(diào)用靜態(tài)方法 yii\base\Event::on() 在類級(jí)別附加處理器。
例如,活動(dòng)記錄對(duì)象要在每次往數(shù)據(jù)庫(kù)新增一條新記錄時(shí)觸發(fā)一個(gè) yii\db\BaseActiveRecord::EVENT_AFTER_INSERT 事件。要追蹤每個(gè)活動(dòng)記錄對(duì)象的新增記錄完成情況,應(yīng)如下寫代碼:
use Yii; use yii\base\Event; use yii\db\ActiveRecord; Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) { Yii::trace(get_class($event->sender) . ' is inserted'); });
每當(dāng) yii\db\BaseActiveRecord 或其子類的實(shí)例觸發(fā) yii\db\BaseActiveRecord::EVENT_AFTER_INSERT 事件時(shí),這個(gè)事件處理器都會(huì)執(zhí)行。在這個(gè)處理器中,可以通過(guò) $event->sender 獲取觸發(fā)事件的對(duì)象。
當(dāng)對(duì)象觸發(fā)事件時(shí),它首先調(diào)用實(shí)例級(jí)別的處理器,然后才會(huì)調(diào)用類級(jí)別處理器。
可調(diào)用靜態(tài)方法yii\base\Event::trigger()來(lái)觸發(fā)一個(gè)類級(jí)別事件。類級(jí)別事件不與特定對(duì)象相關(guān)聯(lián)。因此,它只會(huì)引起類級(jí)別事件處理器的調(diào)用。如:
use yii\base\Event; Event::on(Foo::className(), Foo::EVENT_HELLO, function ($event) { echo $event->sender; // 顯示 "app\models\Foo" }); Event::trigger(Foo::className(), Foo::EVENT_HELLO);
注意這種情況下 $event->sender 指向觸發(fā)事件的類名而不是對(duì)象實(shí)例。
注意:因?yàn)轭惣?jí)別的處理器響應(yīng)類和其子類的所有實(shí)例觸發(fā)的事件,必須謹(jǐn)慎使用,尤其是底層的基類,如 yii\base\Object。
移除類級(jí)別的事件處理器只需調(diào)用yii\base\Event::off(),如:
// 移除 $handler Event::off(Foo::className(), Foo::EVENT_HELLO, $handler); // 移除 Foo::EVENT_HELLO 事件的全部處理器 Event::off(Foo::className(), Foo::EVENT_HELLO);
全局事件
所謂全局事件實(shí)際上是一個(gè)基于以上敘述的事件機(jī)制的戲法。它需要一個(gè)全局可訪問(wèn)的單例,如應(yīng)用實(shí)例。
事件觸發(fā)者不調(diào)用其自身的 trigger() 方法,而是調(diào)用單例的 trigger() 方法來(lái)觸發(fā)全局事件。類似地,事件處理器被附加到單例的事件。如:
use Yii; use yii\base\Event; use app\components\Foo; Yii::$app->on('bar', function ($event) { echo get_class($event->sender); // 顯示 "app\components\Foo" }); Yii::$app->trigger('bar', new Event(['sender' => new Foo]));
全局事件的一個(gè)好處是當(dāng)附加處理器到一個(gè)對(duì)象要觸發(fā)的事件時(shí),不需要產(chǎn)生該對(duì)象。相反,處理器附加和事件觸發(fā)都通過(guò)單例(如應(yīng)用實(shí)例)完成。
然而,因?yàn)槿质录拿臻g由各方共享,應(yīng)合理命名全局事件,如引入一些命名空間(例:"frontend.mail.sent", "backend.mail.sent")。
給組件對(duì)象綁定事件處理函數(shù)
$component->attachEventHandler($name, $handler); $component->onBeginRequest = $handler ;
yii支持一個(gè)事件綁定多個(gè)回調(diào)函數(shù),上述的兩個(gè)方法都會(huì)在已有的事件上增加新的回調(diào)函數(shù),而不會(huì)覆蓋已有回調(diào)函數(shù)。
$handler即是一個(gè)PHP回調(diào)函數(shù),關(guān)于回調(diào)函數(shù)的形式,本文的最后會(huì)附帶說(shuō)明。如CLogRouter組件的init事件中,有以下代碼:
Yii::app()->attachEventHandler('onEndRequest',array($this,'processLogs'));
這就是給CApplication對(duì)象的onEndRequest綁定了CLogRouter::processLogs()回調(diào)函數(shù)。而CApplication組件確實(shí)存在名為onEndRequest的方法(即onEndRequest事件),它之中的代碼就是激活了相應(yīng)的回調(diào)函數(shù),即CLogRouter::processLogs()方法。所以從這里可以得出,日志的記錄其實(shí)是發(fā)生在CApplication組件的正常退出時(shí)。
在需要觸發(fā)事件的時(shí)候,直接激活組件的事件,即調(diào)用事件即可,如:比如CApplication組件的run方法中:
if($this->hasEventHandler('onBeginRequest')) $this->onBeginRequest(new CEvent($this));
這樣即觸發(fā)了事件處理函數(shù)。如果沒有第一行的判斷,那么在調(diào)試模式下(YII_DEBUG常量被定義為true),會(huì)拋出異常,而在非調(diào)試模式下(YII_DEBUG常量定義為false或沒有定義YII_DEBUG常量),則不會(huì)產(chǎn)生任何異常。
回調(diào)函數(shù)的形式:
普通全局函數(shù)(內(nèi)置的或用戶自定義的)
call_user_func(‘print', $str);
類的靜態(tài)方法,使用數(shù)組形式傳遞
call_user_func(array(‘className', ‘print'), $str );
對(duì)象方法,使用數(shù)組形式傳遞
$obj = new className(); call_user_func(array($obj, ‘print'), $str );
匿名方法,類似javascript的匿名函數(shù)
call_user_func(function($i){echo $i++;},4);
或使用以下形式:
$s = function($i) { echo $i++; }; call_user_func($s,4);
總結(jié)
關(guān)于Yii的事件機(jī)制其實(shí)就是提供了一種用于解耦的方式,在需要調(diào)用event的地方之前,只要你提供了事件的實(shí)現(xiàn)并注冊(cè)在之后的地方需要的時(shí)候即可調(diào)用。
相關(guān)文章
Discuz!下Memcache緩存實(shí)現(xiàn)方法
在PHP+MySQL架構(gòu)的站點(diǎn)中,本文重點(diǎn)從MySQL的角度去分析如何使Discuz!論壇(或者類似的PHP+MySQL架構(gòu)的程序)應(yīng)對(duì)大訪問(wèn)量。同時(shí)給出一些使用Memcache去減輕MySQL壓力的建議2010-05-05php將從數(shù)據(jù)庫(kù)中獲得的數(shù)據(jù)轉(zhuǎn)換成json格式并輸出的方法
今天小編就為大家分享一篇php將從數(shù)據(jù)庫(kù)中獲得的數(shù)據(jù)轉(zhuǎn)換成json格式并輸出的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08php cookie的操作實(shí)現(xiàn)代碼(登錄)
cookie 常用于識(shí)別用戶。cookie 是服務(wù)器留在用戶計(jì)算機(jī)中的小文件。每當(dāng)相同的計(jì)算機(jī)通過(guò)瀏覽器請(qǐng)求頁(yè)面時(shí),它同時(shí)會(huì)發(fā)送 cookie。通過(guò) PHP,您能夠創(chuàng)建并取回 cookie 的值。2010-12-12PHP簡(jiǎn)單實(shí)現(xiàn)二維數(shù)組的矩陣轉(zhuǎn)置操作示例
這篇文章主要介紹了PHP簡(jiǎn)單實(shí)現(xiàn)二維數(shù)組的矩陣轉(zhuǎn)置操作,涉及php針對(duì)二維數(shù)組的遍歷與運(yùn)算操作技巧,需要的朋友可以參考下2017-11-11PHP連接數(shù)據(jù)庫(kù)實(shí)現(xiàn)簡(jiǎn)單的登錄頁(yè)面功能
這篇文章主要介紹了PHP連接數(shù)據(jù)庫(kù)實(shí)現(xiàn)簡(jiǎn)單的登錄頁(yè)面功能,文中的示例代碼講解詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,感興趣的可以了解一下2023-09-09php基于dom實(shí)現(xiàn)的圖書xml格式數(shù)據(jù)示例
這篇文章主要介紹了php基于dom實(shí)現(xiàn)的圖書xml格式數(shù)據(jù),結(jié)合實(shí)例形式分析了php數(shù)組轉(zhuǎn)換xml格式數(shù)據(jù)的相關(guān)操作技巧,需要的朋友可以參考下2017-02-02PHP設(shè)計(jì)模式之工廠模式(Factory Pattern)的講解
今天小編就為大家分享一篇關(guān)于PHP設(shè)計(jì)模式之工廠模式(Factory Pattern)的講解,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03