
Laravel 8.x 发布于 2020 年 9 月当时圈子里普遍有一种声音这是下一个大版本前的过渡版可以再等等。我们团队正好在那个时候做技术选型老项目在 7.x 上跑了半年多新项目又急着开工讨论纪要里甚至出现过“要不要直接扛到 9 出来”的想法。现在回头看这个判断大错特错。Laravel 8.x 不仅不是过渡反而把几乎每个开发日常都会用到的东西都往前推了一大步模型工厂改成了类、应用脚手架换成了 Jetstream/Breeze、迁移能压缩、队列能批处理、测试能控制时间。我后来在多个项目里把这些特性一个个用起来最大的感受是版本号会骗人手感不会。如果你现在正准备从 7.x 升级或者想直接在新项目里用 8.x这篇文章值得参考。我不打算逐条复读官方 release notes而是把实际项目中真正影响开发习惯、值得长期留存的特性拆开讲为什么这么设计、怎么用才不出坑、升级时最容易挂在哪。文章后面还会附一份我自己的避坑清单都是我实操过程中真实遇到过并解决掉的问题。1. 为什么 8.x 会被说成“过渡版本”实际却在悄悄改变日常开发1.1 “过渡版本”这种说法是怎么来的先理解时间线Laravel 官方从 9.0 才开始真正进入语义化版本节奏8.x 恰好处于一个新旧策略切换的节点上。所以在 8.0 刚发布的时候很多人在社区讨论里把它看成“用来过渡到 9 的中间站”。另一个原因是 8.0 同时推出了 Jetstream 和 Breeze 两套新脚手架官方示例项目不再是以前那种 Vue laravel/ui 的登录脚手架这让不少老用户产生了“又要重新学一遍前端”的错觉。但如果剥开表面看源码8.x 这一版做的事情非常实在PHP 7.3 起步并给 PHP 8.0 留好了兼容通道把模型工厂从闭包数组整体迁到类加入迁移压缩加入队列批处理重写速率限制在 Blade 层补上动态组件测试补上时间助手和“冻结时间”的官方玩法。这些不是花活是每个项目最后都会碰到的“毛细血管”问题。我的态度很明确如果新项目没有特殊的历史包袱完全可以直接落在 8.x 上如果老项目在 7.x 上升级成本也完全可控。省得等到 9.x 的时候新旧特性一起涌过来排查起来反而更痛苦。缓升级其实是把风险往后推而不是消掉风险。1.2 支持周期与决策参考选版本之前先看支持周期这应该是团队的基本素养。Laravel 8.x 的 bug 修复支持持续到 2022 年年中安全修复持续到 2023 年初。也就是说如果你今天还在维护一个 8.x 项目时间窗口已经比较紧张了建议尽快规划升级到新主版本但如果你的项目就准备在 8.x 上收尾、不再长期维护问题也不大。这里给一个简单的决策表情况我的建议全新项目无历史代码直接 8.x前端选 Breeze 起步后期按需加 Jetstream老项目在 7.x功能稳定升 8.x重点处理模型工厂和配置文件老项目还在 6.x 或更早先按官方升级指南 6-7-8 顺序走别跳版本项目长期维护、还要活两三年升 8.x 后尽快关注官方新主版本提前规划下一轮这样一列团队内部开会扯皮的时候就有依据了。第 7 章我还会给升级时容易踩的坑做完整清单。2. 模型工厂的全新语法从“写数组”到“写类”的迁移2.1 旧语法到底卡在哪Laravel 7 及之前的模型工厂是这样的一个 database/factories/UserFactory.php 文件里用一堆$factory-define(...)闭包把各模型的默认数据定义好。$factory-define(App\User::class, function (Faker $faker) { return [ name $faker-name, email $faker-unique()-safeEmail, ]; });闭包写法在小项目里很顺手但项目一变大就难受了。第一个问题是状态复用一段“已付费用户”的数据你可能要复制三遍闭包再改字段第二个问题是 IDE 补全几乎为零闭包返回的数组里写了什么字段全靠人和注释维护第三个问题是工厂之间的关系比如创建用户时自动创建所属团队、再创建团队邀请闭包写起来又绕又难读。Laravel 8 把工厂整体改成类正是为了把上面的问题从语言层面解决掉每个模型一个工厂类默认数据放在definition()状态是类方法关联关系可以用has()、for()这种链式方法表达。2.2 新工厂的实际写法新工厂类的骨架是这样的以用户为例namespace Database\Factories; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; class UserFactory extends Factory { protected $model User::class; public function definition() { return [ name $this-faker-name(), email $this-faker-unique()-safeEmail(), password bcrypt(password), ]; } public function premium(): self { return $this-state([ plan premium, ]); } }在测试和 Seeder 里的用法基本不变但表达力强了很多$user User::factory()-premium()-create(); $post Post::factory() -has(Comment::factory()-count(5)) -for($user) -create();需要注意一个小坑premium()这种状态方法的返回类型。如果你在 PHP 8.0 上写可以用static返回类型但 Laravel 8.x 官方要求 PHP 7.3很多生产服务器当时还在 7.4写static会在老环境下直接报语法或类型错误。我在一个项目里就因为这个在 CI 上挂了大半天后来统一改成self或干脆不写返回类型问题就消失了。这也算一个小教训写框架兼容代码时先看一眼项目最低 PHP 版本再决定语法糖。2.3 老工厂代码的升级路径从 7.x 升到 8.x如果项目里还留着一堆$factory-define(...)旧代码官方给了laravel/legacy-factories这个兼容包装上之后旧工厂还能跑。我能理解团队的“先能跑再说”心态但我的建议是趁升级窗口按模块逐个把旧工厂改成新类。我自己的经验是先改protected $model再把原来闭包的 return 数组原封不动挪进definition()然后把那些散落的“条件数据”整理成 state 方法最后跑一轮全部测试对比差异。这个过程中最容易翻车的是模型命名空间因为 Laravel 8 默认模型目录是app/Models而很多 7.x 老项目的默认模型在app根目录下。工厂里protected $model App\User::class这种旧引用如果没有同步更新为App\Models\User等 factory 真正生成类的时候报错信息又绕又难定位。建议升级时全局搜索App\User::class统一替换。3. Jetstream 与 Breeze脚手架选型决定了你后续的认证体系3.1 Jetstream功能全但别一时冲动Jetstream 是 Laravel 8 官方推出的新应用脚手架用来替代老旧的 laravel/ui。它的特点是“全家桶”登录、注册、邮箱验证、双因素认证、会话管理、API 令牌、团队管理统统内置。前端可以在 Livewire 和 Inertia 两个方向里选。安装命令大概是这样的以 8.x 时代文档为准composer require laravel/jetstream php artisan jetstream:install livewire # 或者 php artisan jetstream:install inertia --teams --apiJetstream 的后端认证逻辑主要来自 Laravel Fortify。你看生成出来的代码量就能明白它不是一个小工具包而是一整套账号体系。对团队来说定制空间确实大但反过来如果你只是想要一个登录注册页Jetstream 的生成文件里有一大堆你可能半年都不会碰的配置反而增加了新手的认知负担。我见过不少新人在 Jetstream 生成的代码里迷路半天找不到一个按钮在哪定义的。3.2 Breeze轻量到可以直接看懂Breeze 是同一个定位下的另一个极端默认生成 Blade Tailwind 的登录、注册、密码重置、邮箱验证页面代码量少读起来非常舒服。你需要做的额外工作也少改改 Blade 模板就行。后期版本里 Breeze 也提供了 Inertia Vue/React 的安装选项但我还是建议首版先用默认 Blade把认证逻辑跑通再考虑前端框架。composer require laravel/breeze --dev php artisan breeze:install装完之后你得到的是一套可读性极好的 auth 页面没有额外的双因素、团队等复杂概念。项目一开始就用 Breeze后面需要更复杂的账号能力时再单独引入 Fortify 或 Jetstream路线会清晰很多。3.3 选型对比与我的建议对比维度JetstreamBreeze核心功能全量认证 2FA 会话管理 Teams API登录/注册/找回密码代码量大组件多小一眼能看完前端技术栈Livewire 或 InertiaBlade Tailwind后期可选 Inertia学习曲线陡一点平缓适合场景需要团队管理/API token 的中大型项目大多业务项目起步我个人的实操结论是不要因为“Jetstream 更官方”就默认选它。很多项目其实只需要登录注册强行上 Jetstream 是在给自己造维护成本。如果后面真需要双因素或团队那时再补也不迟。选型这种事一开始能省则省后期需要再加远好过一开始就背上不需要的复杂度。4. 迁移压缩、队列批处理与维护模式让日常运维不用再将就4.1 迁移压缩几百个迁移文件不再是部署包袱项目维护久了migration 文件动辄几百个每次在新环境跑php artisan migrate都要把历史上所有迁移执行一遍。测试环境无所谓生产环境的发布窗口往往只有几分钟每次部署前建一个全新数据库等于让几百个迁移一个个跑性能损耗很直接。Laravel 8 给的方案是“压缩”先把当前数据库结构导成一个 schema 文件之后在全新数据库上执行迁移时先加载这个 schema 文件再只跑那些尚未记录的迁移。php artisan schema:dump # 生成 database/schema/mysql-schema.dump部署到新库时可以直接用php artisan migrate --squash它会先加载 dump 文件里的 schema再执行剩余迁移速度肉眼可见地变快。有一点我必须提醒schema dump 依赖数据库客户端工具mysqldump / pg_dump如果你的迁移里用了某个数据库特有的类型或函数dump 出来的结构未必在另一个数据库版本上完全兼容。我在异构数据库环境比如本地 MySQL 8、线上 MariaDB里遇到过utf8mb4排序规则不一致的问题。所以这个功能适合“数据库版本可控的部署链路”使用。4.2 队列批处理一批任务终于有官方的进度和结果管理在批处理功能出来之前我要给一批任务做“全部完成后发通知”只能自己写一套队列状态表子任务跑完一条更新一条再写个表查询判断是否都跑完了。麻烦不说中途有个任务失败整套状态逻辑经常要跟着打补丁。Laravel 8 的 Job Batching 直接把这件事做成了官方能力。用法很直白use Illuminate\Bus\Batch; use Illuminate\Support\Facades\Bus; Bus::batch([ new ProcessCsvChunk(1), new ProcessCsvChunk(2), new ProcessCsvChunk(3), ])-then(function (Batch $batch) { // 全部成功 })-catch(function (Batch $batch, Throwable $e) { // 其中有一个失败 })-finally(function (Batch $batch) { // 无论成败都会执行 })-dispatch();拿到的是 batch id前端可以一直查进度Bus::findBatch($batchId)-progress()。在 Job 内部只要实现Batchabletrait也能拿到当前 batch 实例方便在某个子任务失败时调用$this-batch()-cancel()把整个批次停下来。我实际用在一个 10 万行 CSV 导入的任务上。以前写状态表、查进度、处理重试前后大概多花了两个工作日换成 batch 之后进度条和失败通知都是现成的。重点提醒批处理要用 redis 或 database 这类支持持久化队列的驱动别用 sync。我在本地测试时图省事用 sync结果 batch 状态根本不可见排查半天才发现是驱动问题。提示批处理任务请使用 redis 或 database 队列驱动sync 驱动下 batch 状态不可靠。至少本地验证时也别用 sync 跑批量逻辑。sync 驱动适合单测和调试生产环境跑批量任务老老实实把队列驱动换成 redis/database。4.3 维护模式的 secret 绕过终于不用再跟 IP 白名单搏斗Laravel 8 把维护模式也改得顺手了。以前php artisan down之后想让特定的人看一眼“施工中”页面要配 IP 白名单公司网络一变化白名单就没法用了。8.x 起可以通过 secret 生成一个临时的“免维护”入口php artisan down --secret你的随机字符串然后访问/你的随机字符串Laravel 会给这个请求种一个特殊 cookie让这个浏览器之后可以看到站点真实内容。这个设计比 IP 白名单灵活太多了任何一台设备只要知道 secret 就能绕过维护模式不需要跟网管对齐 IP。这个特性虽然小但凡是负责过线上发布的人都会明白它值多少钱。以前发版本遇到数据库字段变更我要先把要放行的几个同事 IP 报给运维改了又改现在直接发一个 secret 链接在内部群里谁要看谁点。运维同事来问“这次 down 页面要开给谁”我直接把命令丢过去就行少了很多沟通成本。如果你在 8.x 上做发布建议把这个 secret 配合 CI/CD 环境变量统一管理而不是写死在代码里。5. 速率限制重构与动态 Blade 组件两个容易被低估的效率改进5.1 新速率限制从“数字中间件”变成“可命名的限流器”老 throttle 中间件的写法是throttle:60,1含义是“每分钟 60 次、最多尝试 1 分钟”。这在小 API 上够用但要做“登录接口同一个邮箱每分钟只能试 5 次”这种规则就显得很笨。Laravel 8 引入 RateLimiter 后限流规则变成了可以命名、可以组合的独立定义。use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Support\Facades\RateLimiter; RateLimiter::for(login, function (Request $request) { return Limit::perMinutes(5, 10)-by($request-input(email)); });然后在路由或控制器上直接引用Route::post(/login, [LoginController::class, login])-middleware(throttle:login);by()用来指定按什么维度区分用户可以配合$request-user()?-id ?: $request-ip()这种写法做“登录用户按 ID、游客按 IP”的混合限流。对登录保护、发送短信、支付回调这类场景非常实用。这里有个多节点部署的细节限流状态默认存在缓存里而 Laravel 8 默认缓存驱动是 file。如果生产环境是负载均衡部署file 缓存会造成每个节点各自记一套限流数实际效果约等于没有限流。所以用这个特性之前先把默认缓存驱动换成 redis 或 database并且保证所有节点连同一个后端。应用层的 429 响应是带Retry-After头的客户端能识别但如果你还在前面挂了 nginx也可以顺手配一层limit_req_zone做兜底防突发流量更稳。5.2 动态 Blade 组件渲染组件终于不用再写一堆条件分支新项目里我经常需要做“后台卡片式组件”比如根据配置决定某个位置渲染的是图表组件、表格组件还是富文本组件。Laravel 8 之前这个循环我得写一长串if / elseif或者用include按名字拼模板路径丑陋且难扩展。8.x 的动态组件指令把这件事变成了原地直接写foreach ($widgets as $widget) x-dynamic-component :component$widget[component] :data$widget[data] / endforeach:component传进去的只要是能解析的组件名Blade 就会像正常写x-xxx一样去渲染。这个功能让我那些“配置驱动 UI”的想法落地得非常干净后台配置里写死组件名前端循环出来就行新增组件时不需要动外层模板。有一点要提醒$widget[component]这个名字必须在运行时能被正确映射到真实的组件类或匿名组件。如果名字拼错报错经常是泛泛的 “Component not found”排查起来比较烦。我的习惯是给每一个动态组件做一条渲染测试确保配置里的每个组件名都能被解析出来这样线上配置出问题测试会先报。6. 时间测试助手给测试加上可靠的时间维度6.1 从手动冻结到 travel 系列业务里最让人头疼的一类逻辑就是时间判断订阅过期、限时活动、定时任务、账单提醒。以前写这类测试标准做法是手动Carbon::setTestNow()然后再手动恢复。一旦忘记恢复同一个测试进程里后面的测试全部中招时间错乱到怀疑人生。Laravel 8 给出的方案是更直白的测试助手直接“旅行到未来或过去”public function test_subscription_expired_before_dunning() { $this-travel(30)-days(); $user User::factory()-create([subscribed_at now()]); $this-assertTrue($user-subscription-isExpired()); }如果要跳到指定的绝对时间点用travelTo()$this-travelTo(now()-startOfMonth());测完记得travelBack()恢复时钟。我见过的常见翻车现场就是没在 tearDown 里统一恢复时间。所以我在团队里定的规范是所有用到travel/travelTo的测试要么在方法里明确调用travelBack要么在 base TestCase 的tearDown里兜底调用一次保证互不影响。这样 CI 就不会出现“单独跑全过、一起跑随机挂”的灵异事件。6.2 时间助手和 Pest 的搭配Laravel 8 时期社区测试工具里 Pest 的使用率涨得很快。Pest 不是 Laravel 内置框架它是 PHPUnit 之上的一个语法糖层但和 Laravel 的测试助手配合得很顺。你可以写出这样的测试it(可以验证三小时后的定时任务, function () { $this-travel(3)-hours(); $this-artisan(reports:daily)-assertExitCode(0); });当然Pest 要不要引入取决于团队习惯。如果你对 PHPUnit 的写法已经很熟练不引入也完全没问题但如果你团队里有大量写测试的新人Pest 那种it / describe / expect的自然语言风格确实能把测试门槛降低不少。这一节想表达的重点其实是Laravel 8 把时间维度的测试能力补完整了后续生态也跟了上来这是容易被很多人忽略但回报很高的一块。7. 从 7.x 升级到 8.x我实际踩过并解决掉的问题7.1 升级前 CheckList升主版本不是composer update一把梭我每次升级前都会过一遍下面的清单确认 PHP 版本 7.3优先用 7.4。项目里如果有 PHP 7.2 的机器先解决运行环境再谈框架升级。备份数据库和代码仓库分支升级过程不要直接在主干上操作。用composer update前先看一遍 composer.json 里有没有依赖框架特定版本的包提前排除冲突。对照官方升级指南里的 breaking changes 清单逐条过一遍代码库。php artisan config:cache之前把.env里的差异同步干净这个太容易踩。跑一遍完整测试套件把它作为升级完成的唯一通行证。7.2 三个真实踩坑案例坑 1模型命名空间变了工厂还在找旧类。这个上面已经提过是最普遍的一个。项目从 7.x 升上来App\User还在被老代码引用但新框架默认模型目录是App\Models。如果你的User模型文件已经挪到了app/Models工厂里没同步更新protected $model用User::factory()时就会报 “Class App\User not found”。我当时在代码里全局搜App\User::class搜出来二十多处统一改完才消停。坑 2Jetstream 生成的前端样式和旧 Bootstrap 样式打架。升级时如果顺手装 Jetstream 或 Breeze它们默认用 Tailwind。旧项目如果还保留着一堆 Bootstrap 类的 Blade 模板两套 CSS 同时加载页面布局会完全变乱。解决办法不是去调 Tailwind 的优先级而是先定一个明确的页面边界老模板继续用老样式新脚手架生成的页面独立路由不要混在一个布局里。等后续新一轮重构再统一。坑 3线上 config:cache 和本地 .env 不一致。这个坑和升级本身关系不大但升级后特别容易暴露。本地.env改了一堆变量忘了同步到线上然后升级步骤里又执行了php artisan config:cache于是线上所有配置瞬间变成旧的缓存内容数据库连接都跟着错。我的处理方法是升级部署脚本里把config:cache的执行放在配置校验后面并且 CI 里加一步冒烟检查确保缓存前配置能正常解析。7.3 升级完成后的验证建议升级完不是跑通首页就算完。我把验证拆成三层第一层是测试套件全绿第二层是把队列、调度器、日志、缓存在新版本下各起一次冒烟第三层是挑一条真实业务链路比如注册 - 发邮件 - 登录 - 导出走通。为什么拆三层因为很多 7.x 升级到 8 的问题不会立刻炸在首页上而会炸在队列 job 里、定时任务里或者某个被换掉的中间件上。多花半天把这三层摸一遍比上线后半夜被叫起来看日志划算得多。最后分享一个我个人坚持的小习惯每次升级主版本我都会在项目里维护一个UPGRADES_NOTES.md把这次改了什么、踩了什么坑、用了哪个特性都记下来。一年后团队再升级时这份笔记比任何官方文档都靠谱因为它是我们自己的真实路径。如果你的项目也准备从 7.x 往上走希望这条能帮到你。