☰
PHP7.4和8.3在Laravel框架里兼容性怎么样
2026/10/7 21:30:22 网站建设 项目流程

前言

"我们这个 Laravel 项目现在跑在 PHP 7.4 上,能不能直接换到 8.3?"这是很常见的一个问题,也最容易得到错误答案。错误答案通常来自两种经验:一种是"我们线上跑了没问题",另一种是"改个版本号就行"。前者不可复现,后者忽略了两个硬约束。

第一个硬约束来自 Laravel 本身:每个 Laravel 主版本都有明确的 PHP 最低版本要求,Laravel 8 支持 PHP 7.3 起,Laravel 10 要求 PHP 8.1 起,Laravel 11 要求 PHP 8.2 起。你的项目用的是哪个 Laravel,直接决定了能不能把 PHP 换成 8.3——不是改个配置能解决的。

第二个硬约束来自语言层:从 7.4 到 8.3 跨了四个小版本,期间有被移除的函数、有从警告变成错误的写法、有从"返回 false"变成"抛异常"(exception)的默认行为。这些改动里有相当一部分不会报错,只会静默改变结果,比如数字与字符串的比较规则、null传给内部函数的处理方式。

本文给出两件事:一张 Laravel 与 PHP 的对应关系表(用来判断"能不能升"),以及一分按版本归类的代码风险清单(用来判断"升了之后要改什么")。

一、先看硬约束:Laravel 版本与 PHP 版本

Laravel 版本PHP 最低要求能否运行在 PHP 8.3 上
Laravel 67.2.5不适合,目标环境是 7.2.5 ~ 7.4
Laravel 87.3官方范围内不含 8.3,需自行验证
Laravel 98.0.2可行,但仍建议升到更高主版本
Laravel 108.1可行
Laravel 118.2可行,是较稳妥的选择
Laravel 128.2可行

表中的最低版本以框架官方发布说明为准,补丁版本可能微调,迁移前请再核对一次你项目锁定的具体版本。

在 7.4 的项目上,结论是清楚的:PHP 7.4 到 8.3 这个跨度里,Laravel 也必须跟着升主版本。想升到 PHP 8.3,框架至少要达到能跑在 8.1 以上的版本,也就是 Laravel 10 起。PHP 7.4 的安全支持已经在 2022 年底结束,继续留在 7.4 上只是把风险往后推。

至于 PHP 8.3 本身:它在 2023 年底发布,属于还在安全维护期内的版本,支持周期临近结束(具体截止日期以官方支持时间表为准)。如果你的升级目标只是"跑起来没问题",8.3 是可用选项;如果考虑长期维护,直接规划到更新的版本更省事。

二、判断"能不能升"的两条命令

不要靠读composer.json猜,Composer 自己就能给出答案。

# 明确告诉我:是哪个包不让我用 PHP 8.3 composer why-not php 8.3 # 校验当前依赖集合对平台的实际要求 composer check-platform-reqs # 看看框架本身要求的 PHP 范围 composer show laravel/framework | grep -i requires -A 5

composer why-not php 8.3是最有价值的一条命令——它会把阻碍升级的包名和版本约束直接列出来,比人工翻几十个依赖快得多。

另外,项目的composer.json里通常会有两处需要改:

{ "require": { "php": "^8.1" }, "config": { "platform": { "php": "8.3.0" } } }

config.platform.php的作用是:即使你的本地开发机装的是 PHP 8.1,Composer 也会按 8.3.0 来解析依赖,避免"本地能装、服务器装不上"。这是一个能省掉大量沟通成本的设置。

三、7.4 升到 8.3 会踩到的语言层变更

下面这张表按"症状是否明显"排列,越靠下越隐蔽。

变更引入版本症状危险性
未定义常量变成 Error8.0脚本直接中断明显
@不再抑制致命错误8.0以前被压住的错误全部暴露明显
PDO 默认错误模式改为抛异常8.0查询失败从 false 变成未捕获异常明显
mysqli默认报错模式改为抛异常8.1老数据库代码直接抛异常明显
传 null 给非空内部函数参数被弃用8.1大量弃用日志中等
整体写入$GLOBALS被禁止8.1直接报错明显
动态属性被弃用8.2自定义类上写新属性会刷弃用日志中等
utf8_encode/utf8_decode被弃用8.2弃用日志中等
无参调用get_class()被弃用8.3弃用日志低
数字与字符串比较规则变更8.0不报错,结果变了高

3.1 传 null 给内部函数:Laravel 项目里最高发的一条

这一条特别值得单列,因为 Laravel 的取值方式天然会产出null:

<?php // 需要 PHP 8.0+(用了 ?->)与 Laravel 的请求对象 $name = $request->input('name'); // 字段不存在时返回 null // ❌ PHP 8.1 起:Deprecated: strlen(): Passing null to parameter #1 of type string $len = strlen($name); // ✅ 显式给默认值,或先做类型转换 $len = strlen((string) $request->input('name', ''));

同样的模式会出现在strlen()、strpos()、htmlspecialchars()、trim()、number_format()等一大批内部函数上。弃用提示本身不影响功能,但两个问题会随之而来:一是如果框架或监控把E_DEPRECATED当成异常处理,请求会直接 500;二是这些日志会淹没真正需要关注的错误。所以升级时应当把它当成必改项,而不是"以后再清理的技术债"。

3.2 实现内部接口的类需要补返回类型

PHP 8.1 给一批内部接口(如ArrayAccess、Countable、Iterator、IteratorAggregate)加上了"暂定返回类型"(tentative return types)。自己写的类如果实现了这些接口却没声明返回类型,升级后会出现弃用提示。老项目里自定义集合类、配置类最容易中招:

<?php // 需要 PHP 8.0+ class ConfigBag implements ArrayAccess, Countable { private array $items = []; // ❌ PHP 8.1 起产生弃用提示:没有声明与接口一致的返回类型 // public function offsetExists($offset) { return isset($this->items[$offset]); } // ✅ 声明返回类型 public function offsetExists(mixed $offset): bool { return isset($this->items[$offset]); } public function offsetGet(mixed $offset): mixed { return $this->items[$offset] ?? null; } public function offsetSet(mixed $offset, mixed $value): void { $this->items[$offset] = $value; } public function offsetUnset(mixed $offset): void { unset($this->items[$offset]); } public function count(): int { return count($this->items); } }

如果因为兼容多个版本的原因暂时没法加返回类型,可以在方法上挂#[\ReturnTypeWillChange]属性(PHP 8.1 引入)来消除提示,但更彻底的做法还是补上真实类型。注意mixed这个类型是 PHP 8.0 引入的。

3.3 动态属性被弃用:Eloquent 不受影响,自定义类会中招

PHP 8.2 起,给未声明的属性赋值会产生弃用提示。很多人第一反应是"那 Eloquent 模型不都要炸了吗"——不会。Eloquent 模型通过__get、__set、__isset这些魔术方法处理字段访问,走的是魔术方法路径,不触发动态属性检查。真正会中招的是自己写的、没有魔术方法的数据类:

<?php // 需要 PHP 8.0+ class UserDto { public int $id; } $dto = new UserDto(); $dto->id = 1; // ❌ PHP 8.2 起:Deprecated: Creation of dynamic property UserDto::$name is deprecated // $dto->name = 'tom'; // ✅ 方案一:在类里把属性显式声明出来,赋值就不会再有提示 class UserDtoV2 { public int $id = 0; public string $name = ''; } $dto2 = new UserDtoV2(); $dto2->name = 'tom'; // ✅ 方案二:确实需要动态属性时,显式声明意图(PHP 8.2 引入的属性) #[\AllowDynamicProperties] class LegacyContainer { }

判断方法很简单:跑一遍全量回归,在日志里搜Deprecated和dynamic property,命中的类逐个处理。

3.4 数字与字符串比较:不报错的那一类

这条之所以排在危险性最高,是因为它完全静默:

<?php var_dump(0 == "abc"); // PHP 7.4:true PHP 8.0 起:false var_dump("1" == "01"); // 两个版本都是 true(都是数字字符串)

Laravel 项目里常见的写法是if ($request->input('type') == 0)、in_array($id, $arrayOfStrings)。在 7.4 上"恰好能进"的分支,升级后进不去了,而且没有任何报错。迁移时把这类比较逐个改成严格比较,是最省事的做法。

代码实战:一份兼容性自检脚本

下面这个脚本面向 Laravel 项目,用 Artisan 命令的方式把"当前环境"和"代码里的风险点"一起报出来。保存为app/Console/Commands/CompatCheck.php,然后执行php artisan compat:check。

<?php // 需要 PHP 8.1+ 与 Laravel 10+ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use FilesystemIterator; class CompatCheck extends Command { protected $signature = 'compat:check {--path=app : 要扫描的目录}'; protected $description = '检查运行环境与代码中的 PHP 版本兼容风险'; public function handle(): int { $this->info('== 运行环境 =='); $this->line('PHP 版本 : ' . PHP_VERSION); $this->line('Laravel 版本 : ' . app()->version()); $this->line('error_reporting : ' . error_reporting()); $this->newLine(); $this->info('== 数据库与连接 =='); $row = DB::selectOne('SELECT VERSION() AS v'); $this->line('MySQL 版本 : ' . ($row->v ?? '未知')); // 连接是否可用,能跑通说明 PDO 的异常模式没把请求打断 try { DB::select('SELECT 1'); $this->line('数据库连通 : OK'); } catch (\Throwable $e) { $this->error('数据库连通 : 失败 —— ' . $e->getMessage()); } $this->newLine(); $this->info('== 代码风险扫描 =='); $patterns = [ '空值传入内部函数(8.1 弃用)' => '/\b(strlen|strpos|trim|htmlspecialchars|number_format)\s*\(\s*\$/', '宽松比较(8.0 规则变更)' => '/[^=!<>]==(?!=)\s*[^=]/', ]; $root = base_path($this->option('path')); $hits = 0; $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS) ); foreach ($it as $file) { if ($file->getExtension() !== 'php') { continue; } $lines = file($file->getPathname(), FILE_IGNORE_NEW_LINES); foreach ($lines as $no => $line) { foreach ($patterns as $label => $re) { if (preg_match($re, $line)) { $hits++; $this->line(sprintf( '[%s] %s:%d %s', $label, $file->getPathname(), $no + 1, trim($line) )); } } } } $this->newLine(); $this->line("共命中 {$hits} 处,请逐条确认是否为真的风险点。"); return self::SUCCESS; } }

要注意脚本里的模式匹配只是初筛,宽松比较那一条尤其容易误报,命中的行必须人工确认。真正可信的信号来自运行期:把error_reporting设为E_ALL、打开log_errors,在预发环境跑一轮完整的业务回归,然后按错误类型统计日志。

常见坑点

1. 只看框架能装,不看依赖能不能装


  • ❌composer update报错就手动改composer.lock绕过平台检查

  • ✅ 先跑composer why-not php 8.3,把阻碍升级的包逐个解决;绕过平台检查装上去,运行时照样出问题


2. 把 Deprecated 当成"不影响"


  • ❌ 升级后页面上有弃用提示,觉得无所谓先上线

  • ✅ 弃用提示意味着"下个大版本会变成错误"。而且如果监控把E_DEPRECATED当异常,请求会直接 500


3. 只测功能,不测日志


  • ❌ 手工点几个页面都正常,就当升级完成

  • ✅ 跑全量接口与后台任务,统计日志里的Deprecated、Warning、Fatal error,尤其是定时任务和队列消费者往往没人点


4. 忽略数字与字符串比较


  • ❌ 参数校验里的==全部保留

  • ✅ 升级前统一改成===或显式类型转换。这条不报错,只能在测试用例里发现


5. 自定义类实现内部接口不声明返回类型


  • ❌ 升级到 8.1 后日志里全是 tentative return type 的弃用提示,靠逐个忽略过日子

  • ✅ 补上返回类型;短期可用#[\ReturnTypeWillChange]压制,但这不是长期方案


6. 本地与服务器 PHP 小版本不同


  • ❌ 开发机 8.3.1,服务器 8.3.0,测试结论不可迁移

  • ✅ 用config.platform.php固定解析基准,并保证php -m的扩展清单在两端一致


7. 升级期间同时改业务代码


  • ❌ 一边升 Laravel 主版本,一边顺手重构业务模块,出问题无法二分定位

  • ✅ 把升级拆成"框架与依赖升级"和"业务改造"两步,各自独立提交、独立回归


8. 忘记队列与计划任务


  • ❌ Web 请求全部正常,queue:work因为 PHP 版本升级抛异常,任务 silently 失败

  • ✅ 升级后手动触发一次队列消费和schedule:run,并检查failed_jobs表


总结

问题结论
Laravel 8 能跑 PHP 8.3 吗官方要求是 PHP 7.3 起,8.3 需要自行验证,稳妥做法是升到 Laravel 10 及以上
升到 PHP 8.3 的前提框架版本必须满足最低要求,依赖包也必须支持,用composer why-not php 8.3确认
最主要的代码改动空值传内部函数、实现内部接口要补返回类型、宽松比较改严格比较、动态属性显式声明
最危险的一类变更不报错的那种,比如0 == "abc"的比较规则
怎么验证预发环境跑全量回归,统计日志里的 Deprecated 与 Fatal error,别只看页面能不能打开

兼容性问题的答案从来不是"能"或"不能",而是"要改多少、改在哪里"。判断路径可以固定下来:先用composer why-not php 8.3解决依赖层的硬约束,再用错误日志把语言层的问题一个个捞出来。7.4 到 8.3 的跨度里真正危险的不是那些会报错的地方——报错至少让你知道它在哪——而是那些安静改变行为的规则,它们只会在某个业务场景里以"数据不对"的形式出现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询