提及事件(event),咱们但是一点都不陌生。现实生活当中的事件无处不在,好比你发了一条微博,触发了一条事件,致使关注你的人收到了一条消息,看到你发的内容;好比你经过支付宝买东西,付了款,触发一个事件,致使你收到一条短信,告诉你刚刚扣款了,你帐户余额还有多少...php
咱们将事件稍稍加以抽象,发现事件具备某些共同特色,好比事件其实不是孤立存在,它只是某个流程或者工序的一个特殊的“点”,能够理解为时间点,也能够理解为逻辑的点;其次,事件上能够绑定一些“动做”,好比发送一条短信或者发送一个邮件;第三,我能够绑定,固然也能够解绑,若是我反感频频的短信提醒,我能够选择不发短信,我本身去查看帐户余额;第四,这些动做和主流程每每并无直接的关系,每每是“附加”的:我已经付完款了,你发短信或者不发,发邮件或者直接客服通知我其实影响不大,并不影响我购物这个行为自己——反正我已经付完款,预期不久就会收到商品了。css
其实,说到这里,已经有点入戏的感受了。人有生老病死,一年有春夏秋冬四季演替,封建王朝有兴盛、停滞、衰亡的周期律——“其兴也勃焉,其亡也忽焉”。换句话说,人,季节,王朝等等这些世间万物都有本身的生命周期。nginx
那么在软件行业,一个系统,一个组件,一个功能,一个类都是有本身的生命周期的——建立、运行、销毁。好比一个类(Class)都要通过__construct()
,调用各个类方法,__destruct() 的过程。每一个程序的运行,要理解为一个过程或者流程。那么这样来理解事件就有意义了:事件无非就是这个过程之中一些有意义的“点”。这些点是人为作的设定,好比插入数据库数据,那么校验前、后,插入前、后就多是几个有意义的时间节点,把这些节点当作一个个的事件,就更加便于咱们去理解这整个过程。复杂的东西理解起来困难,咱们分红若干个阶段来理解岂不是就简单许多了吗?想一想为啥计算机网络为啥要分红物理层、数据链路层、网络层,运输层、应用层这个简单道理就好了。除了咱们更好的去理解程序的运行流程,更为重要的是还可以使咱们可以“介入”这个流程,改变这个流程,从而实现咱们的目的。这就是往事件上“附加”一些动做或者行为了,专业点说,就是事件处理器或者事件监听者。咱们想要在某个特定的时间点作点什么,就事先在这个对应的事件上绑定事件处理器,当流程走到这一步时,相应的处理器就被执行,完成咱们事先设定的目的。想象一下:软件的运行就是沿着本身设定的路线,走过一个又一个重要节点,同时触发这些节点事件,最终走到本身生命终点的一个过程。将事件理解为流程中的节点,不只能够帮我更好的认识程序,也能更好的帮助咱们改造程序。数据库
事件的实现,实际上是观察者模式的一种体现。可能有人会说,为啥是观察者模式?Yii的事件并不符合观察者模式的经典定义啊?的确,Yii的事件实现机制中确实不存在经典Observer和Subject即观察者和被观察者。可是,不要在乎这些细节——观察者模式主要面临场景是一对多松耦合的对象之间的关系,要解决的问题就是对象状态改变,其余对象能获得通知。“一对多”和“松耦合”是其核心要义和功能。看看Yii的事件其实正好能实现这两个功能:Yii的每一个事件(beforeSave,afterSave这些)都是一个被观察者,每一个事件处理器都是它的观察者,被观察者被触发时,全部观察者都依次要执行。编程
小编心得:观察者模式的经典定义——观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的全部依赖者都会受到通知并自动更新。数组
下面,咱们来谈谈Yii2自己是如何实现事件的。网络
在Yii中,事件是Component引入的,BaseObject 是不支持事件的,BaseObject只支持属性。当你须要使用事件时,就须要继承Component或者它的子类,而不能直接继承BaseObject。好在咱们以前反复提过,Yii2是基于Component的,就是说,Yii2中大部分类都是Component的子类,是天生带有事件功能的。和其余框架相比,我觉这是Yii2的一大优点——从“原子”层面都已经让类具有很强的扩展性了。面对具体业务场景,事件每每成为一把利剑,使得使用者驾轻就熟。同时,\yii\base\Event是于Yii2事件功能紧密相连的一个类,他封装与事件相关的有关数据,并能够自定义一些功能函数做为辅助。数据结构
class Event extends BaseObject { // 事件的名称 public $name; // 事件发布者,一般是调用了 trigger() 的对象或类,也可传递别的对象。 public $sender; // 此事件是否被处理了,若为true,那事件的处理到此为止 public $handled = false; // 事件的相关数据 public $data; // 保存全局事件的处理器 private static $_events = []; // 绑定类级别事件handler public static function on($class, $name, $handler, $data = null, $append = true) { // ... } // 取消类级别事件handler的绑定 public static function off($class, $name, $handler = null) { // ... } // 解绑全部类级别事件的handler public static function offAll() { self::$_events = []; } // 判断一个类和其全部父类上是否绑定有事件处理器 public static function hasHandlers($class, $name) { // ... } // 触发类级别的事件,执行全部这个事件上绑定的handler public static function trigger($class, $name, $event = null) { // ... } }
所谓事件处理器(事件handler)就是事件处理程序。“触发事件”和执行“事件处理器”实际上是一个意思。说到底,事件处理器究竟是什么呢?其实仅仅只是一个callable——或者是全局函数,或者是类的方法等。本质上说,事件处理器就是一段PHP代码,一个PHP函数。app
具体说来,handler 有这么几种形式:框架
function ($event) { ... }
$object->handleClick() 即 [$object, 'handleClick']
Page::handleClick()
不管是哪一种形式的handler,它们最终都必需要有以下的形式:
function ($event){...}
这里的$event正是yii\base\Event类或者其子类的实例。
有了事件处理器,就能够将其绑定到特定的事件上了。绑定事件处理器是经过yii\base\Component::on() 来进行,相应的解绑是yii\base\Component::off()。
// 绑定的过程就是将handler写入$_event[]的过程 public function on($name, $handler, $data = null, $append = true) { $this->ensureBehaviors(); //$append为true时$handler置于数组最后,触发时最后被执行 if ($append || empty($this->_events[$name])) { $this->_events[$name][] = [$handler, $data]; } else { //$append 为false时$handler置于数组最前,触发时最早被执行 array_unshift($this->_events[$name], [$handler, $data]); } }
例如:
$order = new Order(); $order->on(Order::EVENT_CREATING, [$obj, 'verifyOrder',false]) ; $order->on(Order::EVENT_CREATED, 'checkOrder') ; $order->on(Order::EVENT_PAYED, ['\common\lib\Email', 'send']) ; $order->on(Order::EVENT_PAYED, function ($event) { echo '订单已经支付'; });
绑定事件处理器时能够提供额外数据做为 \yii\base\Component::on() 方法的第三个参数,数据在事件被触发时能被处理器使用。如:
// 第三个参数传递数据,当订单被支付时输出'订单已支付' $order->on(Order::EVENT_PAYED, function ($event) { echo $event->data; }, '订单已支付');
事件处理器的绑定能够如上在运行的时候进行,也能够在配置的进行绑定。不管哪一种方式,都必须是先绑定,后触发。
public function off($name, $handler = null) { $this->ensureBehaviors(); if (empty($this->_events[$name])) { return false; } // 若是没有传递$handler参数,那么解绑事件$name名下的全部handler if ($handler === null) { unset($this->_events[$name]); return true; } $removed = false; // 若是传递参数$name,那么解绑$name下特定的handler:$handler foreach ($this->_events[$name] as $i => $event) { // 注意$event的第一个元素是hanlder,第二个是$data if ($event[0] === $handler) { unset($this->_events[$name][$i]); $removed = true; } } if ($removed) { $this->_events[$name] = array_values($this->_events[$name]); } return $removed; }
解绑的例子:
// 解绑事件下全部的事件处理器 $order->off(Order::EVENT_CREATING) ; // 解绑事件下checkOrder处理器 $order->off(Order::EVENT_CREATED, 'checkOrder') ;
通常来讲,能够经过on绑定的handler,均可以经过off来解绑。可是匿名函数是除外的,你不能解绑某个特定的匿名函数,除非你事先将其保存为变量,然后经过这个变量来指定其解绑。
$myHandler = function($event) {....} $order->on(Order::EVENT_CREATED, $myHandler); $order->off(Order::EVENT_CREATED, $myHandler);
在Component中$_events专门用来维护这些handler:
private $_events = [];
事件的绑定逻辑是这样的:
$append
是否为 true , 表示所要绑定的事件handler要放在 $_event[] 数组的最后面。这也是默认的绑定方式。$_event[EVENT_NAME][]
只有这么一个元素,既是第一个也是最后一个handler在 $event[] 数组中的位置就表明执行的前后顺序,在现实生活中每每意义重大。
事件经过Component::trigger()来触发,触发本质就是执行事件handler的过程。
$order->trigger(Order::EVENT_CREATED);
源码以下:
public function trigger($name, Event $event = null) { $this->ensureBehaviors(); if (!empty($this->_events[$name])) { // 执行handler必须传递一个Event实例,用来传递数据 if ($event === null) { $event = new Event(); } // 指定是谁触发的这个事件,默认就是trigger调用者自身 if ($event->sender === null) { $event->sender = $this; } $event->handled = false; // 默认事件没有被处理 $event->name = $name; // 事件名称 foreach ($this->_events[$name] as $handler) { $event->data = $handler[1]; // 最关键的地方:全部handler都是经过call_user_func来执行的 call_user_func($handler[0], $event); // 若是在$handler[0]中,$event->handled被置为true,表示事件已经被处理好了,后续handler不用再执行了 if ($event->handled) { return; } } } // 执行类级别的事件处理器 Event::trigger($this, $name, $event); }
在触发事件时,咱们常常会经过Event对象来传递数据: 假若有个场景是在文章下面评论送积分,那么评论后就会触发送积分的事件user_after_publish,另外咱们知道送积分还有不少别的场合,不一样场合送的分数不同。所以咱们积分须要有个类PointEvent来表示:
use yii\base\Event; class PointEvent extends Event { //要赠送的积分数量 public $points = 0; // 事件处理结果消息 public $msg = ''; // 其余方法 }
发表评论以前,先绑定“送积分”事件handler:
// 绑定handler $user = Yii::$app->user->identity; $user->on(User::EVENT_AFTER_PUBLISH, [$obj, 'afterPublish']); .... // 在某个时间点触发 // 实例化一个PointEvent,points 指定为 10 $event = new PointEvent(); $event->points = 10; $user->trigger(User::EVENT_AFTER_PUBLISH, $event);
事件处理器要为发表评论的用户积分+10:
public function afterPublish($event) { $user = $event->sender; $points = $event->points; $user->points += $points; $user->save(); }
说到这里,可能有的小伙伴会问,用户发表评论得到积分这么个功能为啥要经过这种方式来实现,为啥不用下面“简单”的方式来实现呢:
public function publishComment() { $param = Yii::$app->request->post(); $user = Yii::$app->user->identity; // 新建评论 $comment = new Comment(); $comment->load($param, 'data'); $comment->save(); // 更新用户积分 $user->points += intval($param['points']); $user->save(); }
这样作,就是把新建评论和更新用户放在一个方法里面。可是这样作将会有个隐形的问题,若是用户评论以后,不光要积分+10,还要告知文章做者怎么办?这里只能在publishComment()后面继续添加代码了,若是哪天又要发送邮件啥的,还得继续往里面添加代码。这违反了“面对扩展开放,面对修改关闭”的编程原则,长此以往这块代码就会变得很是臃肿,很难再维护,分出去再写几个方法也无济于事。
分析这缘由,就是从功能上说,评论和用户得到积分实际上是松耦合的关系——你能够给积分也能够不给积分。可是在publishComment()方法你把这两个功能捆绑的死死的,丝毫分不开——这就是一种糟糕的设计。
相反,用事件就能很好解决这个问题。咱们说,事件实际上是一个流程上的某个特定的点,这里是流程就是用户发表评论,EVENT_AFTER_PUBLISH是发表成功后的一个节点,程序走到这里,触发一下。有处理器就执行,没有就继续日后执行。咱们绑定了一个送积分的处理器 ,也能够绑定推送消息的处理器。这么看待问题,就已经解耦了两种功能。在实现上,将发表评论看作流程自己,而送积分,推送提醒看作是“附加”的,在须要的时候绑定下,不须要就不绑定。
下面是主流程:
public function publishComment() { $user = Yii::$app->user->identity; // 新建以前的时间点:触发EVENT_BEFORE_PUBLISH $user->trigger(User::EVENT_BEFORE_PUBLISH); // 新建评论 $comment = new Comment(); $comment->load($param, 'data'); $comment->save(); // 新建以后的时间点:触发下 EVENT_AFTER_PUBLISH $user->trigger(User::EVENT_AFTER_PUBLISH, $event); }
有的场合是须要送积分:
$user->on(User::EVENT_AFTER_PUBLISH, [Points, 'givePoints']);
有的场合是要发邮件提醒:
$user->on(User::EVENT_AFTER_PUBLISH, [Email, 'notifyPoserOwner']);
还有的场合是须要判断用户有没有发表评论的权限:
$user->on(User::EVENT_BEFORE_PUBLISH, [Authorization, 'checkAuth']); ... public function checkAuth($event) { $user = $event->sender; if (!$user->isAdmin) { throw new AccessDeniedException('你没有权限发表评论'); } }
事件处理器Points::givePoints,Email::notifyPoserOwner,Authorization::checkAuth 分布在不一样的类中,并无出如今上面publishComment里面,并且可增可减,彻底视须要而定。所以真正知足了“开闭原则”的要求。
小编心得:开闭原则——对扩展开放,对修改关闭。这是代码组织当中的一条很是重要的原则。
前面的事件,都是针对类的实例而言的,也就是事件的触发、处理所有都在实例范围内。这种级别的事件用情专注,不与类的其余实例发生关系,也不与其余类、其余实例发生关系。除了实例级别的事件外,还有类级别的事件。
这就比如是公司里的不一样阶层。底层的码农们只能本身发发牢骚,我的的喜怒哀乐只发生在本身身上,影响不了其余人。而团队负责人若是心情很差,整个团队的全部成员今天都要战战兢兢,慎言慎行。到了公司老总那里,他今天不爽,那么公司全部员工均可能跟着遭殃。事件也是这样的,不一样层次的事件,决定了他影响到的范围。
类级别的事件是由\yii\base\Event中的方法来实现的,和实例级别的事件在Component中的实现原理大同小异,只不过做用范围不一样:
好比,我打算全部人发表评论后,都要发送给文章做者了。那么每一个用户都去绑定一下那是多么糟糕的事情,那咱们能够将发送提醒绑定在User类上,那么任何$user实例在发表评论后的节点上都绑定了发送提醒的处理器了。
若是某个场合又给某个实例$user
绑定了送积分的事件,那么等评论一发表,用户首先得到积分,而后收到邮件——实例级别的事件老是先触发,类级别的事件老是后触发。别的$user
没有绑定送积分的话固然就没有积分送了。
类级别事件handler的绑定经过Event::on()来实现的:
public static function on($class, $name, $handler, $data = null, $append = true) { $class = ltrim($class, '\\'); // 若是$append为true,那么$handler置于handler列表最后面 if ($append || empty(self::$_events[$name][$class])) { self::$_events[$name][$class][] = [$handler, $data]; } else { // 若是$append为true,那么$handler置于handler列表最前面 array_unshift(self::$_events[$name][$class], [$handler, $data]); } }
相比Component::on(),Event::on()
还须要传入类(名) $class
。Event维护的$_event[]
要比Component下的$_event[]
多一个维度:$class
。绑定和解绑事件handler其实至关于向$_events[$name][$class]
数组插入/删除handler的过程。当$append为true,$handler
将被最后执行(默认状况),当为false,$handler
将被第一个执行,除非有后面的handler取代其位置。
Event::on(User::class, Comment::EVENT_AFTER_PUBLISH, [$this, 'afterPublish'], $data); Event::on(User::class, Comment::EVENT_BEFORE_PUBLISH, [$this, 'beforePublish'], [], false);
类级别事件解绑经过Event::off(),和实例级别事件解绑过程相似:
public static function off($class, $name, $handler = null) { $class = ltrim($class, '\\'); if (empty(self::$_events[$name][$class])) { return false; } if ($handler === null) { unset(self::$_events[$name][$class]); return true; } $removed = false; foreach (self::$_events[$name][$class] as $i => $event) { if ($event[0] === $handler) { unset(self::$_events[$name][$class][$i]); $removed = true; } } // ... }
若是传递参数$handler
,那么解绑类$class
下事件$name
中的$handler,不然就将$class
下的事件$name
的处理器清空。
事件的触发经过Event::trigger()来实现:
public static function trigger($class, $name, $event = null) { if (empty(self::$_events[$name])) { return; } if ($event === null) { $event = new static(); } $event->handled = false; $event->name = $name; if (is_object($class)) { // 当$class为一个对象时,sender默认便指定为这个对象 if ($event->sender === null) { $event->sender = $class; } $class = get_class($class); } else { $class = ltrim($class, '\\'); } // 将$class,$class全部的父类,$class全部实现的接口放入待检查的数组$classes $classes = array_merge( [$class], class_parents($class, true), class_implements($class, true) ); foreach ($classes as $class) { if (empty(self::$_events[$name][$class])) { continue; } // 若是$classes中绑定有事件$name,那么就执行其名下全部handler foreach (self::$_events[$name][$class] as $handler) { $event->data = $handler[1]; // 类级别的事件handler最终也是经过call_user_func来执行,并传递事件$event call_user_func($handler[0], $event); if ($event->handled) { return; } } } }
经过上面的源码和注释,可知类级别的事件能够向全部父类传递,子类事件handler执行,父类同名事件的handler也要执行。因为类级别事件会被类自身、类的实例、后代类、后代类实例所触发,因此,对于越底层的类而言,其类事件的影响范围就越大。所以,在使用类事件上要注意,尽量日后代类安排,以控制好影响范围,尽量不在基础类上安排类事件。
还有一种更为抽象方式来处理事件,你能够为特殊的事件建立一个独立的接口, 而后在你须要的类中实现它。好比:
namespace app\interfaces; interface DanceEventInterface { const EVENT_DANCE = 'dance'; }
而后在两个类中实现:
class Dog extends Component implements DanceEventInterface { public function meetBuddy() { echo "Woof!"; $this->trigger(DanceEventInterface::EVENT_DANCE); } } class Developer extends Component implements DanceEventInterface { public function testsPassed() { echo "Yay!"; $this->trigger(DanceEventInterface::EVENT_DANCE); } }
要处理由这些类触发的 EVENT_DANCE ,调用 Event::on()来绑定,并将接口类名做为第一个参数: 你能够在这些类中触发这个事件:
// trigger event for Dog class Event::trigger(Dog::class, DanceEventInterface::EVENT_DANCE); // trigger event for Developer class Event::trigger(Developer::class, DanceEventInterface::EVENT_DANCE);
其实这没有什么神秘的,看看Event::trigger()中有这么一段你就明白了:
$classes = array_merge( [$class], class_parents($class, true), // $class 实现的接口事件也触发下 class_implements($class, true) );
全局事件本质上也是实例事件的一种,只不过是能够在任何地方进行绑定,解绑,触发。好比Application和Yii::$app所管理的任何组件。全局事件无非是由于这些实例是可全局访问的罢了。由于这些实例都是继承Component的,因此用法和Component的事件基本相同:
use app\components\Foo; Yii::$app->on('beforeRequest', function ($event) { echo get_class($event->sender); // 显示 "app\components\Foo" }); Yii::$app->trigger('beforeRequest', new Event(['sender' => new Foo]));