- migrating-codeigniter-iis-to-openlitespeed (engineering): the two fatal errors IIS + SSO had been hiding (env() called from Constants.php; parent::__construct in a controller) — both only surfaced on the first unauthenticated request - upgrading-codeigniter-46-to-47 (notes): the two undefined config properties the official upgrade guide cannot warn about (Config\App::$permittedURIChars, Config\Format::$jsonEncodeDepth) - both flipped draft:true -> false, pubDate 2026-09-20 -> 2026-09-27 (real publish date) - generated 1200x630 OG cards + 1600x900 banners for both slugs - verified before commit: EN+ZH pages build, language switch links both ways, listing order stays monotonic on /posts, /zh and the homepage, banner terminal panel centred (gapAbove 102 / gapBelow 104) with no overflow
9.3 KiB
title, description, pubDate, category, tags, ogImage, banner, draft
| title | description | pubDate | category | tags | ogImage | banner | draft | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CodeIgniter 4.6 升级到 4.7:官方指南没说到的破坏性变更 | CodeIgniter 4.7 官方升级指南列了八项破坏性变更。真正让我应用致命崩溃的那两项不在里面——而且它们属于同一类问题。 | 2026-09-27 | notes |
|
/og/upgrading-codeigniter-46-to-47.png | /banners/upgrading-codeigniter-46-to-47.png | false |
我把一个 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 在路由任何东西之前就抛异常了。
// 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() 解的,失败信号在很下游才爆出来,表现为一个让人一头雾水的解析错误,而不是真正的"属性未定义"。
// 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/ 里框架自带的配置默认值,再解析你项目里的配置类,列出前者有、后者缺的属性。这样你一次拿到整类问题,而不是每重启一次收获一个致命错误。
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 个配置文件。我把缺失的属性合并进每一个文件,以标记块的形式追加在类的末尾,这样上面原有的设置和行为完全不动:
// ----------------------------------------------------------------
// 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 版本迁移,以及那些升级指南默认你自己会搞定的配置合并工作。