搜索全站
插件 15 分钟阅读

WordPress 插件不兼容 PHP 新版本:临时补丁与替代方案

升级 PHP 8.2 后插件报 Deprecated、前台空白?本文列出 PHP 8 各版本真实报错与成因,给出 mu-plugin 临时屏蔽、兼容补丁写法、PHP 兼容扫描命令与替代插件选择标准。

U
UFO_PF 2022-03-06 · 阅读 15 分钟
Wo 插件
阅读提示
本文约 5,803 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:升级 PHP 之后,插件先崩了

主机商提醒 PHP 7.4 已停止安全支持,你在面板里切到 PHP 8.2,前台立刻冒出几行 Deprecated: Creation of dynamic property ... is deprecated,某个插件的页面直接 Fatal error 白屏。后台点进去,Site Health 一片红。

这类问题的尴尬之处在于:PHP 版本必须升(老版本没有安全补丁),插件却没人维护。你既不能等作者更新,也不能把站点晾着。可行的路径有三条:先止血(让站点可用)→ 再打补丁(自己修)→ 最后评估替代(换插件),三条按这个顺序走。

原理分析:PHP 8 各版本到底改了什么

PHP 版本 关键变更 典型报错
8.0 移除 each()、create_function();字符串与数字比较规则变更 Uncaught Error: Call to undefined function create_function()
8.0 内部函数参数类型严格化 Uncaught TypeError: strlen(): Argument #1 ($string) must be of type string, null given
8.1 禁止向内部函数传 null Deprecated: Passing null to parameter #1 ($string) of type string is deprecated
8.1 接口返回类型暂定(tentative return types) Return type of X::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(): mixed, or the #[\ReturnTypeWillChange] attribute should be used
8.2 动态属性弃用 Deprecated: Creation of dynamic property Foo::$bar is deprecated
8.2 ${} 字符串插值弃用 Deprecated: Using ${var} in strings is deprecated

注意区分两类:Deprecated(弃用) 只是提示,功能还能用,默认不会中断页面——之所以你能看到,是因为开了 WP_DEBUG 且 display_errors 打开了;Fatal error / TypeError 才是真中断,必须改代码。

所以第一件要事是:生产环境永远 display_errors = Off,把弃用提示写进日志而不是打在页面上。这不是掩盖问题,而是让站点先恢复可用,再从容修。

保姆级实操

第一步:先止血,恢复站点可用

// wp-config.php:生产环境标准配置
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );            // 写入 wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false );       // 不在页面上显示
@ini_set( 'display_errors', '0' );

如果只想静音特定插件的弃用提示(避免日志被刷爆),用 set_error_handler 精确吞掉:

<?php
// wp-content/mu-plugins/legacy-deprecated-shim.php
add_action( 'muplugins_loaded', function () {
    if ( ! WP_DEBUG ) {
        return;
    }
    set_error_handler( function ( $errno, $errstr, $errfile, $errline ) {
        if ( E_DEPRECATED !== $errno ) {
            return false;   // 其他错误交给标准处理器
        }
        // 只静音指定老插件目录里的弃用提示
        $targets = [
            'wp-content/plugins/old-plugin/',
            'wp-content/plugins/legacy-addon/',
        ];
        foreach ( $targets as $target ) {
            if ( str_contains( $errfile, $target ) ) {
                return true;   // 已处理,不再上报
            }
        }
        return false;
    } );
} );

set_error_handler 的回调返回 true 表示错误已处理、不再冒泡,返回 false 交给内置处理器。这个方案只是争取时间,不代表问题消失。

第二步:扫描插件里真正的不兼容写法

cd wp-content/plugins/old-plugin

# 已被 PHP 8 移除的函数
grep -rIn -e 'each(' -e 'create_function(' -e 'mysql_query(' \
          -e '\$HTTP_RAW_POST_DATA' -e '\${' --include='*.php' . | head -30

# 动态属性:给类里未声明的属性赋值(PHP 8.2 弃用)
grep -rInE '\$this->[a-zA-Z_]+ *= ' --include='*.php' . | head -20

# 需要补 ReturnTypeWillChange 的接口实现
grep -rInE 'function (offsetGet|offsetSet|offsetExists|offsetUnset|count|getIterator|jsonSerialize|rewind|current|key|next|valid)\s*\(' --include='*.php' .

# PHP 8.0 起构造函数名与类名不一致不再被当作构造器
grep -rInE 'function +[a-z_]+\s*\(.*\)\s*\{' --include='*.php' classes/ | head -20

grep 只能找嫌疑点,精确扫描应该用 PHP CodeSniffer + PHPCompatibility 标准:

composer global require phpcompatibility/php-compatibility
phpcs --config-set installed_paths ~/.composer/vendor/phpcompatibility/php-compatibility
phpcs -p wp-content/plugins/old-plugin \
      --standard=PHPCompatibility \
      --runtime-set testVersion 8.0-8.3 \
      --report=summary

第三步:打最小兼容补丁

最常见也最好修的是接口返回类型问题,在方法上加属性即可:

<?php
// 原代码(PHP 8.1 起报 Deprecated)
class Legacy_Collection implements ArrayAccess, Countable {
    private $items = [];

    public function offsetGet( $offset ) {
        return $this->items[ $offset ] ?? null;
    }

    public function count() {
        return count( $this->items );
    }
}

打完补丁:

<?php
class Legacy_Collection implements ArrayAccess, Countable {
    private $items = [];

    #[\ReturnTypeWillChange]
    public function offsetGet( $offset ) {
        return $this->items[ $offset ] ?? null;
    }

    #[\ReturnTypeWillChange]
    public function count() {
        return count( $this->items );
    }
}

#[\ReturnTypeWillChange] 是 PHP 8.1 引入的属性(attribute),告诉引擎「我知道返回类型待定,先别报警」。注意 PHP 8.0 里它不存在,但用 #[...] 语法写注释形式在旧版会被当注释,不会报错,所以可以安全地写进代码。

动态属性问题则是在类上加 #[AllowDynamicProperties](PHP 8.2+)或干脆把属性声明出来。注意:直接改 wp-content/plugins/ 里的文件,插件一更新就被覆盖,所以补丁要么配合自建更新源(WordPress 插件自建更新源),要么记录成 patch 文件:

# 生成补丁,插件更新后重新应用
diff -u old-plugin/includes/class-collection.php.orig \
        old-plugin/includes/class-collection.php > /root/patches/old-plugin.patch

第四步:评估是否换插件

四个判断点,看 wp.org 插件页右侧栏:

  • Requires PHP:写着 7.2,说明作者按 7.2 写,8.x 只是没测;
  • Tested up to:WordPress 版本,长期落后两三个大版本的说明维护不积极;
  • Last updated 与支持论坛的最后回复时间:超过一年基本可判定停止维护;
  • Active installations 与 Rating:用户基数小的,出问题没人反馈,作者也没动力修。

如果插件已停止维护且替代方案成熟(比如发信插件、缓存插件、表单插件都有多个成熟选择),迁移成本往往低于长期打补丁。迁移前用 wp option list --search='*插件前缀*' 摸清它存了哪些数据,确认新插件有导入工具。

第五步:实在修不了就先停在旧版 PHP

多数主机面板(cPanel、宝塔、Cloudways)支持多 PHP 版本共存,可以按站点切换。停在 PHP 7.4 / 8.0 只是权宜之计,但要有期限:给旧 PHP 的站点单独加固(WAF、限制后台入口、关闭 XML-RPC),同时把插件替换排进计划。

常见踩坑

  1. 关掉 WP_DEBUG 就当修好了:弃用提示今天只是提示,PHP 9.0 会变成 Fatal error,迟早要爆。

  2. 案例: 某站升级 PHP 8.2 后,一个 2015 年的图库插件在 foreach 里用 $this->$key = $value 动态建属性,一页刷出 2000 多行 Deprecated 写进 debug.log,三天后日志文件 1.8GB,把磁盘撑满导致数据库写入失败、站点全站 500。教训:静音弃用提示的同时,必须给 debug.log 配 logrotate,或直接记录到 syslog。

  3. 改了插件文件忘了记录:两个月后插件自动更新,补丁消失,故障复现,没人记得改过什么。

  4. 在 PHP 8.0 上写 #[\ReturnTypeWillChange]:PHP 8.0 不识别这个属性,但它作为注释语法不会报错,只是不生效——如果你还跑在 8.0,弃用提示不会消失。

  5. mysql_* 系列函数:PHP 7.0 就移除了,插件如果还在用,说明它至少十年没更新,直接换掉,别修。

  6. PHP 扩展缺失:升级 PHP 后 imagick、zip、intl 没装,插件报 Call to undefined class Imagick,看起来像兼容问题其实是扩展没装。用 wp eval 'print_r( get_loaded_extensions() );' 核对。

  7. 只测首页:很多兼容问题只在特定页面(结算页、插件设置页)触发,升级后必须把关键路径全跑一遍。

总结

判断标准:Deprecated 可以暂时静音,Fatal error 必须立刻修;维护中的插件等更新,停止维护的插件要么打补丁要么换掉。

行动清单:升级前在 staging 环境跑 phpcs --standard=PHPCompatibility 全量扫描;生产环境强制 WP_DEBUG_DISPLAY = false 并给日志加轮转;用 set_error_handler 精准静音老插件的弃用提示;接口返回类型问题加 #[\ReturnTypeWillChange];所有补丁存成 patch 文件并记录;查插件页的 Tested up to 与最后更新时间,决定是等还是换;实在不行就短期停在旧 PHP 并给站点加额外防护。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。