## **事件**
先來看下官方文檔是怎么樣描述事件的。
> 新版的事件系統可以看成是`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;
}
```
就可以正常跑起來了。
第一次寫文檔,語言描述,敘事順序都不好,請包涵。還有文檔中有錯誤的地方請指出來,我會及時修改的。因為個人能力有限,不確定一些地方是否是正確的用法(╥╯^╰╥)。