关于 php-migration
这是面向 baserCMS 开发者的 PHP 版本迁移指南,按版本整理 8.2、8.4 和 8.5 中的弃用项、破坏性变更及修复方法。它也说明如何区分警告与致命错误,并建议结合测试定位问题。
主要功能
- 01按 PHP 版本整理迁移问题
- 02涵盖 PHP 8.2、8.4 和 8.5
- 03提供弃用项修复示例
- 04说明警告与致命错误的区别
- 05给出问题排查步骤
适用场景
- 01准备 baserCMS 的 PHP 版本升级
- 02查找特定 PHP 版本的弃用变更
- 03定位升级后出现的测试失败
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ baserproject/ baser-skills --skill php-migration安装后在 Claude Code 中输入/php-migration确认已加载。
详细介绍
PHP 版本迁移指南(baserCMS)
汇总 PHP 升级过程中遇到的弃用和破坏性变更,以及修复方案。按版本整理并持续完善。由 CakePHP 框架本身引起的问题,请参阅另一项技能 cakephp-migration。
收录版本:PHP 8.2 / 8.4 / 8.5。支持新版本时,请追加新的「## PHP X.Y」章节。 注意:PHP 8.5 是 9.0 发布前的版本。9.0 将删除所有已弃用功能,因此请尽早处理 8.5 中出现的弃用警告。
基本原则
- 区分「致命错误」和「弃用警告」。在调试模式下,弃用警告会显示在页面底部,看起来像「错误」,但在
logs/debug.log中以debug:开头的内容是警告(程序会继续运行)。Fatal/Exception 才是真正的错误。 - 区分回归问题和已有失败。使用
git diff HEAD -- <file>确认相关文件是否属于自己的修改范围。 - 更改父类的签名后,也要同步修改子类和接口。
- 要全面检测弃用问题,可以使用 Rector 等静态分析工具。
单元测试的执行方法、语法检查和失败问题的排查,请参阅
basercms-unittest技能。php -l(语法检查)也会同时显示隐式 nullable 等弃用警告。
PHP 8.2
动态属性弃用
给未声明的属性赋值已弃用。警告为 Creation of dynamic property X::$prop is deprecated。
- 方案 A(推荐):在类中声明属性。
※ 典型情况是在构造函数中通过/** @var ThemeFoldersServiceInterface */ protected $ThemeFoldersService; protected $_tempalteTypes = [];$this->Xxx = $this->getService(...)赋值。 - 方案 B:对于会影响大量子类的基类(例如测试基类
BcTestCase有 400 多个子类),添加#[\AllowDynamicProperties]。该属性会被子类继承,因此只需添加一处。#[\AllowDynamicProperties] class BcTestCase extends TestCase { ... }
PHP 8.4
1. 隐式 nullable 参数弃用
型 $x = null 属于隐式 nullable,现已弃用。请显式写成 ?型。
// Before
function foo(int $contentId = null, BcAbstractDetector $agent = null)
// After
function foo(?int $contentId = null, ?BcAbstractDetector $agent = null)
- 检测(正则表达式):匹配未以
?开头、写成型 $name = null的参数。 例如:([A-Za-z_\\][A-Za-z0-9_\\]*)\s+(\$[A-Za-z0-9_]+)\s*=\s*null(前面没有?)。 - 更改父类的签名时,也要同步修改子类/接口。
- 警告:
Implicitly marking parameter $x as nullable is deprecated, the explicit nullable type must be used instead。
2. fgetcsv() 的 $escape 弃用
省略 $escape 参数已弃用。请显式指定。
// Before
$head = fgetcsv($fp, 10240);
// After
$head = fgetcsv($fp, 10240, ',', '"', '\\');
- 自定义实现(例如使用
fgets+正则表达式解析 CSV 的函数)没有使用fgetcsv,因此不属于处理范围。只需修改实际调用fgetcsv()的位置。
3. 弃用单参数调用 ReflectionProperty::setValue()
对于非静态属性,请传入目标对象。
// Before
$property->setValue($value);
// After
$property->setValue($object, $value);
- 警告:
Calling ReflectionProperty::setValue() with a single argument is deprecated。 - 这类问题容易藏在测试工具中(例如生成请求或操作私有属性的辅助函数)。
- 对于静态属性,第一个参数传入
null:setValue(null, $value)。这在 8.1〜8.5 中均有效(不会破坏对 8.1 的兼容性)。
4. 弃用向内部函数的 string 参数传入 null(实际从 8.1+ 开始)
向非 ?string 参数传入 null,例如 parse_url(null) / version_compare($a, null) / str_replace(..., null),会出现 Passing null to parameter #N ($x) of type string is deprecated。请使用 (string) 转换显式转换。
// Before → After
parse_url($url); → parse_url((string)$url);
version_compare($current, $required); → version_compare((string)$current, (string)$required);
- 该弃用在 8.1 中引入,但在迁移过程中(尤其是升级到 8.4/8.5 时)会大量显现,因此也收录在本指南中。
- 只有运行时的值为 null 时才会出现,因此仅用静态 grep 无法全部检测到。请汇总所有测试日志中的
Passing null to parameter,以确定问题来源。
PHP 8.5(2025-11-20 发布)
9.0 发布前的最后一个小版本。以下大多数问题会产生警告,程序仍会继续运行,但相关功能将在 9.0 中删除,因此需要处理。按对应用/框架影响的可能性排序。※ 仅「0.」不是警告,而是致命错误(破坏性变更),请优先处理。
⚠️ 维持 PHP 8.1 兼容性期间的注意事项(
composer.json为"php": ">=8.1")。 如果按「推荐的替代方案」处理 8.5 中的弃用问题,有些替代方案在 8.1 中不存在,会导致 8.1 因 Fatal 而无法运行。以下标有 ❌ 的项目应暂缓处理,直到停止支持 8.1(8.5 会产生警告,但可以接受)。在 8.5 中运行本身不会破坏 8.1 兼容性(只会产生警告)。
- ❌
str_increment()(8.3+)→ 在 8.1 中继续使用$s++- ❌
http_get_last_response_headers()(8.4+)→ 在 8.1 中继续使用$http_response_header- ❌ 带命名空间的 PDO(例如
Pdo\Mysql::ATTR_*,8.4+)→ 在 8.1 中继续使用PDO::MYSQL_ATTR_*除以上项目外(非规范强制类型转换、将
null偏移量改为''、删除自动释放函数、将反引号改为shell_exec、case :、__serialize),在 8.1 中也可以安全应用。
0. 【破坏性变更・致命】使用 use 引用保留类名(如 Array)会触发 Fatal
从 8.5 开始,Array 等名称被保留为特殊类名,use ...\Array; 和 class Array 会触发 Fatal(不是警告)。如果在测试收集(自动加载)阶段崩溃,整个测试套件都无法启动,因此请优先处理。
Fatal error: Cannot use BcMail\Test\TestCase\Model\Array as Array because 'Array' is a special class name
- baserCMS 实例:
MailMessagesTableTest.php开头残留了 CakePHP2 时代的代码use ...\Array;/use ...\ClassRegistry;(都是指向不存在类的无效 import)。这些代码在 8.4 及更早版本中无害,但在 8.5 中会触发 Fatal。删除相关的use行即可解决(引用位置已通过markTestIncomplete标记为未执行代码)。 - 检测:
grep -rnE "use [A-Za-z0-9_\\\\]+\\\\(Array|Object|Resource|Enum|Mixed|Never|Void|Null|False|True|Iterable|Numeric)\s*;" --include="*.php"。 - 注意:这是致命错误,可以通过
php -l单独检查文件时检测到。但在运行所有测试前先用 grep 全面排查,可以避免测试套件在收集阶段崩溃。
1. 非规范强制类型转换名称弃用(影响较大)
(boolean) (integer) (double) (binary) 已弃用。请替换为规范名称。
// Before → After
(boolean) → (bool)
(integer) → (int)
(double) → (float)
(binary) → (string)
- 检测:
grep -rnE "\((boolean|integer|double|binary)\)" --include="*.php"。 - 旧代码中常有残留,可以通过机械替换处理。
2. null を配列オフセットに使うのが非推奨
$arr[null] / array_key_exists(null, $arr) 等で null をキーに使うのが非推奨(暗黙に "" 変換されていた)。明示的に空文字 "" を使う。
// Before
$arr[null] = $v; array_key_exists(null, $arr);
// After
$arr[''] = $v; array_key_exists('', $arr);
- 変数がキーになる箇所(
$arr[$key]で$keyが null になりうる)に注意。 - 静的 grep では検出困難(実行時に値が null のときだけ出る)。全テストを 8.5 で流して
grep "null as an array offset" phpunit.logで発生元(on line N of <file>)を集計するのが確実。 - 修正方針: ① 代入/参照のキーを
$key ?? ''(従来の暗黙変換と同等。set/get 両方を同じキーに揃えること)、②isset()/参照の手前で$key !== null &&ガード、のいずれか。getData('batch')等の null 返却を?? ''で受けるのも有効。 - baserCMS 実績(計 308 件):
BcFileUploader(uploadingFiles[$bcUploadId]の set/get を?? ''で整合)、BcUploadBehavior(oldEntity[$alias][$entity->_bc_upload_id]を?? '')、MailMessagesTable($dists[$mailField->group_valid])、SearchIndexes/Contents/PermissionsController(getData('batch') ?? '')、BcTextHelper::arrayValue/BcUtil::decodeContent($key !== null && isset(...))。
3. オブジェクトを自動解放するリソース解放関数の非推奨
対象はリソースではなくオブジェクトになっており、GC で自動解放されるため明示的な close/free 呼び出しが非推奨。呼び出しを削除すればよい。
curl_close()/curl_share_close()(CurlHandle / CurlShareHandle)finfo_close()(finfo)imagedestroy()(GdImage)← 画像処理ユーティリティで使われがちxml_parser_free()(XMLParser)- 検出:
grep -rnE "\b(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free)\s*\(" --include="*.php"。 - baserCMS 実績:
Imageresizer.php・BcFileUploader.phpのimagedestroy()4箇所。単純削除でもよいが、ループ内のメモリ即時解放の意図を保つためunset($img)に置換(GdImage はオブジェクトなので参照を外せば GC が解放。>=8.1 で安全)。
3-2. ReflectionProperty/Method::setAccessible() の非推奨(発生件数が最も多い)
setAccessible() は PHP 8.1 以降 no-op(private/protected も getValue/setValue/invoke で直接アクセス可)。8.5 で非推奨化。呼び出し行を削除すればよい(>=8.1 で安全)。
// Before
$property = new ReflectionProperty($obj, 'foo');
$property->setAccessible(true); // ← この行を削除
$property->setValue($obj, $v);
- baserCMS 実績: src/テスト合わせて33箇所(
BcUtil/BcTestCase/BaserCorePlugin/PreviewController/BcDatabaseServiceほか)。ホットパス(テスト基底等)にあるためランタイム発生件数が桁違いに多い(全テストで約6,300件 ≒ 8.5 非推奨の大半)。まずこれを潰すとログが激減する。 - 一括削除(単独文に限定):
sed -i -E "/^[[:space:]]*\$[A-Za-z0-9_]+->setAccessible\(true\);[[:space:]]*$/d" <file>。 - 検出時の注意: grep パターンを
->setAccessibleのように-で始めると grep がオプションと誤認して 0 件になる。grep -rn "setAccessible"のように-始まりを避ける(またはgrep -e)。
4. 非数値文字列のインクリメント非推奨
$str++(非数値文字列)が非推奨。str_increment() を使う。※ str_increment() は 8.3+。8.1 維持中は $str++ のまま据え置く。
// Before
$code++; // $code が 'AZ' 等
// After
$code = str_increment($code);
5. __sleep() / __wakeup() のソフト非推奨
__serialize() / __unserialize() を使う(PHP7 互換が不要なら移行)。
6. $http_response_header の非推奨
スーパーグローバル $http_response_header が非推奨。http_get_last_response_headers() を使う。※ http_get_last_response_headers() は 8.4+。8.1 維持中は $http_response_header のまま据え置く。
7. バッククォート演算子の非推奨
`command`(shell_exec() のエイリアス)が非推奨。shell_exec() を直接使う。
8. case 文を ; で終端するのが非推奨
case 1; は非推奨。case 1: にする。
- 検出:
grep -rnE "case\s+.+;\s*$" --include="*.php"(誤検出が多いので目視確認)。
8-2. array_key_exists(null, ...) の非推奨(serialize の不正形で誘発)
array_key_exists() の第1引数 null が非推奨(空文字を使う)。フレームワーク内部(CakePHP JsonView::_dataToSerialize)で出る場合、原因は自前コードの serialize オプションの不正形。setOption('serialize', ['foo', 'message' => $message]) のように変数名のリストに 'key' => 値 を混ぜると、$message が null のとき serialize 配列に 'message' => null が入り、array_key_exists(null, ...) が走る。
- 修正: serialize はビュー変数名の羅列にする。
['foo', 'message' => $message]→['foo', 'message'](値は$this->set()済み)。baserCMS 実績:MailMessagesControllerの index/view アクション。 - 発生元がフレームワーク内のときの特定法: vendor を一時計測(
if ($key === null) file_put_contents(..., var_export($serialize,true).getTraceAsString()))→ 該当プラグインのテストを流す → スタックトレースの先頭の自前コントローラを特定 → vendor を戻す。
8-3. ArrayObject にオブジェクトを backing array として渡す非推奨
new ArrayObject($obj) でオブジェクト(json_decode() の stdClass 等)を渡すのが非推奨。配列にキャストする。
- 修正:
new ArrayObject(json_decode($body)->devices)→new ArrayObject((array)json_decode($body)->devices)。baserCMS 実績:SitesControllerTest。
9. 其他(如适用)
- PDO 驱动专属常量/方法已迁移到带命名空间的形式(
PDO::MYSQL_ATTR_*→Pdo\Mysql::ATTR_*、PDO::sqliteCreateFunction()→Pdo\Sqlite::createFunction()等)。大多数情况下,这些都在框架内部;如果你自行使用了数据库驱动常量,则需要相应调整。PDO 的"uri:"DSN 方案也已弃用。※ 带命名空间的 PDO 类仅适用于 PHP 8.4+。如果仍维持 PHP 8.1,则继续使用PDO::MYSQL_ATTR_*。 openssl_pkey_derive()的$key_length、finfo_buffer()的$context参数已弃用(会被忽略)。- 非 CLI SAPI 中的
register_argc_argvINI 配置已弃用(改用$_GET/$_SERVER['QUERY_STRING'])。 - 部分闭包绑定操作已弃用,例如绑定到静态闭包。
clone null的错误消息变更(影响测试):截至 PHP 8.4,错误消息为__clone method called on non-object;PHP 8.5 中为clone(): Argument #1 ($object) must be of type object, null given。如果测试通过字符串匹配验证对null执行clone的异常情况,则需要更新预期值(baserCMS 实例:UploaderCategoriesControllerTest的 Admin/Api 各自的test_copy)。
⚠️ 运行所有测试时出现连锁错误,应先重新运行以判断原因:在此代码库中,完整运行测试时偶尔会出现短暂的状态污染:“某个先行测试污染了全局状态(例如插件设置 Configure 等),导致之后几乎所有测试在 Application bootstrap 时出现
PluginCollection::create(): $config ... null given等连锁错误”。如果插件单独的测试套件(例如plugins/baser-core/tests/TestCase)通过,且重新运行后测试正常,则这不是代码导致的问题,而是测试不稳定。务必通过隔离运行和重新运行进行确认。
调查步骤(检查清单)
- 使用
php -l(语法检查)或查看logs/debug.log,确定弃用警告发生的文件和行号。 - 对照相应版本章节,判断属于哪种模式并进行修复。可通过机械替换处理的情况(例如非规范的类型转换)可使用 grep 批量查找。
- 使用
php -l重新确认(检查警告是否已消失)。 - 使用
--filter单独运行相关测试进行确认 → 最后重新运行所有测试。
相关:由框架导致的问题(事件返回值、Association 重复、ResultSet、FormProtection 等)请参阅 cakephp-migration 技能。