大半年前接手的一个项目里,用户注册模块的控制器简直像个“大总管”,注册完用户之后,先是发邮件,再调短信接口,最后还要写日志、更新统计,一套流程下来近两百行,而且后期每加一个新的后续操作,就得跑去改控制器。后来试着把这一堆副作用拆分到事件系统里,发现整个注册方法清爽到不敢相信——只做了一件事:创建用户并触发事件。其余的邮件、短信、日志全部由各自的事件监听器接管,新增一个任务也只需加一个新的监听器,完全不用碰主流程。
这篇文章就复原那次改造的核心思路和具体代码,让你读完就能在自己的ThinkPHP 8项目里用起事件系统来。
事件系统能解决什么
在传统的MVC写法里,很多“后续处理”——比如注册后发邮件、下单后减库存、登录后记录日志——会跟着业务主逻辑牢牢捆在一起。后面加功能的时候,就不得不去修改原来的方法,测试也得全量重跑。事件系统相当于在主逻辑上开了一个“标准化接口”:做完核心操作后,抛出某个事件名称,所有关心这个事件的监听器自动执行,彼此之间隔离,互不影响。
ThinkPHP 8对事件系统的支持很完善,使用起来也很直接:定义事件、监听器,然后通过Event::trigger触发即可。
第一步:快速体验一个简单事件
假设我们要在用户登录后记录一条日志。先在app/event.php事件定义文件(如果不存在就新建)里绑定:
// app/event.php
return [
'bind' => [
'UserLogin' => 'applistenerUserLoginListener',
],
];
创建监听器app/listener/UserLoginListener.php:
namespace applistener;
class UserLoginListener
{
public function handle($event)
{
// $event 是触发事件时传递的数据
thinkfacadeLog::info('用户登录: ' . $event['user_id']);
}
}
在控制器里触发:
use thinkfacadeEvent;
Event::trigger('UserLogin', ['user_id' => 1]);
运行后日志文件里就会多出一条记录。这就是事件系统的骨架:绑定——监听——触发。
第二步:向监听器传递结构化数据(事件类)
简单场景用字符串事件名加数组就够,但数据一多就容易散落。ThinkPHP 8支持将事件封装成类,监听器的handle方法直接接收该类的实例,更利于类型提示和IDE辅助。
新建事件类app/event/UserRegistered.php:
namespace appevent;
class UserRegistered
{
public function __construct(
public readonly int $userId,
public readonly string $email,
public readonly string $phone,
) {}
}
然后定义对应的监听器app/listener/SendWelcomeMail.php:
namespace applistener;
use appeventUserRegistered;
class SendWelcomeMail
{
public function handle(UserRegistered $event): void
{
// 发送欢迎邮件
thinkfacadeLog::info("向 {$event->email} 发送注册欢迎邮件");
// 实际调用邮件服务,这里仅示意
}
}
在event.php中进行绑定:
return [
'bind' => [],
'listen' => [
'UserRegistered' => [
applistenerSendWelcomeMail::class,
],
],
];
触发方式变为:
use appeventUserRegistered;
use thinkfacadeEvent;
Event::trigger(new UserRegistered(1, 'test@example.com', '13800138000'));
第三步:一个事件多个监听器
用户注册后需要发邮件、发短信、记录日志。可以继续添加监听器并挂载到同一个事件下:
// app/listener/SendWelcomeSms.php
namespace applistener;
use appeventUserRegistered;
class SendWelcomeSms
{
public function handle(UserRegistered $event): void
{
thinkfacadeLog::info("向 {$event->phone} 发送欢迎短信");
}
}
注册监听:
'listen' => [
'UserRegistered' => [
applistenerSendWelcomeMail::class,
applistenerSendWelcomeSms::class,
],
],
触发一次事件,两个监听器都会执行,而且按照绑定顺序执行。如果想调整顺序,移动数组位置即可。
第四步:用事件订阅者集中管理
当监听器越来越多,放在同一个文件里便于维护。ThinkPHP支持事件订阅者,一个类里可以响应多个不同事件。例如我们创建一个UserEventSubscriber:
namespace appsubscribe;
use appeventUserRegistered;
class UserEventSubscriber
{
public function onUserRegistered(UserRegistered $event): void
{
// 这里可以发邮件、发短信,或者调用专门的业务类
applistenerSendWelcomeMail::handle($event);
applistenerSendWelcomeSms::handle($event);
}
public function subscribe(): array
{
return [
'UserRegistered' => 'onUserRegistered',
];
}
}
然后在event.php中注册订阅者:
return [
'listen' => [],
'subscribe' => [
appsubscribeUserEventSubscriber::class,
],
];
订阅者的优势在于,所有与该用户模块相关的事件响应都集中在一个文件,修改时不用在多个监听器间跳转。
完整案例:用户注册控制器重构对比
重构前的控制器(节选):
public function register(Request $request)
{
$data = $request->post();
$user = User::create($data);
// 发邮件
Mail::send($user->email, '欢迎注册', '欢迎...');
// 发短信
Sms::send($user->phone, '您已注册成功');
// 写日志
Log::info('用户注册', ['id' => $user->id]);
// 更新统计
Stats::increment('register_count');
return json(['msg' => '注册成功']);
}
各种副作用堆积,后期再加一个“注册后赠送积分”就又得改这里。重构后:
use appeventUserRegistered;
use thinkfacadeEvent;
public function register(Request $request)
{
$data = $request->post();
$user = User::create($data);
Event::trigger(new UserRegistered($user->id, $user->email, $user->phone));
return json(['msg' => '注册成功']);
}
赠送积分只需加一个监听器GrantPoints并绑定到UserRegistered事件,主业务纹丝不动。整个流程的依赖方向也清晰了:控制器依赖事件,事件不依赖任何监听器,新增监听器不影响原有代码。
实际运行的解释
上面的监听器代码里我们用Log来占位,真实项目里你会注入邮件、短信服务类。推荐在监听器构造器里通过依赖注入或app()助手获取服务,保持可测试性。比如:
class SendWelcomeMail
{
protected $mailer;
public function __construct()
{
$this->mailer = app('mailer'); // 假设已绑定邮件服务
}
public function handle(UserRegistered $event): void
{
$this->mailer->send($event->email, '欢迎');
}
}
注意事项
- 事件不要滥用:只有那些明确的“已完成某某动作”才适合作为事件,把每个小操作都写成事件反而会增加理解成本。
- 异步执行:如果发邮件耗时较长,可以结合ThinkPHP的队列系统,在监听器里把任务推到队列,避免阻塞响应。
- 事件命名:建议使用“系统.动作”格式,如
User.Login、Order.Paid,或者直接使用事件类名。 - 记得清理:当使用订阅者模式时,确定不再需要的事件绑定记得在
subscribe返回数组中移除,以免失效但没报错导致沉默。
总结
事件系统不是什么新概念,但ThinkPHP 8把它做得足够轻巧和贴心。从字符串事件到事件类,从分散监听器到集中订阅者,每个层次都能按需使用,没有一个强制的大框架。用户注册这个场景只是一个缩影,登录、支付、文章发布……所有主流程之外的零散操作都可以用事件解耦成独立的监听器。
下次再碰到一个方法里掺了五六个不相干的后续处理,试着把它们拆成事件,你会发现修改代码的底气都足了不少——因为你知道,改一个监听器,绝不会拖死主流程。

