导读:手里维护着一个庞大的 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第二步:迁移目录结构与路由文件
- 搬迁业务模块:将原 TP5
application/下的模块目录(如index、admin、api、common)复制到 TP6 的app/目录下。 - 搬迁公共函数:将
application/common.php移动到app/common.php。 - 清理单应用目录(多应用模式必做):如果安装了
think-multi-app使用多应用模式,请务必删除app/根目录下默认生成的controller、model等单应用文件夹!否则 TP6 会误判当前为单应用模式,导致多应用路由失效。确保app/下只有各子应用目录(如app/index、app/admin)及全局基础文件(BaseController.php、common.php、Request.php等)。 迁移路由配置:
- 单应用模式下,路由依然放在根目录的
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,替换为以下完整兼容代码。它一次性恢复了:
- 视图方法:
$this->assign()、$this->fetch()、$this->display()以及$this->view->xxx属性调用。 - 跳转与响应方法:
$this->success()、$this->error()、$this->result()、$this->redirect()(均支持不写return直接中断)。 - 前置操作属性:
$beforeActionList。 - 初始化方法兼容:同时兼容 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('提交成功') 来渲染跳转过渡页:
- 将原 TP5 项目
thinkphp/tpl/dispatch_jump.tpl复制到 TP6 的app/common/view/dispatch_jump.tpl。 - 在 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 行为绑定需要按职责拆分为两部分迁移:
请求生命周期拦截类(如原
app_init、module_init、action_begin中的权限校验、跨域 Header 设置、Token 验证):- 迁移去向:中间件(Middleware)。
- 使用命令
php think make:middleware CheckAuth创建中间件,然后在全局app/middleware.php或应用目录下的middleware.php中注册。
业务逻辑解耦类(如原代码中手动调用
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 时代!如果你在迁移过程中遇到其他奇葩报错,欢迎在评论区留言交流。
版权属于:soarli
本文链接:https://blog.soarli.top/archives/1189.html
转载时须注明出处及本声明。