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


祝你学习愉快! 🎉

相关推荐
JaguarJack1 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo1 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack2 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理3 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082853 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe3 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5
longxiangam3 天前
Composer 私有仓库搭建
php·composer
上海云盾-高防顾问3 天前
DNS异常怎么办?快速排查+解决指南
开发语言·php
ShoreKiten3 天前
关于解决本地部署sqli-labs无法安装低版本php环境问题
开发语言·php
liliangcsdn3 天前
深入探索TD3算法的推理过程
开发语言·php