0
点赞
收藏
分享

微信扫一扫

Yii2之事件

众所周知,yii的三大特性是:属性、事件、行为,上一篇博文简单讲解了yii中的属性,本文接着讲讲yii的事件。

  事件是代码解耦的一种方式,设计业务流程的一种模式。在yii2.0中,通过Yii\base\Component继承yii\base\Object,重载__get()、__set()方法,引入了事件和行为,使得开发变得十分方便。然而,在方便开发的同时也牺牲了一定的效率,所以若不需要使用事件和行为,可不必继承Component而选择继承Object,Object的效率更接近原生的PHP类。

  首先说说yii事件的使用,由于Yii\base\Component类已经实现了事件,所以只要某个类继承了Component类,它的对象就可以调用on()方法来绑定事件,然后在需要的地方调用trigger()方法触发指定事件。下面举一个简单的例子来说明。

  例如,在博客系统中,博文数据表中有created_at(发表时间)和updated_at(更新时间)两个字段,现在我想通过事件来在更新数据表之前给这两个字段赋值。我们知道,yii的AR模型在数据表插入或更新数据之前会调用beforeSave()方法,现在就在博文AR模型中重写这个方法去绑定并触发事件给这两个字段赋值,代码如下:

public function beforeSave($insert) {
if(parent::beforeSave($insert)) {
$this->on('haha', [$this, 'setTime'], $insert);
$this->trigger('haha');
return true;
}
return false;
}


(这个例子中,事件的绑定和触发在同一个地方进行,这里使用事件意义不大,只是为了举个简单例子来说明yii事件的使用而已哈)

  以上代码将[$this, ‘setTime’]这个处理器绑定在名为’haha’的事件上,这个处理器其实就是博文AR模型的setTime()方法,第三个参数表示是否插入数据,将会在事件触发的时候传递给setTime()方法。setTime()方法代码如下:

public function setTime($event) {
if($event->data) {
$this->uid = User::getUid();
$this->created_at = time();
}
$this->updated_at = time();
}


其中$event->data就是on()函数的第三个参数了。setTime方法的参数$event在yii中是yii\base\Event类的对象,若在事件触发之时需要传递一些参数给处理器函数,可以写一个子类继承yii\base\Event类,设置一些成员变量,然后创建该类的一个对象,把需要传递的参数赋值给这个对象的成员变量,接着把这个对象赋值给事件触发方法trigger()的第二个参数,在处理器函数中就可以接收到这些参数值了。

  没错,在yii中使用事件就是这么简单!可以看到,其实事件的本质就是把一段代码抽出来单独写成一个方法,然后把它绑定在某个事件上,最后在需要调用这个方法的地方触发这个事件就ok了。个人理解,设计事件主要是为了代码解耦与重用吧。

  Yii的事件到底是怎么实现的呢?说到底其实就是一个数组和三个方法就能搞定的事情,且听我细细道来。首先,使用一个数组(下文称为事件队列)来保存所有的事件,数组的键为各个事件名称,数组的元素值则是各个事件触发时需要调用的函数以及需要传递给函数的参数(下文称为事件处理器),可以有一个或多个,所以也是使用数组来存储,下文称为处理器队列,一个事件对应一个处理器队列。为一个事件绑定一个事件处理器则是在事件队列中找到这个事件对应的处理器队列并添加一个处理器,把一个处理器从某个事件解绑则是在事件队列中找到这个事件指定的处理器并从它的处理器队列中删除,触发一个事件则是在事件队列中找到这个事件然后按顺序调用它对应的处理器队列中的所有处理器。

  为了方便理解,下面上图说明(事件队列数据结构图):


Yii2之事件_事件处理


事件处理器(Event Handlers)

事件处理器是一个​​PHP 回调函数​​,当它所附加到的事件被触发时它就会执行。可以使用以下回调函数之一:

  • 字符串形式指定的 PHP 全局函数,如​​'trim'​​ ;
  • 对象名和方法名数组形式指定的对象方法,如​​[$object, $method]​​ ;
  • 类名和方法名数组形式指定的静态类方法,如​​[$class, $method]​​ ;
  • 匿名函数,如​​function ($event) { ... }​​ 。

事件处理器的格式是:

function ($event) {
// $event 是 yii\base\Event 或其子类的对象
}


通过 $event 参数,事件处理器就获得了以下有关事件的信息:

[[yii\base\Event::name|event name]]:事件名
[[yii\base\Event::sender|event sender]]:调用 trigger() 方法的对象
[[yii\base\Event::data|custom data]]:附加事件处理器时传入的数据,默认为空,后文详述
附加事件处理器
调用 [[yii\base\Component::on()]] 方法来附加处理器到事件上。如:


$foo = new Foo;

// 处理器是全局函数
$foo->on(Foo::EVENT_HELLO, 'function_name');

// 处理器是对象方法
$foo->on(Foo::EVENT_HELLO, [$object, 'methodName']);

// 处理器是静态类方法
$foo->on(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']);

// 处理器是匿名函数
$foo->on(Foo::EVENT_HELLO, function ($event) {
//事件处理逻辑
});


附加事件处理器时可以提供额外数据作为 [[yii\base\Component::on()]] 方法的第三个参数。数据在事件被触发和处理器被调用时能被处理器使用。如:

// 当事件被触发时以下代码显示 "abc"
// 因为 $event->data 包括被传递到 "on" 方法的数据
$foo->on(Foo::EVENT_HELLO, function ($event) {
echo $event->data;
}, 'abc');


事件处理器顺序

可以附加一个或多个处理器到一个事件。当事件被触发,已附加的处理器将按附加次序依次调用。如果某个处理器需要停止其后的处理器调用,可以设置 ​​$event​​ 参数的 [yii\base\Event::handled]] 属性为真,如下:

$foo->on(Foo::EVENT_HELLO, function ($event) {
$event->handled = true;
});


默认新附加的事件处理器排在已存在处理器队列的最后。因此,这个处理器将在事件被触发时最后一个调用。在处理器队列最前面插入新处理器将使该处理器最先调用,可以传递第四个参数 ​​$append​​ 为假并调用 [[yii\base\Component::on()]] 方法实现:

$foo->on(Foo::EVENT_HELLO, function ($event) {
// 这个处理器将被插入到处理器队列的第一位...
}, $data, false);


触发事件

事件通过调用 [[yii\base\Component::trigger()]] 方法触发,此方法须传递事件名,还可以传递一个事件对象,用来传递参数到事件处理器。如:

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


以上代码当调用 ​​bar()​​​ ,它将触发名为 ​​hello​​ 的事件。

推荐使用类常量来表示事件名。上例中,常量 EVENT_HELLO 用来表示 hello 。这有两个好处。第一,它可以防止拼写错误并支持 IDE 的自动完成。第二,只要简单检查常量声明就能了解一个类支持哪些事件。


有时想要在触发事件时同时传递一些额外信息到事件处理器。例如,邮件程序要传递消息信息到 ​​messageSent​​ 事件的处理器以便处理器了解哪些消息被发送了。为此,可以提供一个事件对象作为 [[yii\base\Component::trigger()]] 方法的第二个参数。这个事件对象必须是 [[yii\base\Event]] 类或其子类的实例。如:

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)
{
// ...发送 $message 的逻辑...

$event = new MessageEvent;
$event->message = $message;
$this->trigger(self::EVENT_MESSAGE_SENT, $event);
}
}


当 [[yii\base\Component::trigger()]] 方法被调用时,它将调用所有附加到命名事件(trigger 方法第一个参数)的事件处理器。

移除事件处理器

从事件移除处理器,调用 [[yii\base\Component::off()]] 方法。如:

// 处理器是全局函数
$foo->off(Foo::EVENT_HELLO, 'function_name');

// 处理器是对象方法
$foo->off(Foo::EVENT_HELLO, [$object, 'methodName']);

// 处理器是静态类方法
$foo->off(Foo::EVENT_HELLO, ['app\components\Bar', 'methodName']);

// 处理器是匿名函数
$foo->off(Foo::EVENT_HELLO, $anonymousFunction);



注意当匿名函数附加到事件后一般不要尝试移除匿名函数,除非你在某处存储了它。以上示例中,假设匿名函数存储为变量​​$anonymousFunction​​ 。

移除事件的全部处理器,简单调用 [[yii\base\Component::off()]] 即可,不需要第二个参数:

$foo->off(Foo::EVENT_HELLO);

类级别的事件处理器

以上部分,我们叙述了在实例级别如何附加处理器到事件。有时想要一个类的所有实例而不是一个指定的实例都响应一个被触发的事件,并不是一个个附加事件处理器到每个实例,而是通过调用静态方法 [[yii\base\Event::on()]] 在类级别附加处理器。

例如,​​活动记录​​​对象要在每次往数据库新增一条新记录时触发一个 [[yii\db\BaseActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] 事件。要追踪每个​​活动记录​​对象的新增记录完成情况,应如下写代码:

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');
});


每当 [[yii\db\BaseActiveRecord|ActiveRecord]] 或其子类的实例触发 [[yii\db\BaseActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] 事件时,这个事件处理器都会执行。在这个处理器中,可以通过 ​​$event->sender​​ 获取触发事件的对象。

当对象触发事件时,它首先调用实例级别的处理器,然后才会调用类级别处理器。

可调用静态方法[[yii\base\Event::trigger()]]来触发一个类级别事件。类级别事件不与特定对象相关联。因此,它只会引起类级别事件处理器的调用。如:

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​​ 指向触发事件的类名而不是对象实例。

Note: 因为类级别的处理器响应类和其子类的所有实例触发的事件,必须谨慎使用,尤其是底层的基类,如 [[yii\base\Object]]。

移除类级别的事件处理器只需调用[[yii\base\Event::off()]],如:

// 移除 $handler
Event::off(Foo::className(), Foo::EVENT_HELLO, $handler);

// 移除 Foo::EVENT_HELLO 事件的全部处理器
Event::off(Foo::className(), Foo::EVENT_HELLO);


全局事件
所谓全局事件实际上是一个基于以上叙述的事件机制的戏法。它需要一个全局可访问的单例,如应用实例。

事件触发者不调用其自身的 trigger() 方法,而是调用单例的 trigger() 方法来触发全局事件。类似地,事件处理器被附加到单例的事件。如:


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]));

全局事件的一个好处是当附加处理器到一个对象要触发的事件时,不需要产生该对象。相反,处理器附加和事件触发都通过单例(如应用实例)完成。

然而,因为全局事件的命名空间由各方共享,应合理命名全局事件,如引入一些命名空间(例:"frontend.mail.sent", "backend.mail.sent")。





 好了,理解了事件的大概实现逻辑之后,让我们从Yii\base\Component类的源码来看看具体的实现细节,我根据自己的理解在代码适当位置中加上了一些注释:

/**
*
* @var 存储事件列表的数组,形式:事件名称 => 对应的事件处理器列表
*/
private $_events = [];

/**
* 为某个事件绑定一个事件处理器
* @param $name:事件名称,字符串形式
* @param $handler:事件处理器,指定事件触发时调用的函数,有4种形式:
* 1.全局php函数名,字符串形式
* 2.[类名, 方法名],数组形式
* 3.[对象, 方法名],数组形式
* 4.匿名函数,形式:function($event){ ... }
* @param $data:事件触发时传递给事件处理器函数的参数,在事件处理器函数调用形式:$event->data
* @param $append:为true时表示将绑定的事件处理器添加在事件处理器列表的最后,为false则添加在最前面
*/
public function on($name, $handler, $data = null, $append = true)
{
$this->ensureBehaviors();
if ($append || empty($this->_events[$name])) {//添加到列表后面
$this->_events[$name][] = [$handler, $data];
} else {//添加到列表前面
array_unshift($this->_events[$name], [$handler, $data]);
}
}

/**
* 解绑事件处理器
* @param $name:事件名称
* @param $handler:要解绑的事件处理器,为null表示解绑这个事件的所有处理器,即删除整个事件
* @return boolean
*/
public function off($name, $handler = null)
{
$this->ensureBehaviors();
if (empty($this->_events[$name])) {//事件不存在
return false;
}
if ($handler === null) {//删除事件
unset($this->_events[$name]);
return true;
}

$removed = false;
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);//删除指定的事件处理器
$removed = true;
}
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
}
return $removed;
}

/**
* 触发一个事件
* @param $name:事件名称
* @param $event:\yii\base\Event类对象,作为传递给事件处理器的参数
* @return type
*/
public function trigger($name, Event $event = null)
{
$this->ensureBehaviors();
if (!empty($this->_events[$name])) {
if ($event === null) {//没有$event参数则创建一个默认对象
$event = new Event;
}
if ($event->sender === null) {//指定触发事件的对象
$event->sender = $this;
}
$event->handled = false;//事件是否处理完毕
$event->name = $name;//事件名称
foreach ($this->_events[$name] as $handler) {//遍历事件对应的处理器,逐个调用
$event->data = $handler[1];//这里把on()方法绑定事件处理器时传递的$data参数传递给事件处理器
call_user_func($handler[0], $event);//调用事件处理器方法
if ($event->handled) {//若在某个事件处理器中将$event->handled置为true,表示事件处理完毕,后面的处理器不再被调用
return;
}
}
}
//触发类级别事件
Event::trigger($this, $name, $event);
}



 案例:

namespace app\controllers;

Yii2之事件_数组_02Yii2之事件_事件处理_03

1 <?php
2
3 namespace app\controllers;
4
5 use Yii;
6 use yii\filters\AccessControl;
7 use yii\web\Controller;
8
9 use vendor\animal\Cat;
10 use vendor\animal\Mourse;
11 use vendor\animal\Dog;
12 use yii\base\Event;
13
14 class AnimalController extends Controller
15 {
16
17 /**
18 * Displays homepage.
19 *
20 * @return string
21 */
22 public function actionIndex()
23 {
24 $cat = new Cat();
25 $cat2 = new Cat();
26 $mourse = new Mourse();
27 $dog = new Dog();
28
29 //类级别事件
30 Event::on(Cat::class,'miao',[$mourse,'run']);
31
32 //事件的绑定
33 // $cat->on('miao',[$mourse,'run']);
34 //事件的绑定 顺序
35 // $cat->on('miao',[$dog,'look'],null,false);
36 $cat->shout();
37 $cat2->shout();
38
39
40 //全局事件
41 Yii::$app->on('miao', [$dog,'look']);
42 Yii::$app->trigger('miao');
43 }
44
45
46 }

AnimalController


Yii2之事件_数组_02Yii2之事件_事件处理_03

<?php
namespace vendor\animal;
use \yii\base\Event;
use \yii\base\Component;

class myEvent extends Event{
public $message;
}

class Cat extends Component{
public function shout(){
echo 'miao miao miao <br />';

$me = new myEvent();
$me->message = 'hello my event<br>';
//事件触发
$this->trigger('miao',$me);
}
}

Cat


Yii2之事件_数组_02Yii2之事件_事件处理_03

<?php
namespace vendor\animal;
class Mourse{
public function run($evnet){
echo $evnet->message;
echo ' i am running!<br/><br/>';
}
}

Mourse


Yii2之事件_数组_02Yii2之事件_事件处理_03

<?php
namespace vendor\animal;

class Dog{
public function look(){
echo 'i am looking<br/>';
}
}

Dog











举报

相关推荐

0 条评论