Communitygithub.com

PHP 版本迁移指南(baserCMS)

针对 PHP 版本升级中遇到的弃用和破坏性变更,提供按版本组织的修复方案,助力开发者高效排查和修复。

PHP 版本迁移指南(baserCMS) 是什么?

该技能提供了针对 baserCMS 项目的 PHP 版本迁移指南,涵盖 PHP 8.2、8.4 和 8.5 版本中常见的弃用和破坏性变更,并给出了具体的修复策略和代码示例。它特别强调了区分致命错误与弃用警告、处理回归与已有失败、以及保持子类与接口签名一致性等原则。技能还包含详细的调查步骤和检查清单,帮助开发者系统性地定位和修复问题。此外,它指出了 PHP 8.5 作为 9.0 前最后一个次要版本的重要性,并给出了在保持 PHP 8.1 兼容性时的注意事项。该技能适用于 Claude Code、Cursor 等 AI 编程助手,能够辅助开发者高效完成 PHP 版本迁移工作。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add baserproject/baser-skills

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

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,已被弃用。应显式写为 `?类型`。
```php
// 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)。这在 PHP 8.1 至 8.5 均有效(不会破坏 8.1 兼容性)。

4. 向内部函数的 string 参数传递 null 已被弃用(实际始于 8.1)

parse_url(null) / version_compare($a, null) / str_replace(..., null) 等,向非 ?string 参数传递 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 出现致命错误而无法运行。以下 ❌ 标记的项目应推迟处理,直到放弃 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)会导致致命错误

8.5 中 Array 等被保留为特殊类名,use ...\Array; 或 class Array 会引发致命错误(非警告)。如果在测试收集(自动加载)阶段崩溃,会导致整个测试套件无法启动,需要最优先处理。

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 中变成致命错误。删除对应的 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 "\(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free)\s*\\(" --include="*.php"。
  • baserCMS 实例:Imageresizer.php 和 BcFileUploader.php 中的 4 处 imagedestroy()。可以直接删除,但为了保持循环内立即释放内存的意图,建议替换为 unset($img)(GdImage 是对象,解除引用后 GC 会释放。>=8.1 安全)。

3-2. ReflectionProperty/Method::setAccessible() 的弃用(发生数量最多)

setAccessible() 在 PHP 8.1 之后是空操作(可以直接通过 getValue/setValue/invoke 访问 private/protected 成员)。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 等)。由于位于热点路径(如测试基类),运行时发生次数异常多(所有测试中约 6300 次 ≈ 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, ...) 的弃用(由序列化的错误形式触发)

array_key_exists() 的第一个参数为 null 已被弃用(应使用空字符串)。如果问题出现在框架内部(如 CakePHP JsonView::_dataToSerialize),原因通常是自定义代码中 serialize 选项的形式错误。如果在 setOption('serialize', ['foo', 'message' => $message]) 中将 'key' => 值 混入变量名列表,当 $message 为 null 时,序列化数组会包含 '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() 等)。大多数在框架内部,但如果自定义代码中使用了 DB 驱动常量,则需要处理。PDO 的 "uri:" DSN 方案也被弃用。※ 命名空间 PDO 类需要 8.4+。保持 8.1 兼容性期间,请保留 PDO::MYSQL_ATTR_*。
  • openssl_pkey_derive() 的 $key_length 参数和 finfo_buffer() 的 $context 参数被弃用(会被忽略)。
  • 非 CLI SAPI 中的 register_argc_argv INI 设置被弃用(请使用 $_GET 和 $_SERVER['QUERY_STRING'])。
  • 对静态闭包进行 bind 等部分闭包绑定操作被弃用。
  • clone null 的错误消息变更(影响测试):8.4 之前为 __clone method called on non-object,8.5 为 clone(): Argument #1 ($object) must be of type object, null given。如果测试通过字符串匹配验证 clone null 的异常,则需要更新预期值(baserCMS 实例:UploaderCategoriesControllerTest 的 Admin/Api 各 test_copy)。

⚠️ 全测试运行时出现连锁错误应先重试以确认是否为偶发:本代码库中,全量运行时偶尔会出现“某个先运行的测试污染了全局状态(如插件配置 Configure)→ 后续几乎所有测试在 Application bootstrap 阶段因 PluginCollection::create(): $config ... null given 等而连锁错误”的一过性污染。如果单独运行插件的测试套件(如 plugins/baser-core/tests/TestCase)正常,且重试后变干净,则问题不是代码引起的,而是脆弱的测试(flaky test)。务必通过隔离和重试来确认。


调查步骤(检查清单)

  1. 使用 php -l(语法检查)或检查 logs/debug.log,确定弃用警告发生的文件和行号。
  2. 对照对应版本的章节,分类属于哪种模式并修复。可以机械替换的(如非标准类型转换)使用 grep 批量检测。
  3. 使用 php -l 再次确认(警告是否消失)。
  4. 使用 --filter 单独运行相关测试 → 最后重新运行所有测试。

框架相关的问题(事件返回值、Association 重复、ResultSet、FormProtection 等)请参考 cakephp-migration 技能。


参考来源

相关技能