mcpField
P
php-migration

php-migration

github.com/baserproject开发048 次安装1 周前更新
代码仓库

关于 php-migration

这是面向 baserCMS 开发者的 PHP 版本迁移指南,按版本整理 8.2、8.4 和 8.5 中的弃用项、破坏性变更及修复方法。它也说明如何区分警告与致命错误,并建议结合测试定位问题。

主要功能

  1. 01按 PHP 版本整理迁移问题
  2. 02涵盖 PHP 8.2、8.4 和 8.5
  3. 03提供弃用项修复示例
  4. 04说明警告与致命错误的区别
  5. 05给出问题排查步骤

适用场景

  1. 01准备 baserCMS 的 PHP 版本升级
  2. 02查找特定 PHP 版本的弃用变更
  3. 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_argv INI 配置已弃用(改用 $_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)通过,且重新运行后测试正常,则这不是代码导致的问题,而是测试不稳定。务必通过隔离运行和重新运行进行确认。


调查步骤(检查清单)

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

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


来源

评论

支持 Markdown