ThinkPHP 5.x 到 8.x 行为扩展迁移指南

ThinkPHP 5.x 到 8.x 行为扩展迁移指南

目录

  1. 核心变化概述
  2. 系统事件对照表
  3. 基础概念
  4. 迁移示例
  5. 高级用法
  6. 最佳实践

核心变化概述

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_filterapp_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,
];

总结

核心要点

  1. ThinkPHP 8.x 移除了行为扩展机制,改用更现代的事件系统
  2. 事件系统基于 PSR-14 标准,更加灵活和标准化
  3. 推荐使用中间件处理请求前后的逻辑,更加直观
  4. 事件订阅器可以集中管理多个相关事件
  5. 服务提供者可以实现插件的动态加载

迁移策略

TP5 机制 TP8 推荐方案 优先级
app_begin 中间件
module_init RouteLoaded 事件
action_begin HttpRun 事件或中间件
view_filter HttpEnd 事件或中间件
app_end HttpEnd 事件
插件行为 服务提供者 + 事件

优势对比

ThinkPHP 8.x 事件系统的优势:

✅ 符合 PSR-14 标准,更加通用

✅ 事件和监听器解耦,易于测试

✅ 支持依赖注入

✅ 更好的 IDE 支持和类型提示

✅ 可以动态注册和注销监听器

✅ 支持事件传播控制

参考资源

官方文档

学习建议

  1. 先理解概念:了解事件、监听器、订阅器的区别和使用场景
  2. 从简单开始:先迁移简单的行为扩展,逐步掌握
  3. 多用中间件:对于请求处理相关的逻辑,优先考虑中间件
  4. 保持代码整洁:合理组织事件和监听器的目录结构
  5. 充分测试:迁移后要充分测试所有功能

快速参考

创建事件:

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


祝你学习愉快! 🎉

相关推荐
小虎哥-技术博客6 小时前
ThinkPHP 5 到 ThinkPHP 8 路由迁移完整指南
php
yangSnowy9 小时前
PHP的运行模式
开发语言·php
YJlio12 小时前
网络与通信具总览(14.0):从 PsPing 到 TCPView / Whois 的联合作战
开发语言·网络·php
iCheney!12 小时前
php生成赛博像素风头像
开发语言·php
小虎哥-技术博客14 小时前
ThinkPHP 5.0.24 到 ThinkPHP 8.x 迁移学习指南
php
m0_7381207215 小时前
渗透测试——靶机DC-6详细横向过程(Wordpress渗透)
服务器·网络·python·web安全·ssh·php
傻啦嘿哟16 小时前
实战:用GraphQL接口高效采集数据
开发语言·驱动开发·php
BingoGo16 小时前
CatchAdmin 2025 年终总结 模块化架构的进化之路
后端·开源·php
qq_1171790716 小时前
海康威视球机萤石云不在线问题解决方案
开发语言·智能路由器·php