Two finished posts written from the numerology-report migration work, kept as draft: true so they build no pages and appear in no listing until published. Content bank for weeks when there is nothing fresh to write. - migrating-codeigniter-iis-to-openlitespeed (engineering) IIS -> OpenLiteSpeed/CyberPanel. The two fatals that only appeared once the authentik SSO gate was gone, the docroot public/ separation, and the loopback self-call that becomes a real outbound HTTPS request on LiteSpeed. - upgrading-codeigniter-46-to-47 (notes) The two fatal config properties NOT in the official upgrade guide (permittedURIChars, jsonEncodeDepth), why Composer never merges app/Config, and the property-diff script that finds the whole class of problem at once. Both fill the starved engineering (1 post) and notes categories. project-state.md records them as Step B2b with the publish checklist.
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
---
|
||||
title: "把 CodeIgniter 4 应用从 IIS 迁移到 OpenLiteSpeed"
|
||||
description: "我把一个 17,000 行的 CodeIgniter 4 应用从 Windows IIS 搬到了 CyberPanel 上的 OpenLiteSpeed。迁移后冒出两个致命错误——而 IIS 一直靠 SSO 把它们藏着。"
|
||||
pubDate: 2026-09-20
|
||||
category: engineering
|
||||
tags: [codeigniter, php, openspeedway, cyberpanel, iis, migration, litespeed, linux]
|
||||
ogImage: /og/migrating-codeigniter-iis-to-openlitespeed.png
|
||||
banner: /banners/migrating-codeigniter-iis-to-openlitespeed.png
|
||||
draft: true
|
||||
---
|
||||
|
||||
我维护着一个数字命理报告生成器:一个 CodeIgniter 4 应用,接收出生日期和姓名,跑一遍命理引擎,渲染出 19 页 A4 报告。它在 Windows Server + IIS 上跑了很多年。2026 年 9 月,我把它迁到 CyberPanel 上的 OpenLiteSpeed,跑在 Linux 上,换了个域名。
|
||||
|
||||
迁移本身没什么好说的——拷文件、把 docroot 指向 `public/`、装依赖。真正值得写下来的是:**新主机上第一个未认证请求,就在两条路由上直接致命崩溃——而这两条路由在 IIS 上"一直好好的"。**
|
||||
|
||||
两个 bug 都是真的。两个从一开始就存在于 IIS 上。两个都看不见,因为 IIS 在出问题的路由前面恰好挡了一层 authentik SSO。
|
||||
|
||||
## 为什么这件事重要
|
||||
|
||||
如果你的自托管应用前面挂了一层认证网关,那这层网关做的事不止是保护数据——它同时**在帮你藏 bug**。任何"只对未认证用户出错"的东西永远不会以未认证身份运行,所以永远不会报错。而它会在最糟的时刻暴露:你迁移的时候、你拆掉网关的时候、你把路由暴露到公网的时候。
|
||||
|
||||
我这个应用,就"健康"了整整几年。我把新域名指过去的那一刻,两条路由返 HTTP 500。代码一行没改。唯一变的是网关没了。
|
||||
|
||||
两个修复加起来大概二十分钟。找到它们花了两个小时——因为真正的错误信息被 CodeIgniter 的 CLI 错误渲染器挡在了后面。
|
||||
|
||||
## 环境
|
||||
|
||||
这是一个相当普通的 CI4 项目,但有一个不太常见的架构细节:报告引擎不是库,而是**同一台主机上的 HTTP 端点**。控制器拼一个请求,`curl` 回自己域名的 `/api/single`,引擎返回算好的 JSON 数值,控制器再把它渲染成可打印的报告。
|
||||
|
||||
正是这个"自调用"设计,让 IIS 上必须有 `CONST_IIS_INTERNAL_BASE`。这点我后面会回来说——它才是真正有意思的部分。
|
||||
|
||||
| | 迁移前 | 迁移后 |
|
||||
|---|---|---|
|
||||
| 操作系统 | Windows Server | Ubuntu (CyberPanel) |
|
||||
| Web 服务器 | IIS | OpenLiteSpeed 1.9.0 |
|
||||
| PHP | 8.4 (Windows 版) | 8.4.25 (lsphp84) |
|
||||
| Docroot | `C:\inetpub\calc.hoelee.com` | `/home/<域名>/public_html/public` |
|
||||
| 认证网关 | authentik SSO 挡 `/lifecode`、`/api/*` | 无 |
|
||||
|
||||
## 第一步:docroot 指向 `public/`,不是项目根目录
|
||||
|
||||
CI4 是两段式目录结构。`app/`、`vendor/`、`writable/` 和 `.env` 都在项目根目录,只有 `public/` 本该对 Web 可见。
|
||||
|
||||
CyberPanel 建的站点根目录叫 `public_html`。我的第一反应是把整个项目解压进 `public_html`,docroot 不动。
|
||||
|
||||
**那样会把 `app/`、`vendor/`、`.env` 文件和 `writable/` 的 session 数据全部暴露在 HTTP 之下。** 这个项目的 `.env` 里有一个 webhook 凭证。任何人猜到 `/../.env`——或者干脆直接请求 `/.env`,因为这文件就躺在被服务的目录下面——就拿到了。
|
||||
|
||||
正确做法是解压进 `public_html`,然后把 vhost 的 docroot 往下一层,改成 `public_html/public`:
|
||||
|
||||
```bash
|
||||
# 解压成 app/、vendor/、.env 位于 public_html 之下
|
||||
cd /home/<域名>/public_html
|
||||
tar -xzf /tmp/deploy.tar.gz
|
||||
|
||||
# docroot 必须是 public_html/public,不是 public_html
|
||||
sudo sed -i 's#/home/<域名>/public_html\$#/home/<域名>/public_html/public#' \
|
||||
/usr/local/lsws/conf/vhosts/<域名>/vhost.conf
|
||||
|
||||
sudo /usr/local/lsws/bin/lshttpd -t
|
||||
sudo systemctl restart lsws
|
||||
```
|
||||
|
||||
这是整个迁移里最重要的一步,也是大多数教程跳过的一步。其他什么都不做,这一步也得做。
|
||||
|
||||
## 第二步:IIS 藏起来的两个致命错误
|
||||
|
||||
### 致命错误一 —— `env()` 调用得太早
|
||||
|
||||
第一个 500 完全没有响应体。这不太寻常——CI4 正常会渲染点东西出来。一个空白的 500 通常意味着 PHP 在框架的错误处理器装上之前就死了。
|
||||
|
||||
CI4 的启动流程会在 `Boot::bootWeb()` 里很早加载 `app/Config/Constants.php`(通过 `Boot::loadConstants()`)。而这件事发生在 **`Common.php` 助手文件加载之前**——`env()` 就定义在那个文件里。
|
||||
|
||||
所以这一行:
|
||||
|
||||
```php
|
||||
// app/Config/Constants.php —— 错误写法
|
||||
define('CONST_fullBase', env('hoelee.fullBase'));
|
||||
```
|
||||
|
||||
会失败于:
|
||||
|
||||
```
|
||||
Fatal error: Uncaught Error: Call to undefined function env()
|
||||
in app/Config/Constants.php:96
|
||||
```
|
||||
|
||||
规则是绝对的:**`Constants.php` 里只能放普通常量。** 不能有 `env()`、不能有 `getenv()`、不能有配置助手。如果你真需要依赖环境的值,就定义到启动流程更靠后的位置——或者把这个常量降级成 fallback,真正的值后面再读。
|
||||
|
||||
我做的正是后者。`CONST_fullBase` 变回普通字符串,消费它的助手函数改为**先**查框架自己的 `app.baseURL`(那个才是 `.env` 驱动的):
|
||||
|
||||
```php
|
||||
// app/Helpers/hoelee_helper.php
|
||||
function getFullBase(bool $selfCall = false): string
|
||||
{
|
||||
// .env 驱动的 baseURL 优先;CONST_fullBase 只是 fallback
|
||||
if ($selfCall && defined('CONST_IIS_INTERNAL_BASE') && CONST_IIS_INTERNAL_BASE) {
|
||||
return rtrim(CONST_IIS_INTERNAL_BASE, '/');
|
||||
}
|
||||
$appBase = config('App')->baseURL;
|
||||
if ($appBase) return rtrim($appBase, '/');
|
||||
if (defined('CONST_fullBase') && CONST_fullBase) return rtrim(CONST_fullBase, '/');
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
**这个是我自己搞出来的。** 同一周做凭证清理重构时引入的——我把一个硬编码 URL 挪进 `.env`,然后在 `Constants.php` 里用 `env()` 读它。在我机器上是通的,因为本地 `.env` 走的是另一条路径被读到。干净启动下它从来没通过。我是在**部署测试**后一小时内抓到的;如果只是代码审查,很可能就这么放过去了。
|
||||
|
||||
### 致命错误二 —— CI4 控制器里的 `parent::__construct()`
|
||||
|
||||
第二个失败在 `/lifecode`。这个是应用里**原本就存在**的 bug,不是我引入的。
|
||||
|
||||
绕过 CLI 渲染器之后,真正的报错是:
|
||||
|
||||
```
|
||||
Error: Cannot call constructor
|
||||
```
|
||||
|
||||
CI4 的基础类 `CodeIgniter\Controller` **根本没有构造函数**。它实现的是 `initController()`,由框架把 request、response、logger 三个对象传进去。标准写法是:
|
||||
|
||||
```php
|
||||
// CI4 的正确写法
|
||||
public function initController(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
parent::initController($request, $response, $logger);
|
||||
// 你的初始化代码
|
||||
}
|
||||
```
|
||||
|
||||
但 `Lifecode.php` 和 `ApiEn.php` 用的是传统构造函数,还在调 `parent::__construct()`:
|
||||
|
||||
```php
|
||||
// 错误 —— CodeIgniter\Controller 没有 __construct()
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
对一个没有定义 `__construct()` 的父类调 `parent::__construct()`,在 PHP 8 里是致命错误。把两个控制器改成 `initController()` 就修好了——同时要为新签名补上三个 interface 的 `use` 语句。
|
||||
|
||||
我没有盲信报错信息,而是先确认父类**确实**没有构造函数:
|
||||
|
||||
```bash
|
||||
grep -n 'function __construct\|function initController' \
|
||||
vendor/codeigniter4/framework/system/Controller.php
|
||||
```
|
||||
|
||||
只出来 `initController()`。这一步值得做——"Cannot call constructor" 在父类**有**构造函数但内部报错时也会出现,重写签名之前你得先搞清楚自己是哪种情况。
|
||||
|
||||
## 为什么这两个 bug 在 IIS 上都不出现
|
||||
|
||||
这是让我改变部署观念的部分。
|
||||
|
||||
在 IIS 主机上,`/lifecode` 和 `/api/*` 挡在 authentik SSO 后面。对这两条路由的未认证请求返回 **HTTP 302 跳转到 `auth.hoelee.com`**——压根不会碰到控制器。那个坏掉的构造函数从来没被执行过。这条路由大概从写出来那天就是坏的,而它从来没被要求真正处理过一次请求。
|
||||
|
||||
我直接对比了两台主机来确认:
|
||||
|
||||
```bash
|
||||
curl -sI https://calc.hoelee.com/lifecode | head -1
|
||||
# HTTP/2 302 <- authentik 跳转;控制器根本没运行
|
||||
|
||||
curl -sI http://<新主机>/lifecode | head -1
|
||||
# HTTP/1.1 500 <- 没有网关;控制器运行并致命崩溃
|
||||
```
|
||||
|
||||
那个 302 就是这个 bug 活下来的原因。应用看起来健康,只是因为它不健康的部分根本触不到。
|
||||
|
||||
**教训说白了就是:一条路由前面挂了认证网关,就意味着这条路由自己的代码没有任何有效的测试覆盖。** 如果你要迁移或者拆网关,请专门留出时间,用未认证身份把每一条曾被网关挡着的路由都打一遍,再宣布迁移完成。小应用上这只是十行 `curl` 循环。在我这里,它本来几秒钟就能找出这两个 bug,而不是两个小时:
|
||||
|
||||
```bash
|
||||
for p in / /read/single /read/partner /lifecode /api/date /api/single; do
|
||||
printf '%-16s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://<主机>$p)"
|
||||
done
|
||||
```
|
||||
|
||||
## 第三步:那个不再是 loopback 的自调用
|
||||
|
||||
现在是这个架构里真正有意思的后果。
|
||||
|
||||
报告引擎是靠控制器 `curl` 自己的主机来调用的。在 IIS 上,`CONST_IIS_INTERNAL_BASE` 把这个调用指向 `http://localhost:7296`——loopback 接口,请求不出机器,也不碰 DNS 和 TLS。
|
||||
|
||||
在 LiteSpeed 上这个常量被**故意不定义**,于是 `getFullBase()` 落到 `app.baseURL`。这意味着每一次生成报告,现在都是**一个真实的对外 HTTPS 请求,打到应用自己的公网 URL,再从 Cloudflare 绕回来**。
|
||||
|
||||
那问题就来了:这到底能不能跑?
|
||||
|
||||
答案是能,但值得专门测一次,因为它的失败模式很容易误导人。这是确切的测法——**在服务器上跑**,不是在你的笔记本上:
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'HTTPS self-call: %{http_code}\n' \
|
||||
-X POST 'https://<域名>/api/single' -d 'nameCn=test&dob=1990-01-01'
|
||||
```
|
||||
|
||||
两个要注意的地方:
|
||||
|
||||
- **在服务器上跑。** 我从 Windows 机器上打同一个 URL,拿到的是 404 和 500——那是我这台机器 DNS 解析和 Cloudflare 机器人防护的产物,不是服务器的问题。在错误的主机上测试,会得出很有把握的错误结论。
|
||||
- **看 JSON,不只看状态码。** 一个 200 配一个被截断的 body,比干脆失败更糟。我在信任之前先确认了响应能被解析。
|
||||
|
||||
这里藏着一个真实的取舍,我还没完全解决:走公网 URL 的自调用意味着生成报告依赖 Cloudflare 在线、每次报告多一次 TLS 握手、并且在整个过程中占用两个 PHP worker。在 LiteSpeed 上 worker 池不大的情况下,并发报告一多就可能死锁——每个请求都在等另一个请求,而后者拿不到空闲 worker。以目前的流量没问题。但在规模上去之前,这个自调用应该换成绕过 CDN 的内部路径。
|
||||
|
||||
## 第四步:验证真实产出,不是状态码
|
||||
|
||||
`/` 返 200 只证明落地页能渲染。它不证明报告引擎能用,而引擎才是整个产品。所以最后一步是端到端生成一份真报告:
|
||||
|
||||
```bash
|
||||
curl -s -X POST 'https://<域名>/read/single' \
|
||||
-d 'nameCn=test&dob=1990-01-01&gender=m' \
|
||||
-o report.html -w 'HTTP:%{http_code} bytes:%{size_download}\n'
|
||||
```
|
||||
|
||||
结果:**HTTP 200、99,189 字节、19 页 A4。** 我又用中文字符输入跑了一遍,把引擎的 UTF-8 路径也走通——HTTP 200、99,205 字节、19 页、姓名渲染正常、输出里零 PHP 警告。
|
||||
|
||||
真正有意义的检查,是新旧主机之间页数可比。它证明字体、DPI 和分页逻辑都完整地熬过了这次搬迁。
|
||||
|
||||
关于那个 UTF-8 测试的插曲:我第一次跑的时候返回了 500,差点就一头扎进去查 bug。原因是我自己的 shell——中文字符在进 `curl` 的路上被终端编码搞坏了,应用收到的是非法字节。把同一个请求写成服务器上的脚本再跑,编码在我控制之内,就返回了 200。**在你调试编码故障之前,先确认服务器真正收到的字节就是你打算发出去的字节。**
|
||||
|
||||
## 如果重来一次,我会怎么改
|
||||
|
||||
**迁移前就测未认证路由,而不是迁移后。** 那两个小时的排查,用一个遍历路由列表的 `curl` 循环就能完全避免。我现在把"列出所有路由、以未认证身份逐个请求、记录状态码"当成任何迁移的第零步。
|
||||
|
||||
**不要在 `Constants.php` 里调 `env()`。** 我在文件里留了注释专门写这件事,因为下一个人——多半是六个月后的我自己——一定会想这么干。
|
||||
|
||||
**迁移前先查框架版本对应的 project-space 配置。** 同一周我把 CI4 从 4.6.3 升到 4.7.4,它在两个升级指南里根本没提的属性上致命崩溃了两次:`Config\App::$permittedURIChars`(4.7 的 Router 要求)和 `Config\Format::$jsonEncodeDepth`(JSONFormatter 要求,而第二个正好打断了引擎的 JSON 自调用)。Composer 会更新 `vendor/`,但**永远**不会合并 `app/Config/*.php`,因为那些属于 project-space。我最后往 14 个配置文件里手工合并了新属性。那是另一篇文章,但迁移的教训是同一个形状:**框架会告诉你 `vendor/` 里改了什么;没有任何东西会告诉你 `app/` 里改了什么。**
|
||||
|
||||
**先确认 docroot,再动别的。** 如果我解压进 `public_html` 就收手,应用是能跑的——同时静静地把 `.env` 通过 HTTP 服务出去。能跑的迁移,不等于安全的迁移。
|
||||
|
||||
## 结果
|
||||
|
||||
一个晚上。六条路由全部验证 200,中英文输入的报告产出在字节层面可比,零 PHP 警告,另外还从代码库里清掉了两个长期潜伏的 bug——而这两个 bug 一直被 IIS 的认证网关遮着。
|
||||
|
||||
应用现在跑在 OpenLiteSpeed 上,docroot 正确分离,主机可自动化管理,凭证放在 `.env` 而不是写死在常量里。
|
||||
|
||||
---
|
||||
|
||||
**想让你的应用也做这套迁移?** 我可以在 IIS、Apache、nginx 和 LiteSpeed 之间迁移 PHP 应用——包括那些麻烦的部分:自调用架构、藏 bug 的 SSO 网关,以及会碰到 project-space 配置的框架升级。
|
||||
|
||||
[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=CodeIgniter%20迁移) · [hoelee.com](https://hoelee.com)
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: "CodeIgniter 4.6 升级到 4.7:官方指南没说到的破坏性变更"
|
||||
description: "CodeIgniter 4.7 官方升级指南列了八项破坏性变更。真正让我应用致命崩溃的那两项不在里面——而且它们属于同一类问题。"
|
||||
pubDate: 2026-09-20
|
||||
category: notes
|
||||
tags: [codeigniter, php, upgrade, composer, breaking-changes, config, framework]
|
||||
ogImage: /og/upgrading-codeigniter-46-to-47.png
|
||||
banner: /banners/upgrading-codeigniter-46-to-47.png
|
||||
draft: true
|
||||
---
|
||||
|
||||
我把一个 CodeIgniter 4 应用从 4.6.3 升到了 4.7.4。我读了升级指南,把每一项文档化的破坏性变更都对着代码库核了一遍,确认没有一项影响到我。然后我跑了一下应用,它致命崩溃了两次。
|
||||
|
||||
两个致命错误属于同一类问题,而**升级指南里完全没提这类问题**。这是一篇短文,讲真正会坏的是什么,以及为什么指南没法提前警告你。
|
||||
|
||||
## 为什么这件事重要
|
||||
|
||||
Composer 有一条容易忘掉的作用域边界。`composer update` 只写 `vendor/`——也就是**系统作用域**,框架自己的文件,它可以随意替换。
|
||||
|
||||
而你的 `app/Config/*.php` 是**项目作用域**。那是你的文件。Composer 永远不会碰它们,升级指南对它们的措辞是"你可能想要合并这些变更",而不是"这会导致崩溃"。
|
||||
|
||||
但当 4.7 的框架代码去读一个属性,而你 4.6 时代的配置类里根本没定义它时,你得到的是致命错误——不是废弃警告,不是 notice。指南没法穷举这件事,因为这个属性的集合是「新框架读什么」与「你的配置文件里恰好有什么」的交集,而它对后半边完全不可见。
|
||||
|
||||
如果你要升级一个落后不止一个 minor 版本的 CI4 应用,请为此留出预算。我花了一个小时左右,改了 14 个配置文件。
|
||||
|
||||
## 我先核了什么
|
||||
|
||||
4.7.0 文档化的破坏性变更,以及每一项是否适用:
|
||||
|
||||
| 文档化的变更 | 是否影响本应用 |
|
||||
|---|---|
|
||||
| `Model::insertBatch()` / `updateBatch()` 返回值变化 | 否——没用 Model |
|
||||
| Entity 类型转换变更 | 否——没用 Entity |
|
||||
| 验证规则变更(`regex_match`、`differs`) | 否 |
|
||||
| 加密处理器默认值 | 否 |
|
||||
| 上传文件/图片验证行为 | 否——没有上传 |
|
||||
| `IncomingRequest` 内部变更 | 否 |
|
||||
| 移除的 `Session` 类属性 | 否——见下文 |
|
||||
| `PageCache` 构造函数签名 | 否——见下文 |
|
||||
|
||||
其中两项需要细看,结果两个都是**假警报**,值得讲一下,因为你如果只 grep,它们看起来和真命中一模一样:
|
||||
|
||||
**"移除的 `Session` 类属性"** —— 在配置里 grep `session`,立刻一片飘红。但那些属性(`sessionDriver`、`sessionCookieName` 之类)是我 `Config\App` **自己的属性**,并没有被移除。指南指的是 `Session` 类上的属性。同一个词,不同的类。
|
||||
|
||||
**"`PageCache` 构造函数签名变更"** —— 我确实引用了 `pagecache`,但只是把它作为 `Config\Filters` 里的一个 filter 别名。这并不等于继承或实例化这个类,而只有后者才会坏。**引用不等于子类。**
|
||||
|
||||
这两次核对教给我同一件事:**grep 找到的是「提及」,不是「使用」。** 要确认命中的那个类是指南说的那个类,以及你确实继承或调用了那个发生变化的东西。
|
||||
|
||||
## 指南里没有的两个致命错误
|
||||
|
||||
两个都在干净启动时冒出来。两个都是 4.7 框架代码要读、而 4.6 时代的配置文件没有定义的属性。
|
||||
|
||||
### 1. `Config\App::$permittedURIChars`
|
||||
|
||||
```
|
||||
Undefined property: Config\App::$permittedURIChars
|
||||
```
|
||||
|
||||
4.7 的 Router 要求这个属性,用它校验进来的 URI 字符。4.6 时代的 `App.php` 里没有它,于是 Router 在路由任何东西之前就抛异常了。
|
||||
|
||||
```php
|
||||
// app/Config/App.php —— 追加到类里
|
||||
/**
|
||||
* CI4 4.7 compatibility: allowed characters in a URI.
|
||||
* Required by the 4.7 Router.
|
||||
*/
|
||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
|
||||
```
|
||||
|
||||
### 2. `Config\Format::$jsonEncodeDepth`
|
||||
|
||||
```
|
||||
Undefined property: Config\Format::$jsonEncodeDepth
|
||||
```
|
||||
|
||||
4.7 的 `JSONFormatter` 要求它。这一个直接打断了应用的**报告引擎**——因为引擎的 JSON 响应是用 `json_decode()` 解的,失败信号在很下游才爆出来,表现为一个让人一头雾水的解析错误,而不是真正的"属性未定义"。
|
||||
|
||||
```php
|
||||
// app/Config/Format.php —— 追加到类里
|
||||
/**
|
||||
* CI4 4.7 compatibility: json_encode() depth limit.
|
||||
*/
|
||||
public int $jsonEncodeDepth = 512;
|
||||
```
|
||||
|
||||
第二个值得多说一句。表面上看到的错误是:
|
||||
|
||||
```
|
||||
Failed to parse JSON string. Malformed UTF-8 characters
|
||||
```
|
||||
|
||||
指向编码问题。真实原因是缺一个配置属性。**当框架的错误信息描述的是症状而不是病因时,先去日志里找真正的异常,再照着你拿到的那句话行动**——我在一个从来没坏过的 UTF-8 上白白耗了时间。
|
||||
|
||||
## 怎么在它们咬到你之前找出剩下的
|
||||
|
||||
两个致命错误都是靠**运行应用**发现的。运行是可靠的方法,但你可以先一步靠"属性覆盖差异对比"把这一类问题一次挖出来。
|
||||
|
||||
思路是:解析 `vendor/` 里框架自带的配置默认值,再解析你项目里的配置类,列出前者有、后者缺的属性。这样你一次拿到整类问题,而不是每重启一次收获一个致命错误。
|
||||
|
||||
```python
|
||||
import re, pathlib
|
||||
|
||||
sys_ = pathlib.Path('vendor/codeigniter4/framework/system/Config')
|
||||
app_ = pathlib.Path('app/Config')
|
||||
|
||||
def props(path):
|
||||
try:
|
||||
src = path.read_text(encoding='utf-8', errors='replace')
|
||||
except FileNotFoundError:
|
||||
return set()
|
||||
# public/protected/private $name = ...
|
||||
return set(re.findall(r'(?:public|protected|private)\s+(?:[\w\\\[\]|?]+\s+)?\$(\w+)', src))
|
||||
|
||||
# 4.7 需要、而陈旧配置可能漏掉的属性所在类
|
||||
for name in ['App', 'Cache', 'ContentSecurityPolicy', 'CURLRequest', 'DocTypes',
|
||||
'Email', 'Encryption', 'Exceptions', 'Format', 'Honeypot',
|
||||
'Migrations', 'Paths', 'Routing', 'Toolbar', 'View']:
|
||||
missing = props(sys_ / f'{name}.php') - props(app_ / f'{name}.php')
|
||||
if missing:
|
||||
print(name, '->', sorted(missing))
|
||||
```
|
||||
|
||||
在 `composer update` **之后**、启动应用**之前**,拿新的 `vendor/` 跑一遍。它打印出来的任何东西都是潜在的致命错误。
|
||||
|
||||
在这个项目里它标出了 14 个配置文件。我把缺失的属性合并进每一个文件,以标记块的形式追加在类的末尾,这样上面原有的设置和行为完全不动:
|
||||
|
||||
```php
|
||||
// ----------------------------------------------------------------
|
||||
// CI4 4.7 compatibility — properties the 4.7 framework reads.
|
||||
// Appended as a block so existing settings above are unchanged.
|
||||
// ----------------------------------------------------------------
|
||||
```
|
||||
|
||||
框架新带出来的两个配置文件在项目里完全没有对应物,需要新建而不是合并:`Hostnames.php` 和 `WorkerMode.php`。直接从 `vendor/codeigniter4/framework/app/Config/` 拷过去就行。
|
||||
|
||||
## 升级前还值得知道的几点
|
||||
|
||||
**`app.baseURL` 的校验变严了。** 4.7 会对 4.6 接受的值抛 `ConfigException`。`'http://localhost/'`——这个值在我本地 `.env` 里躺了好几个月、一直好好的——现在直接抛异常。生产环境没受影响,因为它用的是真实 URL,但任何用了简写 baseURL 的开发环境都会拒绝启动。
|
||||
|
||||
**安全上的理由是真的。** 单是 4.7.4 就修了上传文件扩展名绕过(`is_image` / `mime_in`)、`deleteBatch()` 的 SQL 注入、以及 `UploadedFile::move()` 的路径穿越。这三个在本应用里都不可利用——它没有上传、也不碰数据库——但它们会在你加上这些功能的那一刻变成真实风险。**在你需要这些功能之前先升级,是更省事的顺序。**
|
||||
|
||||
**PHP 最低版本现在是 8.2。** 动手前先确认你的主机,不是事后。
|
||||
|
||||
## 如果重来一次,我会怎么改
|
||||
|
||||
**先拿新的 `vendor/` 跑一遍应用,别的什么都别做。** `composer update`、启动、然后修坏掉的地方。读指南告诉你可能变什么;只有运行告诉你实际变了什么。
|
||||
|
||||
**把属性差异对比当成常规动作。** 它把一串串行的调试过程变成一份报告。
|
||||
|
||||
**把升级做成独立的一次提交。** 我就是这么做的,这让两个致命错误变得极易二分定位——一条 `git show` 就精确告诉我每个错误是哪次改动引入的。把框架升级混进功能分支,会把五分钟的诊断变成一场考古。
|
||||
|
||||
**读 changelog,不要只读升级指南。** 指南列的是维护者判断「可能会弄坏你」的东西。changelog 列的是实际变了的东西。这两者不是同一个集合,而这两个致命错误正好住在两者的缝隙里。
|
||||
|
||||
## 结果
|
||||
|
||||
一个 17,000 行的 CodeIgniter 应用从 4.6.3 升到 4.7.4:合并 14 个配置文件、新增 2 个配置文件、找到并修复 2 个致命错误,所有路由验证返回 200——包括一次真实的报告生成,产出 19 页 A4,与升级前的基线在字节层面可比。应用日志零错误。
|
||||
|
||||
---
|
||||
|
||||
**手上跑着旧版 CodeIgniter?** 我做这类框架升级——CI4 minor 版本升级、PHP 版本迁移,以及那些升级指南默认你自己会搞定的配置合并工作。
|
||||
|
||||
[WhatsApp +60 12-797 2969](https://wa.me/60127972969) · [[email protected]](mailto:[email protected]?subject=CodeIgniter%20升级) · [hoelee.com](https://hoelee.com)
|
||||
Reference in New Issue
Block a user