soarli

从 ThinkPHP 5 到 ThinkPHP 6:老项目无缝升级与平滑迁移终极实战指南
导读:手里维护着一个庞大的 ThinkPHP 5.0 / 5.1 老项目,随着服务器 PHP 版本升级到 PHP ...
扫描右侧二维码阅读全文
23
2026/10

从 ThinkPHP 5 到 ThinkPHP 6:老项目无缝升级与平滑迁移终极实战指南

AI 文章摘要
提炼中... · 本文基于 qwen2.5:14b
✨ 正在提炼文章核心脉络…

导读:手里维护着一个庞大的 ThinkPHP 5.0 / 5.1 老项目,随着服务器 PHP 版本升级到 PHP 7.4 甚至 PHP 8,加上新业务对组件规范化的要求越来越高,升级到 ThinkPHP 6.0 成了必经之路。但是,面对成百上千个控制器里的 $this->success()、$this->assign() 以及满屏的 Db::name()->setInc(),难道要一行行重写吗?

本文将从架构差异对比、标准五步迁移法、BaseController 终极兼容改造(省去 80% 控制器修改工作量)以及数据库与模型语法排坑四个维度,手把手带你完成 TP5 到 TP6 的平滑升级。


前言:为什么不能直接 composer update?

很多同学尝试直接修改 composer.json 中的版本号来升级,结果往往是整个项目瞬间瘫痪。

原因在于:ThinkPHP 6.0 是一次底层的全面重构。它全面遵循了 PSR 标准(PSR-2、PSR-3、PSR-4、PSR-7、PSR-11、PSR-15、PSR-16),强化了容器(Container)与门面(Facade)机制,并且为了保持核心精简,将多应用模式、模板引擎、验证码等非核心功能全部剥离成了独立的扩展包。

因此,官方唯一推荐的升级姿势是:全新安装一个 TP6 项目骨架,然后将原 TP5 项目的业务代码按模块逐步搬迁并改造。


一、 全局视野:TP5 与 TP6 核心差异速查表

在动手搬代码之前,我们先通过一张全景对比表,摸清这次升级要面对的所有“敌人”:

核心模块ThinkPHP 5 (5.0 / 5.1)ThinkPHP 6.0升级改造要点
PHP 版本要求PHP 5.6 / 7.0 及以上PHP 7.2.5 及以上(支持 PHP 8)需先升级服务器与本地 PHP 环境
应用目录结构application/app/业务目录变更为 app/,命名空间仍为 app\
多模块/多应用原生默认支持多模块默认单应用模式多模块项目必须额外安装 think-multi-app 扩展
模板引擎内置 think-view默认不内置视图组件需额外安装 topthink/think-view
Session 机制默认开启,直接调用默认关闭需在全局中间件 middleware.php 中手动开启
基础控制器继承 think\Controller继承 app\BaseController移除了内置的 success、error、fetch、assign
查询返回结果5.0 默认返回数组(可配置)统一返回 Collection 数据集对象若强依赖纯数组,需链式调用 toArray()
模型静态查询支持 Model::get() / all()彻底废弃 get() 和 all()统一改为 Model::find() 和 Model::select()
字段自增/自减setInc() / setDec()废弃 setInc / setDec / setField改为 inc() / dec() 配合 update()
钩子与行为Hook + Behavior废弃行为与钩子彻底改用 事件(Event) 与 中间件(Middleware)

二、 标准五步法:搭建骨架与基础环境迁移

第一步:创建全新 TP6 骨架并安装必备扩展

在全新目录中初始化 TP6 项目,并把 TP6 从核心中剥离出去的常用组件装回来:

# 1. 创建 TP6.0 基础骨架
composer create-project topthink/think tp6-project
cd tp6-project

# 2. 【必装-原多模块项目】安装多应用模式扩展(对应 TP5 的多模块目录结构)
composer require topthink/think-multi-app

# 3. 【按需】如果老项目使用了原生模板渲染(fetch/assign)
composer require topthink/think-view

# 4. 【按需】如果老项目使用了官方验证码或消息队列
composer require topthink/think-captcha
composer require topthink/think-queue

第二步:迁移目录结构与路由文件

  1. 搬迁业务模块:将原 TP5 application/ 下的模块目录(如 index、admin、api、common)复制到 TP6 的 app/ 目录下。
  2. 搬迁公共函数:将 application/common.php 移动到 app/common.php。
  3. 清理单应用目录(多应用模式必做):如果安装了 think-multi-app 使用多应用模式,请务必删除 app/ 根目录下默认生成的 controller、model 等单应用文件夹!否则 TP6 会误判当前为单应用模式,导致多应用路由失效。确保 app/ 下只有各子应用目录(如 app/index、app/admin)及全局基础文件(BaseController.php、common.php、Request.php 等)。
  4. 迁移路由配置:

    • 单应用模式下,路由依然放在根目录的 route/app.php。
    • 多应用模式下,根目录的 route/ 不再生效,必须将路由文件移至各个子应用内部,例如:app/index/route/app.php、app/admin/route/app.php。

第三步:迁移并改造配置文件

TP6 的配置文件统一放在根目录的 config/ 下,但内部结构有所调整。切忌直接用 TP5 的配置文件覆盖 TP6,正确做法是打开 TP6 的原生配置文件,将老项目的参数逐项填进去。

  • .env 环境变量:读取方式由 Env::get('database.hostname') 统一推荐为助手函数 env('database.hostname', '127.0.0.1')。
  • config/database.php 数据库配置:TP6 原生支持多连接池,原数据库账号密码需填入 connections 数组下的 mysql 节点中:
// TP6 config/database.php 结构示例
return [
    // 默认使用的数据库连接配置
    'default'     => env('database.driver', 'mysql'),

    // 数据库连接配置信息
    'connections' => [
        'mysql' => [
            'type'     => env('database.type', 'mysql'),
            'hostname' => env('database.hostname', '127.0.0.1'),
            'database' => env('database.database', ''),
            'username' => env('database.username', 'root'),
            'password' => env('database.password', ''),
            'hostport' => env('database.hostport', '3306'),
            // 其余配置填入此处...
        ],
    ],
];

第四步:开启 Session 与全局中间件

为了提升纯 API 项目的性能,TP6 默认是关闭 Session 的。如果老项目用到了 Session 登录态或官方验证码(底层依赖 Session),必须打开 app/middleware.php,取消 SessionInit 的注释:

// app/middleware.php
return [
    // 全局请求缓存
    // \think\middleware\CheckRequestCache::class,
    // 多语言加载
    // \think\middleware\LoadLangPack::class,
    // Session初始化(使用Session或验证码必须开启此项!)
    \think\middleware\SessionInit::class,
];

第五步:全局替换门面(Facade)命名空间

如果你是从 ThinkPHP 5.0 升级上来,代码中一定存在大量类似 Db::name()、Session::get()、Cache::set() 的静态调用。

在 TP6 中,think\ 根命名空间下的类都是容器实例类,任何静态调用都必须通过 think\facade\ 门面命名空间进行。打开你的 IDE(如 PhpStorm 或 VS Code),直接使用全局搜索替换:

// ❌ TP5.0 写法(在 TP6 中静态调用会直接报 Non-static method 错误)
use think\Db;
use think\Cache;
use think\Session;
use think\Cookie;
use think\Log;
use think\Request;

// ✅ TP6 正确写法(统一加上 facade)
use think\facade\Db;
use think\facade\Cache;
use think\facade\Session;
use think\facade\Cookie;
use think\facade\Log;
use think\facade\View;
use think\facade\Request;

三、 偷懒神技:改造 BaseController 一步兼容 80% 控制器老代码

在升级过程中,最让人崩溃的往往是控制器(Controller)。TP6 移除了 think\Controller,改用项目自带的 app\BaseController,并且大刀阔斧地砍掉了 $this->assign()、$this->fetch()、$this->success()、$this->error()、$this->result()、$this->redirect() 以及 $beforeActionList(前置方法)。

难道我们要去几百个控制器里把 $this->success() 全部改成 return json() 吗?更要命的是,很多老代码里写 $this->error('参数错误'); 时根本没写 return,指望着它直接中断执行!

核心破局原理:HttpResponseException

在 TP6 中,我们绝不能在控制器里用 exit 或 die 来中断程序,因为 TP6 的后置中间件(包括 Session 写入磁盘、日志保存)是在控制器返回 Response 对象之后才执行的。一旦 exit,Session 就会丢失。

那么如何在不写 return 的情况下安全中断控制器执行?答案是:在 BaseController 封装的方法底部抛出 think\exception\HttpResponseException 异常。TP6 内核捕获到这个特定异常后,会直接跳过控制器后续代码,立刻把响应体交由中间件走完标准生命周期并输出给客户端!

完整改造版 app/BaseController.php

确保已执行 composer require topthink/think-view 后,打开 app/BaseController.php,替换为以下完整兼容代码。它一次性恢复了:

  1. 视图方法:$this->assign()、$this->fetch()、$this->display() 以及 $this->view->xxx 属性调用。
  2. 跳转与响应方法:$this->success()、$this->error()、$this->result()、$this->redirect()(均支持不写 return 直接中断)。
  3. 前置操作属性:$beforeActionList。
  4. 初始化方法兼容:同时兼容 TP5.0 的 _initialize() 和 TP5.1/TP6 的 initialize()。
<?php
declare (strict_types = 1);

namespace app;

use think\App;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\facade\View;
use think\Response;
use think\Validate;

/**
 * 控制器基础类(完美兼容 TP5 核心写法)
 */
abstract class BaseController
{
    /**
     * Request实例
     * @var \think\Request
     */
    protected $request;

    /**
     * 应用实例
     * @var \think\App
     */
    protected $app;

    /**
     * 视图实例(兼容 TP5 中 $this->view->assign() 的写法)
     * @var \think\View
     */
    protected $view;

    /**
     * 是否批量验证
     * @var bool
     */
    protected $batchValidate = false;

    /**
     * 控制器中间件
     * @var array
     */
    protected $middleware = [];

    /**
     * 前置操作方法列表(兼容 TP5 的 $beforeActionList)
     * @var array
     */
    protected $beforeActionList = [];

    /**
     * 构造方法
     * @access public
     * @param  App $app 应用对象
     */
    public function __construct(App $app)
    {
        $this->app     = $app;
        $this->request = $this->app->request;
        $this->view    = $this->app->view;

        // 控制器初始化
        $this->initialize();

        // 兼容 TP5 前置操作方法
        if (!empty($this->beforeActionList)) {
            foreach ($this->beforeActionList as $method => $options) {
                is_numeric($method) ?
                    $this->beforeAction($options) :
                    $this->beforeAction($method, $options);
            }
        }
    }

    /**
     * 初始化方法(自动兼容 TP5.0 的 _initialize)
     */
    protected function initialize()
    {
        if (method_exists($this, '_initialize')) {
            $this->_initialize();
        }
    }

    // =========================================================================
    // 1. 兼容 TP5 视图方法 ($this->assign / $this->fetch / $this->display)
    // =========================================================================

    /**
     * 模板变量赋值
     * @param string|array $name  模板变量
     * @param mixed        $value 变量值
     * @return \think\View
     */
    protected function assign($name, $value = null)
    {
        return View::assign($name, $value);
    }

    /**
     * 解析和获取模板内容
     * @param string $template 模板文件名或者内容
     * @param array  $vars     模板变量
     * @return string
     */
    protected function fetch(string $template = '', array $vars = [])
    {
        return View::fetch($template, $vars);
    }

    /**
     * 渲染内容输出
     * @param string $content 内容
     * @param array  $vars    模板变量
     * @return string
     */
    protected function display(string $content = '', array $vars = [])
    {
        return View::display($content, $vars);
    }

    // =========================================================================
    // 2. 兼容 TP5 跳转与响应方法 ($this->success / error / result / redirect)
    // =========================================================================

    /**
     * 操作成功跳转的快捷方法
     * @param mixed  $msg    提示信息
     * @param string $url    跳转的URL地址
     * @param mixed  $data   返回的数据
     * @param int    $wait   跳转等待时间
     * @param array  $header 发送的Header信息
     * @return void
     */
    protected function success($msg = '', ?string $url = null, $data = '', int $wait = 3, array $header = [])
    {
        if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) {
            $url = $_SERVER["HTTP_REFERER"];
        } elseif ($url) {
            $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : (string)$this->app->route->buildUrl($url);
        }

        $result = [
            'code' => 1,
            'msg'  => $msg,
            'data' => $data,
            'url'  => $url,
            'wait' => $wait,
        ];

        $type = $this->getResponseType();
        if ($type === 'html') {
            $template = $this->app->config->get('app.dispatch_success_tmpl');
            $response = Response::create($template, 'view')->assign($result)->header($header);
        } else {
            $response = Response::create($result, $type)->header($header);
        }

        throw new HttpResponseException($response);
    }

    /**
     * 操作错误跳转的快捷方法
     * @param mixed  $msg    提示信息
     * @param string $url    跳转的URL地址
     * @param mixed  $data   返回的数据
     * @param int    $wait   跳转等待时间
     * @param array  $header 发送的Header信息
     * @return void
     */
    protected function error($msg = '', ?string $url = null, $data = '', int $wait = 3, array $header = [])
    {
        if (is_null($url)) {
            $url = $this->request->isAjax() ? '' : 'javascript:history.back(-1);';
        } elseif ($url) {
            $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : (string)$this->app->route->buildUrl($url);
        }

        $result = [
            'code' => 0,
            'msg'  => $msg,
            'data' => $data,
            'url'  => $url,
            'wait' => $wait,
        ];

        $type = $this->getResponseType();
        if ($type === 'html') {
            $template = $this->app->config->get('app.dispatch_error_tmpl');
            $response = Response::create($template, 'view')->assign($result)->header($header);
        } else {
            $response = Response::create($result, $type)->header($header);
        }

        throw new HttpResponseException($response);
    }

    /**
     * 返回封装后的 API 数据到客户端
     * @param mixed  $data   要返回的数据
     * @param int    $code   返回的 code
     * @param mixed  $msg    提示信息
     * @param string $type   返回数据格式
     * @param array  $header 发送的 Header 信息
     * @return void
     */
    protected function result($data, int $code = 0, $msg = '', string $type = '', array $header = [])
    {
        $result = [
            'code' => $code,
            'msg'  => $msg,
            'time' => time(),
            'data' => $data,
        ];

        $type     = $type ?: $this->getResponseType();
        $response = Response::create($result, $type)->header($header);

        throw new HttpResponseException($response);
    }

    /**
     * URL 重定向(兼容不写 return 的直接调用)
     * @param string $url  跳转的 URL 表达式
     * @param int    $code http code
     * @return void
     */
    protected function redirect(string $url, int $code = 302)
    {
        $response = Response::create($url, 'redirect', $code);
        throw new HttpResponseException($response);
    }

    /**
     * 获取当前的 response 输出类型
     * @return string
     */
    protected function getResponseType(): string
    {
        return ($this->request->isJson() || $this->request->isAjax()) ? 'json' : 'html';
    }

    // =========================================================================
    // 3. 兼容 TP5 前置操作与验证器
    // =========================================================================

    /**
     * 前置操作方法处理
     * @param string $method  前置操作方法名
     * @param array  $options 调用参数 ['only'=>[...]] 或者 ['except'=>[...]]
     * @return void
     */
    protected function beforeAction(string $method, array $options = [])
    {
        if (isset($options['only'])) {
            if (is_string($options['only'])) {
                $options['only'] = explode(',', $options['only']);
            }
            if (!in_array($this->request->action(), $options['only'])) {
                return;
            }
        } elseif (isset($options['except'])) {
            if (is_string($options['except'])) {
                $options['except'] = explode(',', $options['except']);
            }
            if (in_array($this->request->action(), $options['except'])) {
                return;
            }
        }

        call_user_func([$this, $method]);
    }

    /**
     * 验证数据
     * @param array        $data     数据
     * @param string|array $validate 验证器名或者验证规则数组
     * @param array        $message  提示信息
     * @param bool         $batch    是否批量验证
     * @return array|string|true
     * @throws ValidateException
     */
    protected function validate(array $data, $validate, array $message = [], bool $batch = false)
    {
        if (is_array($validate)) {
            $v = new Validate();
            $v->rule($validate);
        } else {
            if (strpos($validate, '.')) {
                [$validate, $scene] = explode('.', $validate);
            }
            $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
            $v     = new $class();
            if (!empty($scene)) {
                $v->scene($scene);
            }
        }

        $v->message($message);

        if ($batch || $this->batchValidate) {
            $v->batch(true);
        }

        return $v->failException(true)->check($data);
    }
}

配套配置:恢复 HTML 跳转提示页(笑脸/哭脸倒计时页)

如果老项目在非 Ajax 请求下使用了 $this->success('提交成功') 来渲染跳转过渡页:

  1. 将原 TP5 项目 thinkphp/tpl/dispatch_jump.tpl 复制到 TP6 的 app/common/view/dispatch_jump.tpl。
  2. 在 TP6 的 config/app.php 中加上模板路径配置:
// config/app.php
return [
    // ... 原有其他配置 ...

    // 默认跳转页面对应的模板文件
    'dispatch_success_tmpl' => app()->getRootPath() . 'app/common/view/dispatch_jump.tpl',
    'dispatch_error_tmpl'   => app()->getRootPath() . 'app/common/view/dispatch_jump.tpl',
];

⚠️ 必看避坑点:小心 try...catch 吞掉跳转异常!

因为 $this->success() 和 $this->error() 底层是通过抛出 HttpResponseException(继承自 \RuntimeException 和 \Exception)来中断执行的。如果你的业务代码里写了宽泛的 try { ... } catch (\Exception $e) { ... },并在 try 内部调用了 $this->error(),它就会被你的 catch 块误捕!

解决办法:在捕获普通 \Exception 之前,先把 HttpResponseException 原样抛出去:

try {
    if ($checkFail) {
        $this->error('校验未通过'); // 底层抛出 HttpResponseException
    }
    // 其他数据库操作...
} catch (\think\exception\HttpResponseException $e) {
    throw $e; // 关键:必须直接向上抛出,交由框架内核处理响应!
} catch (\Exception $e) {
    // 处理真正的业务异常或数据库异常
    Log::error($e->getMessage());
}

四、 持久战:数据库(Db)、模型(Model)与架构语法改造

搞定了控制器基类后,接下来我们需要清理散落在 Model 和 Service 层中的废弃数据库语法。

1. 数据库(Db)链式查询的四大变更

(1)废弃 setInc / setDec,改用 inc / dec + update

在 TP6 中,inc() 和 dec() 只负责构建 SQL 表达式,必须链式调用 update() 才会真正执行 SQL!

// ❌ TP5 写法:
Db::name('user')->where('id', 1)->setInc('score', 5);
Db::name('user')->where('id', 1)->setDec('score', 2);

// ✅ TP6 写法(切记末尾必须加 update()!):
Db::name('user')->where('id', 1)->inc('score', 5)->update();
Db::name('user')->where('id', 1)->dec('score', 2)->update();

(2)废弃 setField,统一使用 update

// ❌ TP5 写法:
Db::name('user')->where('id', 1)->setField('status', 1);

// ✅ TP6 写法:
Db::name('user')->where('id', 1)->update(['status' => 1]);

(3)废弃 exp 表达式查询,改用 whereRaw 或 Db::raw

// ❌ TP5 写法:
Db::name('user')->where('score', 'exp', '> 100')->select();

// ✅ TP6 写法:
Db::name('user')->whereRaw('score > 100')->select();
// 或者字段对比:
Db::name('user')->where('score', '>', Db::raw('level * 10'))->select();

(4)查询结果统一返回 Collection 对象

在 TP5.0 中,Db::name()->select() 默认返回原生二维数组 array。而在 TP6 中,无论是 Db 还是 Model,select() 永远返回 think\Collection 数据集对象。

  • 好消息:Collection 实现了 ArrayAccess 和 Iterator 接口,你依然可以直接用 foreach ($list as $item) 遍历,或者用 $list[0]['name'] 访问。
  • 坏消息:你不能对它使用 is_array($list) 判断,也不能直接把它传给 array_column()、array_merge()、array_filter() 等只接收原生 array 的 PHP 内置函数。如果用到了这些函数,请在查询末尾加上 ->toArray(),或者直接使用 Collection 自带的同名方法(如 $list->column('name'))。

2. 模型(Model)语法的两大变更

(1)彻底废弃 get() 与 all()

TP6 将模型的查询语法与 Db 彻底统一,不再保留两套名字:

// ❌ TP5 写法:
$user = User::get(1);
$list = User::all([1, 2, 3]);

// ✅ TP6 写法:
$user = User::find(1);
$list = User::select([1, 2, 3]);

(2)模型事件(钩子)写法重构

TP5 中通常在模型的 init() 方法里通过 self::event('before_insert', ...) 注册钩子;TP6 改为直接在模型类中定义固定命名的静态方法,框架会自动识别并触发:

namespace app\model;
use think\Model;

class User extends Model
{
    // TP6 支持的模型事件静态方法名:
    // onBeforeInsert, onAfterInsert, onBeforeUpdate, onAfterUpdate
    // onBeforeWrite, onAfterWrite, onBeforeDelete, onAfterDelete
    public static function onBeforeInsert($user)
    {
        if (empty($user->create_time)) {
            $user->create_time = time();
        }
    }
}

3. 行为(Behavior)与钩子(Hook)的去向

TP6 彻底删除了 think\Hook 和 Behavior。原项目中的 tags.php 行为绑定需要按职责拆分为两部分迁移:

  1. 请求生命周期拦截类(如原 app_init、module_init、action_begin 中的权限校验、跨域 Header 设置、Token 验证):

    • 迁移去向:中间件(Middleware)。
    • 使用命令 php think make:middleware CheckAuth 创建中间件,然后在全局 app/middleware.php 或应用目录下的 middleware.php 中注册。
  2. 业务逻辑解耦类(如原代码中手动调用 Hook::listen('user_login', $user) 触发登录积分奖励、发送通知):

    • 迁移去向:事件系统(Event)。
    • 将触发代码替换为 event('UserLogin', $user),并在 app/event.php 的 listen 数组中绑定对应的事件监听类。

五、 上线前必看的全局排查 Checklist

当所有代码搬迁完毕后,建议按照以下 Checklist 进行最后的全局扫描与联调:

  • [ ] 全局替换控制器父类:将所有控制器的 extends Controller(以及 use think\Controller;)替换为 extends BaseController(use app\BaseController;)。
  • [ ] 全局正则扫描废弃方法:

    • 搜索 ->setInc( 和 ->setDec(,改造为 ->inc()->update()。
    • 搜索 ->setField(,改造为 ->update([...])。
    • 搜索 ::get( 和 ::all(,改造为 ::find( 和 ::select(。
  • [ ] 全局扫描 exit 与 die:排查控制器和公共函数中的 exit / die(),统一替换为 return json(...) 或抛出 HttpResponseException,防止 Session 失效。
  • [ ] 检查伪静态配置:确认 Nginx / Apache 的入口目录依然指向 public/,且 Rewrite 规则正常生效。
  • [ ] 开启调试模式逐模块回归测试:在根目录 .env 中设置 APP_DEBUG = true,打开浏览器与接口调试工具,重点测试登录态保持、分页列表、表单增删改以及异常报错拦截。

结语

从 ThinkPHP 5 升级到 ThinkPHP 6,看似改动点繁多,但只要抓住了“全新骨架 + 多应用扩展 + BaseController 兼容层 + 全局替换废弃 Db/Model 语法”这条主线,整个迁移过程其实非常清晰可控。

希望这篇实战指南能帮你少走弯路,顺利将老项目带入更现代、更规范的 ThinkPHP 6 时代!如果你在迁移过程中遇到其他奇葩报错,欢迎在评论区留言交流。

最后修改:2026 年 11 月 01 日 03 : 21 AM

👍 相关推荐

2026 年 10 月 31 日
ESXi 存储深度优化与排障全指南:从快照底层原理、系统盘瘦身、UNMAP 自动回收到文件死锁急救
2026 年 10 月 31 日
Linux 运维实战指南:从 SCP 跨平台备份、Ubuntu 权限管理到 SSH 隧道进阶
2026 年 10 月 31 日
从 weixin:// 报错说起:企业级私有化应用唤醒协议(Deep Link)架构与跨端落地全指南
✂️

💬 评论

0 / 1000