Draft bank: 2 CodeIgniter posts (EN+ZH), held unpublished
Deploy / build (push) Successful in 18s

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:
2026-09-20 04:19:37 +08:00
parent a1488da2d8
commit a9a91a25d4
5 changed files with 824 additions and 0 deletions
@@ -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)