## **事件**
先来看下官方文档是怎么样描述事件的。
> 新版的事件系统可以看成是`5.1`版本行为系统的升级版,事件系统相比行为系统强大的地方在于事件本身可以是一个类,并且可以更好的支持事件订阅者。
事件相比较中间件的优势是事件比中间件更加精准定位(或者说粒度更细),并且更适合一些业务场景的扩展。例如,我们通常会遇到用户注册或者登录后需要做一系列操作,通过事件系统可以做到不侵入原有代码完成登录的操作扩展,降低系统的耦合性的同时,也降低了BUG的可能性。
如果不理解,可以看成是之前版本里面的**钩子和行为**。
官方文档提供的操作方法有:
```
//直接使用事件类触发
Event::trigger('UserLogin');
//动态绑定
Event::bind(['UserLogin' => 'app\event\UserLogin']);
//手动注册监听
Event::listen('UserLogin', 'app\listener\UserLogin');
```
事件触发之前是需要注册事件监听的。
也就是说使用`Event::trigger`之前需要`Event::listen`注册事件的监听(不讨论event.php)
在正常工作中应该很容易遇到这样的问题:
今天做一个用户登录模块,用户的需求就是简单的用户登录,登录完成以后不需要其他操作。然后作为码农的你很开心的写起了代码,很快就完成了。
第二天,用户又提出了新的需求,用户登录以后需要把用户登录次数增加,并且记录下用户的登录IP。然后码农的你还是很开新的写起了代码,这个也简单,很快的完成了。
第三天,用户又又来了。用户登录以后如果不是常用IP地址给用户发个邮件提醒下账号存在风险。
第四天,用户登录以后如果不是常用IP地址给用户发个邮件并且发送短信提醒下账号存在风险。
……
一个月以后,你的控制器,你的逻辑,不敢想象……
这个时候事件就是一个很好的解决方案。
下面是一个超级简单的事件用法。
首先是Model,定义了一个方法,获取用户ID为1的用户信息。
```
namespace app\model;
use think\Model;
class User extends Model
{
//获取用户ID为1的用户信息
public function getUserInfo()
{
return self::where('id', 1)->find();
}
}
```
然后用`php think make:event User`生成了一个User事件类,构造函数依赖注入UserModel;setLoginCount方法用于用户登录完成以后给用户登录次数+1(不要在意这个地方合不合理๑乛◡乛๑)。
```
namespace app\event;
use app\model\User as UserModel;
class User
{
public $user;
public function __construct(UserModel $user)
{
$this->user = $user;
}
//给用户登录次数+1
public function setLoginCount()
{
$userInfo = $this->user->getUserInfo();
$userInfo->login_count += 1;
return $userInfo;
}
}
```
定义了一个事件监听类User
```
namespace app\listener;
class User
{
public function handle(\app\event\User $event)
{
$userInfo = $event->user->getUserInfo();
echo 'listen监听器输出:' . json_encode($userInfo, JSON_UNESCAPED_UNICODE) . '<br />';
echo 'listen监听器输出:' . json_encode($event->setLoginCount(), JSON_UNESCAPED_UNICODE) . '<br />';
}
}
```
在控制器中执行
```
namespace app\controller;
use think\facade\Event;
use app\model\User;
class Index
{
public function index()
{
//……在此之前一系列的登录操作
$user = new User();
$userInfo = $user->getUserInfo();
echo '控制器输出:' . json_encode($userInfo, JSON_UNESCAPED_UNICODE) . '<br />';
Event::listen('UserLogin', 'app\listener\User');
Event::trigger('UserLogin');
}
}
```
输出结果
```
控制器输出:{"id":1,"username":"路人甲","login_count":0}
listen监听器输出:{"id":1,"username":"路人甲","login_count":0}
listen监听器输出:{"id":1,"username":"路人甲","login_count":1}
```
可以看到用户登录+1已经被解耦了,相应的如果增加用户IP记录,给登录异常用户发送邮件都可以这样做。
上面一直没有提到`Event::bind`,没错因为我也不知道具体有什么用(╥╯^╰╥)
`Event::trigger`可以传递参数
```
Event::trigger('UserLogin',time());
```
`Event::listen`是可以监听多个事件的
用`php think make:listen Email
`生成一个Email监听类
```
namespace app\listener;
class Email
{
public function handle()
{
echo '在Email监听器输出 <br />';
}
}
```
在控制器中调用2次`Event::listen`把多个监听事件类绑定到同一个事件标识上面
```
namespace app\controller;
use think\facade\Event;
use app\model\User;
class Index
{
public function index()
{
$user = new User();
$userInfo = $user->getUserInfo();
echo '控制器输出:' . json_encode($userInfo, JSON_UNESCAPED_UNICODE) . '<br />';
Event::listen('UserLogin', 'app\listener\User');
Event::listen('UserLogin', 'app\listener\Email');
Event::trigger('UserLogin');
}
}
```
输出
```
控制器输出:{"id":1,"username":"路人甲","login_count":0}
listen监听器输出:{"id":1,"username":"路人甲","login_count":0}
listen监听器输出:{"id":1,"username":"路人甲","login_count":1}
在Email监听器输出
```
发现可以这样使用是看了源代码发现,向`$this->listener`数组添加监听关系的时候是用数组的方式
```
/**
* 注册事件监听
* @access public
* @param string $event 事件名称
* @param mixed $listener 监听操作(或者类名)
* @param bool $first 是否优先执行
* @return $this
*/
public function listen(string $event, $listener, bool $first = false)
{
if (!$this->withEvent) {
return $this;
}
if (isset($this->bind[$event])) {
$event = $this->bind[$event];
}
if ($first && isset($this->listener[$event])) {
array_unshift($this->listener[$event], $listener);
} else {
$this->listener[$event][] = $listener;
}
return $this;
}
```
现在有个问题,如果我要定义多个监听标识就必须这样写:
```
Event::listen('UserLogin', 'app\listener\User');
Event::listen('Email', 'app\listener\Email');
Event::trigger('UserLogin');
Event::trigger('Email');
```
如果很多个,简直不敢相信,那么这个时候就有一个高大上、白富美的东西出现了,它的名字叫**事件订阅**
还拿上面的例子来做演示
我们使用`php think make:subscribe User`创建一个订阅类
```
namespace app\subscribe;
class User
{
public function onUserLogin(){
echo 'subscribe输出的onUserLogin<br />';
}
public function onEmail(){
echo 'subscribe输出的onEmail<br />';
}
}
```
控制器
```
namespace app\controller;
use think\facade\Event;
use app\model\User;
class Index
{
public function index()
{
$user = new User();
$userInfo = $user->getUserInfo();
echo '控制器输出:' . json_encode($userInfo, JSON_UNESCAPED_UNICODE) . '<br />';
Event::subscribe(\app\subscribe\User::class);
Event::trigger('UserLogin');
}
}
```
输出结果
```
控制器输出:{"id":1,"username":"路人甲","login\_count":0}
subscribe输出的onUserLogin
```
可以看到只是动态注册了一个事件订阅者类然后执行就可以方便的调用subscribe类里面的监听方法,这样就可以在一个subscribe类中监听多个事件。
当然subscribe类也有隐藏功能,如果想在subscribe类中一次性监听并且处理多个事件只需要在subscribe类中定义subscribe方法即可
```
namespace app\subscribe;
class User
{
public function onUserLogin(){
echo 'subscribe输出的onUserLogin<br />';
}
public function onEmail(){
echo 'subscribe输出的onEmail<br />';
}
public function subscribe(){
echo 'subscribe输出的subscribe<br />';
}
}
```
控制器不变的情况下输出:
```
控制器输出:{"id":1,"username":"路人甲","login\_count":0}
subscribe输出的subscribe
```
这是为什么呢,来看下源码:
```
/**
* 注册事件订阅者
* @access public
* @param mixed $subscriber 订阅者
* @return $this
*/
public function subscribe($subscriber)
{
if (!$this->withEvent) {
return $this;
}
$subscribers = (array) $subscriber;
foreach ($subscribers as $subscriber) {
if (is_string($subscriber)) {
$subscriber = $this->app->make($subscriber);
}
if (method_exists($subscriber, 'subscribe')) {
// 手动订阅
$subscriber->subscribe($this);
} else {
// 智能订阅
$this->observe($subscriber);
}
}
return $this;
}
```
源码中可以看到如果出现了subscribe方法,就会直接执行。
这个时候可能事件没有执行成功,一直在报错,因为确实我也没执行成功,我是修改了源码才可以正常执行
`vendor\topthink\framework\src\think\Event.php`文件中` $method = 'on' . substr(strrchr($event, '\\'), 1);`这一行会报错,因为`$events`数组在框架初始化的时候会加载进去一些预定义的事件,可能是官方遗漏,导致执行报错(我也不知道为什么(╥╯^╰╥),瞎猜的)
```
array (size=8)
0 => string 'think\event\AppInit' (length=19)
1 => string 'AppBegin' (length=8)
2 => string 'AppEnd' (length=6)
3 => string 'think\event\LogLevel' (length=20)
4 => string 'think\event\LogWrite' (length=20)
5 => string 'ResponseSend' (length=12)
6 => string 'ResponseEnd' (length=11)
7 => string 'app\event\UserLogin' (length=19)
```
```
/**
* 自动注册事件观察者
* @access public
* @param string|object $observer 观察者
* @return $this
*/
public function observe($observer)
{
if (!$this->withEvent) {
return $this;
}
if (is_string($observer)) {
$observer = $this->app->make($observer);
}
$events = array_keys($this->listener);
foreach ($events as $event) {
$method = 'on' . substr(strrchr($event, '\\'), 1);
if (method_exists($observer, $method)) {
$this->listen($event, [$observer, $method]);
}
}
return $this;
}
```
把源码改成
```
/**
* 自动注册事件观察者
* @access public
* @param string|object $observer 观察者
* @return $this
*/
public function observe($observer)
{
if (!$this->withEvent) {
return $this;
}
if (is_string($observer)) {
$observer = $this->app->make($observer);
}
$events = array_keys($this->listener);
foreach ($events as $event) {
$onAction=strrchr($event, '\\');
if(false===$onAction){
continue;
}
$method = 'on' . substr($onAction, 1);
if (method_exists($observer, $method)) {
$this->listen($event, [$observer, $method]);
}
}
return $this;
}
```
就可以正常跑起来了。
第一次写文档,语言描述,叙事顺序都不好,请包涵。还有文档中有错误的地方请指出来,我会及时修改的。因为个人能力有限,不确定一些地方是否是正确的用法(╥╯^╰╥)。