项目从 PHP 7.4 迁到 8.3,一开始以为只是改改配置,结果 error_log 刷了几百行。这篇把实际踩到的坑按「编译期致命 / 运行时告警 / 静默行为变化」三类整理出来,附上修改前后对比。
一、先搞清楚哪些是「致命」,哪些只是「告警」
升级前先做一件事:把所有代码跑一遍语法检查。
find . -name "*.php" -not -path "./vendor/*" -print0 \
| xargs -0 -n1 -P8 php -l 2>&1 | grep -v "No syntax errors"这一步能抓出编译期就报错的代码。剩下的运行时问题,靠把 error_reporting 开到 E_ALL 再跑一遍业务来暴露。
// 升级期间临时打开,上线前务必关掉 display_errors
error_reporting(E_ALL);
ini_set('display_errors', '1');二、编译期致命错误
1. __autoload() 被彻底移除
7.x 时代还能用的老式自动加载,8.0 直接报 Fatal error。
// PHP 7.4 能跑,8.0 直接挂
function __autoload($class) {
require_once __DIR__ . '/' . $class . '.php';
}改成 spl_autoload_register():
spl_autoload_register(function (string $class): void {
$file = __DIR__ . '/' . str_replace('\\', '/', $class) . '.php';
if (is_file($file)) {
require_once $file;
}
});如果这个文件是被别的库 require 进来的,函数名改成 __autoload_legacy() 之类就行——关键是别叫 __autoload。
2. 花括号取字符串下标 $str{0}
这个语法 7.4 就废弃了,8.0 移除。
// 挂了
$first = $str{0};
// 改成
$first = $str[0];有些第三方库(尤其是老版本的二维码、加密库)大量使用这种写法,升级时要重点搜:
grep -rn --include="*.php" -E '\$[a-zA-Z_][a-zA-Z0-9_]*\{[0-9$]' .3. each()、create_function()
两个都在 8.0 移除。
// 旧写法
while (list($k, $v) = each($arr)) { /* ... */ }
// 新写法
foreach ($arr as $k => $v) { /* ... */ }// 旧写法
$fn = create_function('$a, $b', 'return $a + $b;');
// 新写法
$fn = function ($a, $b) { return $a + $b; };三、运行时告警:这类最容易被忽略
1. 给非 nullable 的内建函数传 null(8.1 起废弃)
这是升级后报得最多的一条。以前 trim(null) 会静默返回 '',现在会抛 Deprecated。
我在一个老主题里就遇到这个:
// 报 Deprecated: trim(): Passing null to parameter #1 ($string) of type string is deprecated
$mail = $db->fetchRow(...)['mail']; // 查不到时返回 null
$hash = md5(strtolower(trim($mail)));问题根源是查询没命中,$mail 是 null。正确做法不是把 trim 包起来,而是修掉数据来源:
$row = $db->fetchRow($db->select()->from('table.users')->order('uid', 'ASC')->limit(1));
$mail = (is_array($row) && isset($row['mail'])) ? (string) $row['mail'] : '';
$hash = md5(strtolower(trim($mail)));2. 浮点数隐式转整型丢精度(8.1 起废弃)
// Deprecated: Implicit conversion from float 2.25 to int loses precision
$i = (int) (2.25 * 1); // 这种显式转换没问题
// 出问题的是隐式转换,比如传参给 int 形参
function scale(int $n) { /* ... */ }
scale(2.25); // Deprecated如果是图像处理、缩放比例计算这类场景,用 intdiv() 或显式 (int) 并接受截断:
$blocks = intdiv($width, $moduleSize);3. 动态属性(8.2 起废弃)
class Order {}
$o = new Order();
$o->discount = 10; // Deprecated: Creation of dynamic property三种解法,按优先级:
// 方案一:声明属性(最推荐)
class Order {
public int $discount = 0;
}
// 方案二:确实需要动态属性,加注解
#[\AllowDynamicProperties]
class Order {}
// 方案三:用魔术方法自己接管
class Order {
private array $extra = [];
public function __get($name) { return $this->extra[$name] ?? null; }
public function __set($name, $value) { $this->extra[$name] = $value; }
}四、静默行为变化:不报错,但结果变了
1. 字符串与数字比较
这个坑很隐蔽。PHP 8 之前 0 == "abc" 是 true(因为 "abc" 被转成 0),8.0 起变成 false。
// PHP 7: true PHP 8: false
var_dump(0 == "abc");
// 如果你原来依赖 7.x 的行为,现在必须显式判断
if (is_numeric($input) && 0 == $input) { /* ... */ }同样受影响的还有 in_array() 和 array_search() 的非严格模式。老代码里到处是 in_array($x, $whitelist),升级后建议全部加 true:
in_array($value, ['a', 'b'], true);2. implode() 参数顺序
// 7.4 起就废弃了这种写法,8.0 移除
implode($array, ',');
// 正确
implode(',', $array);3. 排序算法稳定性
8.0 起 sort()、usort() 等变成稳定排序。这个变化是「好事」,但如果你的代码依赖了不稳定排序带来的副作用顺序,结果可能不同。
五、顺手用上新特性
升级不只是修兼容,8.x 有几个特性用上之后代码能短很多。
构造器属性提升
// 以前
class Point {
private float $x;
private float $y;
public function __construct(float $x, float $y) {
$this->x = $x;
$this->y = $y;
}
}
// 8.0+
class Point {
public function __construct(
private float $x,
private float $y,
) {}
}枚举
enum OrderStatus: string {
case Pending = 'pending';
case Paid = 'paid';
case Refunded = 'refunded';
public function label(): string {
return match ($this) {
self::Pending => '待支付',
self::Paid => '已支付',
self::Refunded => '已退款',
};
}
}
// 用法
$status = OrderStatus::from('paid');
echo $status->label();比一堆常量加 switch 清楚得多,而且 IDE 能补全。
match 表达式
// switch 要写一堆 break,match 直接返回值
$level = match (true) {
$score >= 90 => 'A',
$score >= 75 => 'B',
$score >= 60 => 'C',
default => 'D',
};注意 match 是严格比较(===),这点和 switch 的松散比较不同,迁移时要留意。
空安全操作符
// 以前
$city = null;
if ($user !== null && $user->getAddress() !== null) {
$city = $user->getAddress()->getCity();
}
// 8.0+
$city = $user?->getAddress()?->getCity();只读属性
class Config {
public function __construct(
public readonly string $dsn,
public readonly string $user,
) {}
}赋值一次后再写会抛 Error,比用 private + getter 省事。
六、生产环境一定要做的两件事
升级完成后,关掉错误回显。我在一个站点上就看到页面上直接打印着 Warning: A non-numeric value encountered in ... on line 83——这种页面既难看,也泄露了服务器绝对路径。
; .user.ini 或 php.ini
display_errors = Off
display_startup_errors = Off
log_errors = On
expose_php = Off第二件事:留一份能跑的老版本。升级过程中如果发现某个扩展在 8.3 下行为异常,能立刻切回去。宝塔这类面板可以直接在站点配置里切换 PHP 版本,动手前确认一下。
小结
升级 PHP 8 的难点不在语法,在于那些以前静默失败、现在开始报错的代码。它们本来就写得不对,只是 7.x 替你兜住了。
建议流程:
php -l全量扫一遍,先解决致命错误- 打开
E_ALL跑完整业务路径,收集所有 Deprecated - 逐条修根因,别用
@抑制 - 上线前关掉
display_errors,只留log_errors
按这个顺序走,半天到一天能搞定一个中等规模的老项目。


0 条评论