ThinkPHP 5.x 到 8.x 行为扩展迁移指南
目录
核心变化概述
ThinkPHP 5.x 的行为扩展机制
ThinkPHP 5.x 使用 行为扩展(Behavior) 机制:
- 通过
tags.php配置文件绑定到系统钩子 - 行为类必须实现
run()方法作为入口 - 使用
Hook::listen()触发行为 - 配置相对固定,扩展性有限
示例:TP5 行为类
php
<?php
namespace app\admin\behavior;
class ModuleInitBehavior
{
// 行为扩展的执行入口必须是run
public function run(&$params)
{
// 业务逻辑
$this->_initialize();
}
private function _initialize()
{
// 初始化逻辑
}
}
示例:TP5 配置文件 (tags.php)
php
<?php
return [
'module_init' => [
'app\\admin\\behavior\\ModuleInitBehavior',
],
'action_begin' => [
'app\\admin\\behavior\\ActionBeginBehavior',
],
];
ThinkPHP 8.x 的事件系统
ThinkPHP 8.x 移除了行为扩展机制 ,改用更现代的 事件系统(Event):
- 基于 PSR-14 事件调度标准
- 事件类封装事件数据
- 监听器类处理事件逻辑
- 支持事件订阅器,一个类订阅多个事件
- 更加灵活和标准化
核心优势:
- ✅ 更符合现代 PHP 开发规范
- ✅ 事件和监听器解耦,易于测试
- ✅ 支持依赖注入
- ✅ 可以动态注册和注销监听器
- ✅ 更好的 IDE 支持和类型提示
系统事件对照表
| ThinkPHP 5.x 钩子 | ThinkPHP 8.x 事件 | 触发时机 | 说明 |
|---|---|---|---|
app_init |
AppInit |
应用初始化 | 应用开始时触发 |
app_begin |
已移除 | - | 使用中间件替代 |
module_init |
RouteLoaded |
路由加载完成 | 相当于模块初始化 |
action_begin |
HttpRun |
控制器执行前 | 操作开始执行 |
view_filter |
HttpEnd |
响应输出前 | 视图内容过滤 |
log_write |
LogWrite |
日志写入时 | 日志记录 |
app_end |
HttpEnd |
应用结束 | 响应输出后 |
注意事项:
app_begin在 TP8 中被移除,推荐使用中间件实现相同功能HttpEnd事件同时替代了view_filter和app_end- TP8 中
module()方法改为app()方法
基础概念
1. 事件类(Event)
事件类用于封装事件相关的数据,类似于 TP5 中传递给 behavior 的 $params 参数。
特点:
- 纯数据对象,不包含业务逻辑
- 可以携带任意数据
- 通过构造函数初始化数据
示例:
php
<?php
declare(strict_types=1);
namespace app\event;
/**
* 模块初始化事件
*/
class ModuleInit
{
public string $actionName;
public string $controllerName;
public string $moduleName;
public string $method;
public function __construct()
{
$request = request();
$this->actionName = $request->action();
$this->controllerName = $request->controller();
$this->moduleName = $request->app(); // TP8 使用 app() 替代 module()
$this->method = $request->method();
}
}
2. 监听器类(Listener)
监听器类用于处理事件,相当于 TP5 中的 Behavior 类。
特点:
- 必须实现
handle()方法 - 接收事件对象作为参数
- 包含具体的业务逻辑
示例:
php
<?php
declare(strict_types=1);
namespace app\listener;
use app\event\ModuleInit;
/**
* 模块初始化监听器
*/
class ModuleInitListener
{
/**
* 事件监听处理
*/
public function handle(ModuleInit $event): void
{
// 可以访问事件数据
$controller = $event->controllerName;
$action = $event->actionName;
// 执行业务逻辑
$this->initialize($event);
}
private function initialize(ModuleInit $event): void
{
// 初始化逻辑
}
}
3. 事件订阅器(Subscriber)
可以在一个类中订阅多个事件,更加灵活和集中管理。
示例:
php
<?php
declare(strict_types=1);
namespace app\subscribe;
/**
* 后台管理事件订阅器
*/
class AdminSubscribe
{
/**
* 订阅的事件
*/
public function subscribe($event): array
{
return [
'RouteLoaded' => 'onModuleInit',
'HttpRun' => 'onActionBegin',
'HttpEnd' => 'onAppEnd',
];
}
/**
* 模块初始化
*/
public function onModuleInit($event): void
{
// 模块初始化逻辑
}
/**
* 操作开始
*/
public function onActionBegin($event): void
{
// 操作开始逻辑
}
/**
* 应用结束
*/
public function onAppEnd($event): void
{
// 应用结束逻辑
}
}
4. 事件配置文件
在 app/event.php 中配置事件和监听器的绑定关系。
php
<?php
return [
'bind' => [
// 事件绑定(一对一)
],
'listen' => [
// 事件监听(一对多)
'AppInit' => [],
'RouteLoaded' => [
\app\listener\ModuleInitListener::class,
],
'HttpRun' => [
\app\listener\ActionBeginListener::class,
],
'HttpEnd' => [
\app\listener\ViewFilterListener::class,
],
'LogWrite' => [],
],
'subscribe' => [
// 事件订阅器
\app\subscribe\AdminSubscribe::class,
],
];
迁移示例
示例 1:ModuleInitBehavior 迁移
TP5 原代码
php
<?php
// Thinkphp5.0.24/application/admin/behavior/ModuleInitBehavior.php
namespace app\admin\behavior;
use think\Config;
class ModuleInitBehavior
{
protected static $actionName;
protected static $controllerName;
protected static $moduleName;
protected static $method;
// 行为扩展的执行入口必须是run
public function run(&$params)
{
self::$actionName = request()->action();
self::$controllerName = request()->controller();
self::$moduleName = request()->module();
self::$method = request()->method();
$this->_initialize();
}
private function _initialize()
{
// DDOS 防护检查
if (file_exists('application/admin/logic/DdosLogic.php')) {
$ddosLogic = new \app\admin\logic\DdosLogic;
if (method_exists($ddosLogic, 'firewall_login_check')) {
$ddosLogic->firewall_login_check();
}
}
$this->vertifyCode();
}
private function vertifyCode()
{
$ctlActArr = ['Admin@login', 'Admin@vertify'];
$ctlActStr = self::$controllerName.'@'.self::$actionName;
if (in_array($ctlActStr, $ctlActArr)) {
$row = tpSetting('system.system_vertify', [], 'cn');
$row = json_decode($row, true);
$baseConfig = Config::get("captcha");
if (!empty($row)) {
foreach ($row['captcha'] as $key => $val) {
if ('default' == $key) {
$baseConfig[$key] = array_merge($baseConfig[$key], $val);
} else {
$baseConfig[$key]['is_on'] = $val['is_on'];
$baseConfig[$key]['config'] = array_merge($baseConfig['default'], $val['config']);
}
}
}
Config::set('captcha', $baseConfig);
}
}
}
TP8 迁移代码
步骤 1:创建事件类
php
<?php
// app/event/ModuleInit.php
declare(strict_types=1);
namespace app\event;
/**
* 模块初始化事件
*/
class ModuleInit
{
public string $actionName;
public string $controllerName;
public string $moduleName;
public string $method;
public function __construct()
{
$request = request();
$this->actionName = $request->action();
$this->controllerName = $request->controller();
$this->moduleName = $request->app(); // TP8 中使用 app() 替代 module()
$this->method = $request->method();
}
}
步骤 2:创建监听器类
php
<?php
// app/listener/ModuleInitListener.php
declare(strict_types=1);
namespace app\listener;
use app\event\ModuleInit;
use think\facade\Config;
/**
* 模块初始化监听器
*/
class ModuleInitListener
{
/**
* 事件监听处理
*/
public function handle(ModuleInit $event): void
{
// 执行初始化逻辑
$this->initialize($event);
}
private function initialize(ModuleInit $event): void
{
// DDOS 防护检查
if (file_exists(app_path() . 'admin/logic/DdosLogic.php')) {
$ddosLogic = new \app\admin\logic\DdosLogic();
if (method_exists($ddosLogic, 'firewall_login_check')) {
$ddosLogic->firewall_login_check();
}
}
// 验证码配置
$this->vertifyCode($event);
}
/**
* 登录验证码配置
*/
private function vertifyCode(ModuleInit $event): void
{
$ctlActArr = [
'Admin@login',
'Admin@vertify',
];
$ctlActStr = $event->controllerName . '@' . $event->actionName;
if (in_array($ctlActStr, $ctlActArr)) {
$row = tpSetting('system.system_vertify', [], 'cn');
$row = json_decode($row, true);
$baseConfig = Config::get('captcha');
if (!empty($row)) {
foreach ($row['captcha'] as $key => $val) {
if ('default' == $key) {
$baseConfig[$key] = array_merge($baseConfig[$key], $val);
} else {
$baseConfig[$key]['is_on'] = $val['is_on'];
$baseConfig[$key]['config'] = array_merge($baseConfig['default'], $val['config']);
}
}
}
Config::set(['captcha' => $baseConfig]);
}
}
}
步骤 3:注册事件监听器
php
<?php
// app/event.php
return [
'listen' => [
// 路由加载完成(相当于 module_init)
'RouteLoaded' => [
\app\listener\ModuleInitListener::class,
],
],
];
示例 2:ActionBeginBehavior 迁移
TP5 原代码特点
- 在操作开始时执行
- 包含安全验证、插件缓存清理、多语言权限检查等逻辑
- 根据 POST/GET 请求执行不同逻辑
TP8 实现方式
步骤 1:创建事件类
php
<?php
// app/event/ActionBegin.php
declare(strict_types=1);
namespace app\event;
/**
* 操作开始事件
*/
class ActionBegin
{
public string $actionName;
public string $controllerName;
public string $moduleName;
public string $method;
public function __construct()
{
$request = request();
$this->actionName = $request->action();
$this->controllerName = $request->controller();
$this->moduleName = $request->app();
$this->method = $request->method();
}
}
步骤 2:创建监听器类
php
<?php
// app/listener/ActionBeginListener.php
declare(strict_types=1);
namespace app\listener;
use app\event\ActionBegin;
use think\facade\Db;
/**
* 操作开始监听器
*/
class ActionBeginListener
{
/**
* 事件监听处理
*/
public function handle(ActionBegin $event): void
{
$this->initialize($event);
}
private function initialize(ActionBegin $event): void
{
// 安全验证
$this->securityVerify($event);
if ('POST' == $event->method) {
$this->clearWeapp($event);
} else {
$this->useWeapp($event);
}
// 多语言权限检查
$this->languageAccess($event);
}
/**
* 安全验证
*/
private function securityVerify(ActionBegin $event): void
{
$ctlAct = $event->controllerName . '@' . $event->actionName;
$ctlActArr = ['Arctype@ajax_newtpl', 'Archives@ajax_newtpl'];
if (in_array($event->controllerName, ['Filemanager', 'Weapp'])
|| in_array($ctlAct, $ctlActArr)) {
$security = tpSetting('security');
// 强制开启密保问题认证
if (in_array($event->controllerName, ['Filemanager'])
|| in_array($ctlAct, $ctlActArr)) {
if (empty($security['security_ask_open'])) {
throw new \think\exception\HttpException(403, '需要开启密保问题设置');
}
}
$nosubmit = input('param.nosubmit/d');
if ('POST' == $event->method && empty($nosubmit)) {
if (!empty($security['security_ask_open'])) {
$adminId = session('admin_id', 0);
$adminInfo = Db::name('admin')
->field('admin_id,last_ip')
->where(['admin_id' => $adminId])
->find();
$securityAnswerverifyIp = $security['security_answerverify_ip'] ?? '-1';
// 同IP不验证
if ($adminInfo['last_ip'] != $securityAnswerverifyIp) {
throw new \think\exception\HttpException(403, '出于安全考虑,请勿非法越过密保答案验证');
}
}
}
}
}
/**
* 清除插件缓存
*/
private function clearWeapp(ActionBegin $event): void
{
$ctlActStr = $event->controllerName . '@*';
if ('Weapp@*' === $ctlActStr) {
\think\facade\Cache::delete('hooks');
}
}
/**
* 插件相关处理
*/
private function useWeapp(ActionBegin $event): void
{
if (!request()->isAjax()) {
if ('Weapp@index' == $event->controllerName . '@' . $event->actionName) {
$weappIndexGourl = cookie('admin-weapp_index_gourl');
if (!empty($weappIndexGourl)) {
redirect($weappIndexGourl)->send();
}
} else if ('Weapp@execute' != $event->controllerName . '@' . $event->actionName) {
cookie('admin-weapp_index_gourl', null);
}
}
}
/**
* 多语言功能操作权限
*/
private function languageAccess(ActionBegin $event): void
{
$controllerArr = ['Weapp', 'Filemanager', 'Sitemap', 'Member', 'Seo', 'Channeltype', 'Tools'];
$ctlActArr = ['Admin@index', 'Admin@add', 'Admin@del', 'System@water', 'System@thumb', 'System@api_conf'];
if (in_array($event->controllerName, $controllerArr)
|| in_array($event->controllerName . '@' . $event->actionName, $ctlActArr)) {
$mainLang = get_main_lang();
$adminLang = get_admin_lang();
if (is_language() && $mainLang != $adminLang) {
$langTitle = Db::name('language')
->where('mark', $mainLang)
->value('title');
throw new \think\exception\HttpException(403, '当前语言没有此功能,请切换到【' . $langTitle . '】语言');
}
}
}
}
步骤 3:注册到事件配置
php
<?php
// app/event.php
return [
'listen' => [
'HttpRun' => [
\app\listener\ActionBeginListener::class,
],
],
];
示例 3:使用事件订阅器(推荐方式)
如果你有多个相关的事件需要处理,可以使用事件订阅器,这样更加优雅和集中管理。
创建事件订阅器:
php
<?php
// app/subscribe/AdminSubscribe.php
declare(strict_types=1);
namespace app\subscribe;
/**
* 后台管理事件订阅器
* 集中管理后台相关的所有事件
*/
class AdminSubscribe
{
/**
* 订阅的事件
* 返回事件名称和对应的处理方法
*/
public function subscribe($event): array
{
return [
'RouteLoaded' => 'onModuleInit',
'HttpRun' => 'onActionBegin',
'HttpEnd' => 'onAppEnd',
];
}
/**
* 模块初始化
*/
public function onModuleInit($event): void
{
$request = request();
$actionName = $request->action();
$controllerName = $request->controller();
// 验证码配置
if (in_array($controllerName . '@' . $actionName, ['Admin@login', 'Admin@vertify'])) {
$this->configCaptcha();
}
}
/**
* 操作开始
*/
public function onActionBegin($event): void
{
$request = request();
// 权限验证
$this->checkPermission();
// 记录访问日志
if ($request->isPost()) {
$this->logAccess();
}
}
/**
* 应用结束
*/
public function onAppEnd($event): void
{
// 清理临时数据
$this->cleanTempData();
}
/**
* 配置验证码
*/
private function configCaptcha(): void
{
// 验证码配置逻辑
}
/**
* 检查权限
*/
private function checkPermission(): void
{
// 权限检查逻辑
}
/**
* 记录访问日志
*/
private function logAccess(): void
{
// 日志记录逻辑
}
/**
* 清理临时数据
*/
private function cleanTempData(): void
{
// 清理逻辑
}
}
注册订阅器:
php
<?php
// app/event.php
return [
'subscribe' => [
\app\subscribe\AdminSubscribe::class,
],
];
优势:
- ✅ 集中管理相关事件
- ✅ 代码更加清晰和易于维护
- ✅ 减少配置文件的复杂度
高级用法
1. 动态加载插件事件(类似 weapp 行为)
你的 TP5 项目中有动态加载插件行为的逻辑,在 TP8 中可以通过服务提供者实现。
创建应用服务提供者:
php
<?php
// app/provider.php
declare(strict_types=1);
namespace app;
use think\Service;
use think\facade\Event;
use think\facade\Db;
/**
* 应用服务提供者
*/
class AppService extends Service
{
/**
* 注册服务
*/
public function register(): void
{
// 注册服务
}
/**
* 启动服务
*/
public function boot(): void
{
// 动态加载插件事件监听器
$this->loadWeappListeners();
}
/**
* 动态加载插件事件监听器
*/
protected function loadWeappListeners(): void
{
// 获取已启用的插件
$weappList = Db::name('weapp')
->field('code')
->where('status', 1)
->cache('weapp_list', 3600)
->select()
->toArray();
foreach ($weappList as $weapp) {
$code = $weapp['code'];
// 检查并注册各种事件监听器
$this->registerWeappListener($code, 'AppInit', 'AppInitListener');
$this->registerWeappListener($code, 'RouteLoaded', 'ModuleInitListener');
$this->registerWeappListener($code, 'HttpRun', 'ActionBeginListener');
$this->registerWeappListener($code, 'HttpEnd', 'AppEndListener');
$this->registerWeappListener($code, 'LogWrite', 'LogWriteListener');
}
}
/**
* 注册插件事件监听器
*/
protected function registerWeappListener(string $code, string $event, string $listener): void
{
$listenerClass = "\weapp\{$code}\listener\{$listener}";
if (class_exists($listenerClass)) {
Event::listen($event, $listenerClass);
}
}
}
注册服务提供者:
php
<?php
// config/app.php
return [
// 服务提供者
'service' => [
\app\AppService::class,
],
];
2. 插件事件监听器示例
假设你有一个名为 payment 的插件,在 ThinkPHP 8.x 中的目录结构:
weapp/
└── payment/
├── listener/
│ ├── AppInitListener.php
│ ├── ModuleInitListener.php
│ └── ActionBeginListener.php
└── event.php
插件的模块初始化监听器:
php
<?php
// weapp/payment/listener/ModuleInitListener.php
declare(strict_types=1);
namespace weapp\payment\listener;
/**
* 支付插件 - 模块初始化监听器
*/
class ModuleInitListener
{
/**
* 事件监听处理
*/
public function handle($event): void
{
// 初始化支付配置
$this->initPaymentConfig();
}
/**
* 初始化支付配置
*/
private function initPaymentConfig(): void
{
// 从数据库加载支付配置
$config = \think\facade\Db::name('weapp_payment_config')
->where('status', 1)
->find();
if ($config) {
// 设置到配置中
config(['payment' => $config]);
}
}
}
插件的操作开始监听器:
php
<?php
// weapp/payment/listener/ActionBeginListener.php
declare(strict_types=1);
namespace weapp\payment\listener;
/**
* 支付插件 - 操作开始监听器
*/
class ActionBeginListener
{
/**
* 事件监听处理
*/
public function handle($event): void
{
$request = request();
$controller = $request->controller();
$action = $request->action();
// 支付回调处理
if ($controller === 'Payment' && $action === 'notify') {
$this->handlePaymentNotify();
}
}
/**
* 处理支付回调
*/
private function handlePaymentNotify(): void
{
$paymentType = input('type', '');
// 记录日志
trace('Payment notify: ' . $paymentType, 'info');
// 处理支付回调逻辑
}
}
3. 使用中间件替代部分行为扩展(推荐)
ThinkPHP 8.x 推荐使用中间件来处理请求前后的逻辑,这比事件更加灵活和直观。
创建后台权限验证中间件:
php
<?php
// app/middleware/AdminAuth.php
declare(strict_types=1);
namespace app\middleware;
use Closure;
use think\Request;
use think\Response;
/**
* 后台权限验证中间件(替代 AuthRoleBehavior)
*/
class AdminAuth
{
/**
* 处理请求
*/
public function handle(Request $request, Closure $next): Response
{
// 前置操作:权限验证
$this->checkAuth($request);
// 执行控制器
$response = $next($request);
// 后置操作:记录日志等
$this->afterAction($request, $response);
return $response;
}
/**
* 权限验证
*/
private function checkAuth(Request $request): void
{
$controller = $request->controller();
$action = $request->action();
// 白名单控制器(不需要验证)
$whiteList = ['Login', 'Captcha'];
if (in_array($controller, $whiteList)) {
return;
}
// 检查是否登录
$adminId = session('admin_id');
if (empty($adminId)) {
throw new \think\exception\HttpResponseException(
redirect((string) url('admin/login/index'))
);
}
// 检查权限
$this->checkPermission($controller, $action, $adminId);
}
/**
* 检查权限
*/
private function checkPermission(string $controller, string $action, int $adminId): void
{
// 超级管理员跳过权限检查
$admin = \think\facade\Db::name('admin')->find($adminId);
if ($admin['role_id'] == 1) {
return;
}
// 权限验证逻辑
$rule = $controller . '/' . $action;
$hasPermission = \think\facade\Db::name('admin_access')
->alias('a')
->join('admin_rule r', 'a.rule_id = r.id')
->where('a.admin_id', $adminId)
->where('r.name', $rule)
->where('r.status', 1)
->count();
if (!$hasPermission) {
throw new \think\exception\HttpException(403, '没有操作权限');
}
}
/**
* 后置操作
*/
private function afterAction(Request $request, Response $response): void
{
// 记录操作日志
if ($request->isPost()) {
$this->logAction($request);
}
}
/**
* 记录操作日志
*/
private function logAction(Request $request): void
{
$data = [
'admin_id' => session('admin_id'),
'controller' => $request->controller(),
'action' => $request->action(),
'method' => $request->method(),
'ip' => $request->ip(),
'create_time' => time(),
];
\think\facade\Db::name('admin_log')->insert($data);
}
}
注册中间件的三种方式:
方式 1:全局中间件
php
<?php
// app/middleware.php
return [
// 全局中间件
\app\middleware\AdminAuth::class,
];
方式 2:路由中间件
php
<?php
// route/admin.php
use think\facade\Route;
// 后台路由组,应用中间件
Route::group('admin', function () {
Route::get('index', 'admin/Index/index');
Route::get('user/list', 'admin/User/list');
// ... 更多路由
})->middleware(\app\middleware\AdminAuth::class);
方式 3:控制器中间件
php
<?php
namespace app\admin\controller;
use think\Request;
class Base
{
protected $middleware = [
\app\middleware\AdminAuth::class,
];
}
4. 视图过滤(ViewFilter)的实现
ThinkPHP 5.x 的 view_filter 在 TP8 中可以通过事件或中间件实现。
方式 1:使用事件监听器
php
<?php
// app/listener/ViewFilterListener.php
declare(strict_types=1);
namespace app\listener;
/**
* 视图过滤监听器
*/
class ViewFilterListener
{
/**
* 处理 HttpEnd 事件
*/
public function handle($event): void
{
$response = response();
// 只处理 HTML 响应
if ($response->getHeader('Content-Type') === 'text/html') {
$content = $response->getContent();
// 过滤和替换内容
$content = $this->filterContent($content);
// 设置新内容
$response->content($content);
}
}
/**
* 过滤内容
*/
private function filterContent(string $content): string
{
// 替换模板变量
$content = $this->replaceTemplateVars($content);
// 压缩 HTML
if (config('app.html_compress', false)) {
$content = $this->compressHtml($content);
}
// 添加统计代码
$content = $this->addStatisticsCode($content);
return $content;
}
/**
* 替换模板变量
*/
private function replaceTemplateVars(string $content): string
{
$replaces = [
'__STATIC__' => '/static',
'__PUBLIC__' => '/public',
'__UPLOAD__' => '/uploads',
];
return str_replace(array_keys($replaces), array_values($replaces), $content);
}
/**
* 压缩 HTML
*/
private function compressHtml(string $content): string
{
// 移除注释
$content = preg_replace('/<!--(?!\[if\s).*?-->/s', '', $content);
// 移除多余空白
$content = preg_replace('/\s+/', ' ', $content);
return trim($content);
}
/**
* 添加统计代码
*/
private function addStatisticsCode(string $content): string
{
$statisticsCode = config('app.statistics_code', '');
if ($statisticsCode && strpos($content, '</body>') !== false) {
$content = str_replace('</body>', $statisticsCode . '</body>', $content);
}
return $content;
}
}
注册事件:
php
<?php
// app/event.php
return [
'listen' => [
'HttpEnd' => [
\app\listener\ViewFilterListener::class,
],
],
];
方式 2:使用中间件(推荐)
php
<?php
// app/middleware/ViewFilter.php
declare(strict_types=1);
namespace app\middleware;
use Closure;
use think\Request;
use think\Response;
/**
* 视图过滤中间件
*/
class ViewFilter
{
/**
* 处理请求
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// 只处理 HTML 响应
$contentType = $response->getHeader('Content-Type');
if (strpos($contentType, 'text/html') !== false) {
$content = $response->getContent();
$content = $this->filterContent($content);
$response->content($content);
}
return $response;
}
/**
* 过滤内容
*/
private function filterContent(string $content): string
{
// 替换静态资源路径
$content = str_replace(
['__STATIC__', '__PUBLIC__', '__UPLOAD__'],
['/static', '/public', '/uploads'],
$content
);
return $content;
}
}
最佳实践
1. 选择合适的实现方式
根据不同的场景选择最合适的实现方式:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 请求前后处理 | 中间件 | 更直观,易于理解和调试 |
| 系统级钩子 | 事件监听器 | 符合系统架构,解耦性好 |
| 多个相关事件 | 事件订阅器 | 集中管理,代码更清晰 |
| 插件扩展 | 服务提供者 + 事件 | 动态加载,灵活性高 |
2. 事件命名规范
自定义事件命名建议:
php
// 使用动词 + 名词的形式
UserLogin // 用户登录
UserLogout // 用户登出
OrderCreated // 订单创建
OrderPaid // 订单支付
ArticlePublished // 文章发布
事件类放置位置:
app/
└── event/
├── UserLogin.php
├── UserLogout.php
└── OrderCreated.php
3. 监听器命名规范
监听器命名建议:
php
// 事件名 + Listener 后缀
UserLoginListener
OrderCreatedListener
ArticlePublishedListener
监听器放置位置:
app/
└── listener/
├── UserLoginListener.php
├── OrderCreatedListener.php
└── ArticlePublishedListener.php
4. 性能优化建议
缓存事件监听器列表:
php
<?php
// config/cache.php
return [
'default' => 'file',
'stores' => [
'file' => [
'type' => 'File',
'path' => runtime_path() . 'cache/',
'expire' => 0,
],
],
];
延迟加载监听器:
php
<?php
// app/event.php
return [
'listen' => [
'UserLogin' => [
// 使用闭包延迟加载
function($event) {
return app(\app\listener\UserLoginListener::class)->handle($event);
},
],
],
];
5. 错误处理
在监听器中处理异常:
php
<?php
namespace app\listener;
class UserLoginListener
{
public function handle($event): void
{
try {
// 业务逻辑
$this->logUserLogin($event);
} catch (\Exception $e) {
// 记录错误日志
trace('UserLoginListener error: ' . $e->getMessage(), 'error');
// 不要让异常影响主流程
// 除非这是关键逻辑
}
}
}
6. 事件触发示例
在代码中触发自定义事件:
php
<?php
namespace app\controller;
use think\facade\Event;
use app\event\UserLogin;
class User
{
public function login()
{
// 登录逻辑
$user = $this->doLogin();
// 触发用户登录事件
Event::trigger(new UserLogin($user));
// 或者使用 event() 助手函数
event('UserLogin', $user);
return json(['code' => 0, 'msg' => '登录成功']);
}
}
7. 依赖注入
在监听器中使用依赖注入:
php
<?php
namespace app\listener;
use app\service\LogService;
use app\service\NotifyService;
class UserLoginListener
{
protected $logService;
protected $notifyService;
// 构造函数注入
public function __construct(LogService $logService, NotifyService $notifyService)
{
$this->logService = $logService;
$this->notifyService = $notifyService;
}
public function handle($event): void
{
// 使用注入的服务
$this->logService->log('用户登录', $event->user);
$this->notifyService->notify('登录通知', $event->user);
}
}
8. 迁移检查清单
从 ThinkPHP 5.x 迁移到 8.x 时,请检查以下内容:
- 将所有
tags.php配置迁移到event.php - 将 Behavior 类的
run()方法改为 Listener 类的handle()方法 - 将
request()->module()改为request()->app() - 将
Config::get()改为Config::get()或使用 Facade - 将
Config::set()的参数格式从字符串改为数组 - 检查所有
Hook::listen()调用,改为Event::trigger() - 考虑将
app_begin相关逻辑迁移到中间件 - 更新命名空间声明,添加
declare(strict_types=1); - 添加类型提示(参数类型和返回值类型)
- 测试所有迁移后的功能
常见问题 FAQ
Q1: 为什么 ThinkPHP 8.x 移除了行为扩展?
A: 行为扩展是 ThinkPHP 特有的机制,不符合现代 PHP 开发标准。事件系统基于 PSR-14 标准,更加通用和灵活,易于测试和维护。
Q2: 事件和中间件有什么区别?
A:
- 事件:用于系统内部的解耦通信,一个事件可以有多个监听器,适合发布-订阅模式
- 中间件:用于请求处理流程,按顺序执行,适合请求前后的处理逻辑
Q3: 如何在 TP8 中实现 TP5 的 app_begin 钩子?
A: app_begin 已被移除,推荐使用中间件替代。中间件可以在请求处理前执行逻辑。
Q4: 事件监听器的执行顺序如何控制?
A: 在 event.php 中,监听器按照配置的顺序执行:
php
'listen' => [
'UserLogin' => [
\app\listener\LogListener::class, // 第一个执行
\app\listener\NotifyListener::class, // 第二个执行
],
],
Q5: 如何停止事件传播?
A: 在监听器中返回 false 可以停止后续监听器的执行:
php
public function handle($event)
{
// 业务逻辑
// 返回 false 停止传播
return false;
}
Q6: 如何调试事件监听器?
A: 可以使用以下方法调试:
php
// 1. 在监听器中使用 trace() 记录日志
public function handle($event): void
{
trace('UserLoginListener executed', 'info');
trace($event, 'info');
}
// 2. 查看已注册的事件监听器
dump(Event::listenersOf('UserLogin'));
// 3. 使用断点调试工具(如 Xdebug)
Q7: 插件的事件监听器如何自动加载?
A: 使用服务提供者在应用启动时动态注册插件的监听器(参考"高级用法"章节的示例)。
完整示例:从 TP5 到 TP8 的完整迁移
TP5 项目结构
application/
├── admin/
│ ├── behavior/
│ │ ├── ModuleInitBehavior.php
│ │ ├── ActionBeginBehavior.php
│ │ └── ViewFilterBehavior.php
│ └── tags.php
└── tags.php
TP8 项目结构
app/
├── event/
│ ├── ModuleInit.php
│ └── ActionBegin.php
├── listener/
│ ├── ModuleInitListener.php
│ ├── ActionBeginListener.php
│ └── ViewFilterListener.php
├── middleware/
│ ├── AdminAuth.php
│ └── ViewFilter.php
├── subscribe/
│ └── AdminSubscribe.php
├── event.php
└── middleware.php
完整的配置文件示例
TP8 事件配置文件:
php
<?php
// app/event.php
return [
'bind' => [],
'listen' => [
'AppInit' => [],
'RouteLoaded' => [
\app\listener\ModuleInitListener::class,
],
'HttpRun' => [
\app\listener\ActionBeginListener::class,
],
'HttpEnd' => [
\app\listener\ViewFilterListener::class,
],
'LogWrite' => [],
],
'subscribe' => [
\app\subscribe\AdminSubscribe::class,
],
];
TP8 中间件配置文件:
php
<?php
// app/middleware.php
return [
\app\middleware\AdminAuth::class,
\app\middleware\ViewFilter::class,
];
总结
核心要点
- ThinkPHP 8.x 移除了行为扩展机制,改用更现代的事件系统
- 事件系统基于 PSR-14 标准,更加灵活和标准化
- 推荐使用中间件处理请求前后的逻辑,更加直观
- 事件订阅器可以集中管理多个相关事件
- 服务提供者可以实现插件的动态加载
迁移策略
| TP5 机制 | TP8 推荐方案 | 优先级 |
|---|---|---|
app_begin |
中间件 | 高 |
module_init |
RouteLoaded 事件 | 中 |
action_begin |
HttpRun 事件或中间件 | 高 |
view_filter |
HttpEnd 事件或中间件 | 中 |
app_end |
HttpEnd 事件 | 低 |
| 插件行为 | 服务提供者 + 事件 | 高 |
优势对比
ThinkPHP 8.x 事件系统的优势:
✅ 符合 PSR-14 标准,更加通用
✅ 事件和监听器解耦,易于测试
✅ 支持依赖注入
✅ 更好的 IDE 支持和类型提示
✅ 可以动态注册和注销监听器
✅ 支持事件传播控制
参考资源
官方文档
学习建议
- 先理解概念:了解事件、监听器、订阅器的区别和使用场景
- 从简单开始:先迁移简单的行为扩展,逐步掌握
- 多用中间件:对于请求处理相关的逻辑,优先考虑中间件
- 保持代码整洁:合理组织事件和监听器的目录结构
- 充分测试:迁移后要充分测试所有功能
快速参考
创建事件:
bash
php think make:event UserLogin
创建监听器:
bash
php think make:listener UserLoginListener
创建中间件:
bash
php think make:middleware AdminAuth
附录:TP5 与 TP8 代码对比速查表
1. 基础语法对比
| 功能 | ThinkPHP 5.x | ThinkPHP 8.x |
|---|---|---|
| 获取模块名 | request()->module() |
request()->app() |
| 设置配置 | Config::set('key', $value) |
Config::set(['key' => $value]) |
| 获取配置 | Config::get('key') |
Config::get('key') |
| 触发钩子 | Hook::listen('tag', $params) |
Event::trigger('EventName', $params) |
| 添加行为 | Hook::add('tag', 'Behavior') |
Event::listen('EventName', 'Listener') |
2. 文件位置对比
| 类型 | ThinkPHP 5.x | ThinkPHP 8.x |
|---|---|---|
| 行为类 | application/behavior/ |
已移除 |
| 事件类 | 不存在 | app/event/ |
| 监听器 | 不存在 | app/listener/ |
| 订阅器 | 不存在 | app/subscribe/ |
| 配置文件 | application/tags.php |
app/event.php |
| 中间件 | application/http/middleware/ |
app/middleware/ |
3. 类结构对比
TP5 行为类:
php
class MyBehavior
{
public function run(&$params)
{
// 逻辑
}
}
TP8 监听器类:
php
class MyListener
{
public function handle($event): void
{
// 逻辑
}
}
结语
本文档详细介绍了从 ThinkPHP 5.x 行为扩展到 ThinkPHP 8.x 事件系统的迁移方法。通过学习本文档,你应该能够:
✅ 理解 TP5 和 TP8 的核心差异
✅ 掌握事件系统的基本概念
✅ 能够将现有的行为扩展迁移到事件系统
✅ 选择合适的实现方式(事件 vs 中间件)
✅ 实现插件的动态加载机制
如有疑问,请参考官方文档或在社区寻求帮助。
文档版本: v1.0
最后更新: 2025-12-29
适用版本: ThinkPHP 5.0.24 → ThinkPHP 8.1.3
祝你学习愉快! 🎉