标题提到的PHP 8.2,在本文写作时(2022年9月)尚未正式发布,正式版于2022年12月发布。我们团队为了提前适配,在测试环境用了PHP 8.2 RC版本,结果遇到了一个因为新特性导致的Bug。

虽然是在测试环境,但问题很典型,排查过程也很有参考价值。本文分享这次排查的完整过程,包括问题现象、排查思路、根本原因、解决方案,以及升级PHP版本的注意事项。

一、背景

1. 为什么提前用PHP 8.2

PHP 8.2带来了很多新特性和性能改进:

  • 只读类(Readonly Classes)
  • 弃用动态属性(Dynamic Properties Deprecated)
  • 新的类型系统改进(独立类型、交集类型改进等)
  • 性能优化
  • 新的函数和类

我们团队一直有跟进新版本的习惯,所以在PHP 8.2 RC版本出来后,就在测试环境部署了,想提前发现兼容性问题,等正式版发布后能顺利升级。

2. 项目情况

我们的项目是一个中型的Web应用:

  • 框架:Laravel 9
  • PHP版本:生产环境PHP 8.1,测试环境PHP 8.2 RC
  • 代码量:约20万行
  • 依赖:大量Composer包
  • 部署:Docker容器

二、问题现象

1. 测试环境报错

部署到PHP 8.2测试环境后,一开始一切正常。但跑了几天,某些页面开始报错。

错误信息是:

Deprecated: Creation of dynamic property ... is deprecated

这是一个弃用警告(Deprecated),不是致命错误。但我们的测试环境把所有错误都显示出来了,所以页面上满是警告。

更严重的是,某些接口返回的数据不对,有些字段丢失了。

2. 问题特点

这个问题有几个特点:

  • 只在PHP 8.2环境出现,PHP 8.1正常
  • 不是所有页面都报错,只有某些特定的页面
  • 报错信息都是关于动态属性的
  • 有些功能虽然不报错,但数据不对

三、排查过程

1. 初步分析

看到错误信息,我第一反应是:PHP 8.2弃用了动态属性。

在PHP 8.2之前,你可以给对象动态添加属性,不需要在类中声明:

class User {
    public $name;
}

$user = new User();
$user->age = 25; // 动态属性,PHP 8.1及之前可以,PHP 8.2弃用

PHP 8.2开始,动态属性被弃用,会抛出Deprecated警告。未来的PHP版本会移除这个特性。

但我们的代码里,应该没有动态属性啊?都是用的框架,属性都在类里声明了。

2. 定位报错的代码

我根据报错信息,定位到了具体的代码。

报错的地方,是一个模型类:

class Product extends Model {
    // 没有声明属性,用的是Laravel的魔术方法
}

Laravel的Eloquent模型,用的是魔术方法getset来访问属性。属性存在$attributes数组里,不是真正的对象属性。

但问题出在,我们有些代码,给模型对象动态添加了自定义属性:

$product = Product::find(1);
$product->custom_field = 'some value'; // 动态属性!

这些代码在PHP 8.1中是正常的,但在PHP 8.2中就被弃用了。

3. 更严重的问题

动态属性的弃用警告,只是表面问题。更严重的是,有些数据丢失了。

我仔细检查后发现,问题出在JSON序列化上。

我们的API接口,返回的是模型的JSON。在PHP 8.1中,动态属性会被包含在JSON中。但在PHP 8.2中,动态属性的处理方式变了,有些动态属性没有被正确序列化。

具体来说,Laravel的ArrayableJsonable接口,在处理动态属性时,和PHP 8.2的新行为有冲突。

4. 深入分析

我深入研究了PHP 8.2的变化:

变化一:动态属性弃用

  • PHP 8.2中,给未声明的属性赋值,会抛出Deprecated警告
  • 但如果类使用了#[AllowDynamicProperties]注解,就允许动态属性
  • stdClass和使用了get/set魔术方法的类,不受影响

变化二:只读类

  • PHP 8.2引入了只读类,用readonly关键字声明
  • 只读类的所有属性都是只读的,只能在构造函数中赋值
  • 只读类不能有动态属性

变化三:类型系统改进

  • PHP 8.2改进了类型系统
  • 某些类型的处理方式变了
  • 空值和类型转换的行为有细微变化

5. 找到根本原因

经过仔细排查,我找到了根本原因:

我们的代码中,有一个基类,用了动态属性来存储扩展数据。这个基类没有声明这些属性,也没有用#[AllowDynamicProperties]注解。

在PHP 8.1中,这没问题。但在PHP 8.2中:

  1. 动态属性被弃用,抛出警告
  2. 某些情况下,动态属性没有被正确初始化
  3. JSON序列化时,这些属性丢失了

更麻烦的是,我们依赖的一些第三方包,也用了动态属性。这些包还没有适配PHP 8.2。

四、解决方案

找到问题后,开始解决。

1. 紧急修复

首先,紧急修复测试环境的问题:

方案一:给使用动态属性的类加上#[AllowDynamicProperties]注解

#[AllowDynamicProperties]
class BaseModel {
    // ...
}

方案二:在类中声明所有属性,不用动态属性

class BaseModel {
    public $custom_field;
    // ...
}

方案三:用魔术方法get/set,把属性存在数组里

class BaseModel {
    private $data = [];
    
    public function __get($key) {
        return $this->data[$key] ?? null;
    }
    
    public function __set($key, $value) {
        $this->data[$key] = $value;
    }
}

我们选择了方案一(加注解)作为紧急修复,因为改动最小,能快速解决问题。

2. 根本修复

然后,做根本修复:

  • 审查所有代码,找出使用动态属性的地方
  • 能声明属性的,都声明属性
  • 必须用动态属性的,加上#[AllowDynamicProperties]注解
  • 升级依赖的第三方包,到支持PHP 8.2的版本
  • 对不支持PHP 8.2的包,找替代方案或临时补丁

3. 增加检查

在CI/CD流程中增加PHP 8.2的兼容性检查:

  • 用PHP 8.2运行单元测试
  • 用静态分析工具(如PHPStan)检查动态属性
  • 代码审查时,注意动态属性的使用

五、PHP 8.2其他需要注意的新特性

除了动态属性,PHP 8.2还有一些新特性,升级时需要注意:

1. 只读类(Readonly Classes)

PHP 8.2引入了只读类:

readonly class User {
    public string $name;
    public int $age;
    
    public function __construct(string $name, int $age) {
        $this->name = $name;
        $this->age = $age;
    }
}

只读类的所有属性都是只读的,只能在构造函数中赋值。这对于值对象(Value Object)很有用。

注意:

  • 只读类不能有动态属性
  • 只读类的属性不能有默认值(除了构造函数中)
  • 只读类不能被继承(除非子类也是只读的)

2. 弃用的功能

PHP 8.2弃用了一些功能:

  • 动态属性(前面说的)
  • 某些utf8encodeutf8decode的用法
  • 某些字符串的隐式转换
  • ${}字符串插值语法

升级时,要检查代码中是否用了这些被弃用的功能。

3. 新的类型和函数

PHP 8.2增加了一些新的类型和函数:

  • truefalse作为独立类型
  • nullfalse的交集类型
  • 新的字符串函数和数组函数
  • mysqli的一些改进

这些新特性,可以在升级后逐步使用。

4. 性能改进

PHP 8.2有一些性能改进:

  • 某些操作的速度提升
  • 内存使用优化
  • JIT的改进

但实际性能提升,取决于具体的应用。建议升级后做性能测试。

六、升级PHP版本的一般步骤

总结一下升级PHP版本的一般步骤:

1. 了解新版本的变化

  • 阅读官方的升级指南和迁移文档
  • 了解新特性、弃用的功能、不兼容的变化
  • 关注性能变化和安全改进

2. 在测试环境部署

  • 先在测试环境部署新版本
  • 不要直接在生产环境升级
  • 用Docker等容器技术,方便切换版本

3. 运行测试

  • 运行单元测试、集成测试、功能测试
  • 测试所有核心功能
  • 关注错误日志和警告

4. 修复兼容性问题

  • 修复弃用警告
  • 修复不兼容的代码
  • 升级依赖的第三方包
  • 对不兼容的包,找替代方案

5. 性能测试

  • 对比新旧版本的性能
  • 关注响应时间、内存使用、CPU使用
  • 优化性能瓶颈

6. 灰度发布

  • 不要一次性全量升级
  • 先灰度发布到部分服务器
  • 监控错误率和性能指标
  • 确认稳定后,再全量升级

7. 回滚方案

  • 准备好回滚方案
  • 如果出问题,能快速回滚到旧版本
  • 保留旧版本的环境和配置

七、踩过的坑

坑一:第三方包不兼容

升级PHP版本,最大的问题往往不是自己的代码,而是第三方包。

很多第三方包更新慢,不支持新版本的PHP。我们项目中,有好几个包不支持PHP 8.2,有的甚至已经停止维护了。

解决:

  • 提前检查依赖包的兼容性
  • 对不支持的包,找替代方案
  • 或者给包提交PR,帮助适配新版本
  • 实在不行,临时用补丁

教训: 升级前,先检查所有依赖包的兼容性。


坑二:弃用警告被忽略

PHP的弃用警告(Deprecated),很多人会忽略,觉得不是错误。

但弃用警告意味着,未来的版本会移除这个功能。如果现在不处理,将来升级到下一个版本,就会变成致命错误。

解决:

  • 把弃用警告当成错误来处理
  • 在开发环境显示所有错误
  • 在CI中检查弃用警告
  • 及时修复,不要拖延

教训: 不要忽略弃用警告,现在不修,将来更麻烦。


坑三:魔术方法的行为变化

PHP新版本中,魔术方法(getset__call等)的行为有时候会有细微变化。

我们的代码中,有一个用了__get的类,在PHP 8.2中行为变了,导致某些属性访问不到。

解决:

  • 仔细阅读新版本的魔术方法文档
  • 测试所有用了魔术方法的类
  • 不要过度依赖魔术方法的边缘行为

教训: 魔术方法是黑魔法,能不用就不用。用了就要仔细测试。


坑四:测试覆盖不够

我们的单元测试覆盖率不高,很多边缘情况没有测试到。升级后,一些不常用的功能出了问题,测试没有发现。

解决:

  • 提高测试覆盖率
  • 重点测试核心功能和边缘情况
  • 升级后,做全面的回归测试

教训: 测试是升级的安全网,测试覆盖越高,升级越放心。

八、写在最后

这次PHP 8.2的兼容性问题,虽然是在测试环境发现的,但给我们敲响了警钟。

PHP的版本更新越来越快,新特性越来越多,弃用的功能也越来越多。作为开发者,我们要跟进新版本,但也要注意兼容性问题。

升级PHP版本,不是一件小事,需要认真对待。提前了解变化,在测试环境充分验证,修复兼容性问题,灰度发布,准备回滚方案。每一步都不能少。

2022年了,PHP 8.2即将发布。它带来了很多好的特性,也移除了一些旧的写法。对于我们PHP开发者来说,这既是挑战,也是机遇。拥抱变化,持续学习,才能跟上技术的步伐。

最后,用一句话总结:"PHP升级,提前了解变化,充分测试,逐步迁移。不要忽略弃用警告,不要直接在生产环境升级。"

愿你的PHP升级,一帆风顺,不用熬夜排查问题。