Laravel模型注入先于中间件执行的问题及解决方案咨询
嘿,你的核心问题其实是Laravel路由模型绑定的执行时机早于品牌中间件,导致retrieved观察者触发时,mailings磁盘还没完成配置。咱们先聊聊你那个AppServiceProvider方案的问题,再给你几个更稳妥的处理方式。
你的AppServiceProvider方案可行吗?
这个方案能临时凑效,但会埋下不少坑:
- 首先,
boot阶段调用中间件逻辑脱离了HTTP请求的正常生命周期。request()在服务提供者启动时不一定能拿到完整的请求参数(比如brand.code_name如果来自域名、路由参数或者请求头,这时候可能还没被解析出来),很容易拿错值或者拿不到值。 - 其次,这种写法把中间件的逻辑硬耦合到了服务提供者里,违反了单一职责原则。以后中间件要改逻辑,你还得同步改服务提供者,维护起来特别麻烦。
- 最后,这个逻辑会在所有请求场景(包括CLI命令、队列任务)里执行,而这些场景根本没有HTTP请求或者品牌信息,很容易触发莫名其妙的错误。
所以这个方案绝对不是正确的处理方式,不建议用。
正确的处理方式:从生命周期或加载时机入手
方案1:让品牌中间件优先于路由绑定执行
Laravel的路由模型绑定是由SubstituteBindings中间件负责的,你只需要把你的CheckBrandHost中间件放在它前面执行就行。
具体操作:
- 打开
app/Http/Kernel.php,把你的品牌中间件移到web中间件组的最开头:
protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\CheckBrandHost::class, // 优先执行 \App\Http\Middleware\EncryptCookies::class, // ...其他中间件 \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ];
如果是单独的路由,也可以显式指定中间件顺序:
Route::post('/mailings/{mailing}/send', [MailingCrudController::class, 'send']) ->middleware([CheckBrandHost::class, SubstituteBindings::class]);
这样品牌配置会在模型注入前完成,retrieved观察者就能拿到正确的磁盘配置了。
方案2:用访问器延迟加载content属性
如果调整中间件优先级有困难(比如项目结构复杂,动中间件组影响其他功能),可以把content改成访问器,只在真正需要的时候才从磁盘读取:
class Mailing extends Model { // 如果数据库里没有content字段,不需要加这个,否则隐藏原字段 protected $hidden = ['content']; // 定义content的访问器 public function getContentAttribute() { return \Illuminate\Support\Facades\Storage::disk('mailings') ->get("{$this->id}-{$this->slug}.html"); } }
然后删掉retrieved观察者里的逻辑就行。这样content只会在你真正调用$mailing->content的时候才会去读磁盘,这时候中间件已经执行完毕,磁盘配置肯定是对的。
这种方式更灵活,还能避免每次查询模型都强制读磁盘的开销(比如列表页不需要显示content的时候,就不会白读一次文件)。
方案3:手动获取模型(备选)
如果以上两种都不适用,还可以在控制器里手动查询模型,跳过自动的路由绑定:
public function send(Request $request) { // 这时候中间件已经执行完了,品牌配置没问题 $mailing = \App\Mailing::findOrFail($request->route('mailing')); // 后续逻辑... }
不过这种方式会失去路由模型绑定的便利性,比如自动404、权限验证的集成,所以只建议作为最后的备选。
总结
优先选方案1(调整中间件优先级),最符合Laravel的生命周期设计;如果场景特殊,**方案2(访问器延迟加载)**是更优雅的替代方案。你之前的服务提供者方案虽然能临时解决问题,但维护成本太高,容易出问题,别用。
内容的提问来源于stack exchange,提问作者splig

