Laravel 9:如何通过路由传值确定控制器构造函数的依赖注入
问题描述
我有一个服务,负责给资源控制器的构造函数提供ActionContract实例,但实例的具体类型取决于当前访问的路由(对应不同的操作文件)。目前用switch语句根据路由和请求方法判断返回哪个合约实例,但想换成类似路由指定控制器的方式,直接从路由配置里绑定对应的合约类。
但Laravel 9会在加载中间件之前先执行控制器构造函数,所以中间件绑定的方式没法直接生效,需要找替代方案。
现有实现(switch版本):
// 路由定义 Route::apiResource('foo', App\Http\Controllers\FooController::class); Route::apiResource('bar', App\Http\Controllers\BarController::class); Route::apiResource('foo-bar', App\Http\Controllers\FooBarController::class); // 服务类逻辑 ... switch ($this->getResourceNameFromRoute()) { case 'foo': switch ($request->getMethod()) { case 'GET': return new FooShowActionContract(); case 'POST': return new FooUpdateActionContract(); // 其他方法分支 } case 'bar': // 对应bar的分支逻辑 ... } // FooController构造函数 public function __construct(?ActionContract $action = null) { $this->action = $action; }
理想思路(直接用中间件无效):
// 路由绑定中间件传递合约映射类 Route::apiResource('foo-bar', App\Http\Controllers\FooBarController::class)->middleware('actionMap:FooBarActionMap::class'); // 服务类解析合约 public function resolveContract() { return app()->make(ActionContract::class, ['request' => $request]); } // 控制器构造函数不变 public function __construct(?ActionContract $action = null) { $this->action = $action; } // 中间件尝试绑定,但因为构造函数先执行,无效 public function handle($request, $next, $actionClassName) { // 尝试绑定合约实例到容器,但控制器构造已经执行完了 }
优雅实现方案
方案1:路由参数绑定+服务解析
直接在路由定义中给每个资源方法指定对应合约类,通过路由参数传递,再由服务类解析实例化。
步骤1:修改路由定义
给每个资源的不同方法添加合约参数:
// foo资源路由绑定对应合约 Route::apiResource('foo', App\Http\Controllers\FooController::class) ->parameters([ 'index' => ['action' => FooIndexActionContract::class], 'show' => ['action' => FooShowActionContract::class], 'store' => ['action' => FooStoreActionContract::class], 'update' => ['action' => FooUpdateActionContract::class], 'destroy' => ['action' => FooDestroyActionContract::class], ]); // bar资源同理 Route::apiResource('bar', App\Http\Controllers\BarController::class) ->parameters([ 'index' => ['action' => BarIndexActionContract::class], // 其他方法对应合约 ]);
步骤2:服务类解析合约
从当前路由参数中获取合约类名,验证后实例化:
public function resolveContract(Request $request) { $actionClass = $request->route()->parameter('action'); if (class_exists($actionClass) && is_subclass_of($actionClass, ActionContract::class)) { return app()->make($actionClass); } // 兜底逻辑:抛出异常或返回默认合约 throw new \InvalidArgumentException('Invalid action contract specified for route'); }
步骤3:控制器注入服务
在构造函数中注入服务类,直接解析合约:
public function __construct(ActionContractResolver $resolver) { $this->action = $resolver->resolveContract(request()); }
方案2:自定义路由宏简化绑定
如果手动给每个资源写参数太繁琐,可自定义路由宏批量绑定资源与合约:
步骤1:定义路由宏
在App\Providers\RouteServiceProvider的boot方法中添加:
use Illuminate\Routing\Router; public function boot() { parent::boot(); Router::macro('apiResourceWithActions', function ($name, $controller, array $actionContracts) { return $this->apiResource($name, $controller)->parameters( collect(['index', 'show', 'store', 'update', 'destroy']) ->mapWithKeys(function ($method) use ($actionContracts) { return [$method => ['action' => $actionContracts[$method] ?? null]]; }) ->filter() ->toArray() ); }); }
步骤2:使用路由宏快速绑定
Route::apiResourceWithActions('foo', App\Http\Controllers\FooController::class, [ 'index' => FooIndexActionContract::class, 'show' => FooShowActionContract::class, // 其他方法对应合约 ]);
方案3:约定式路由名称解析
如果合约类命名有规律(如{Resource}{Method}ActionContract),可完全无需手动绑定,根据路由名称自动解析:
修改服务类逻辑
public function resolveContract(Request $request) { $routeName = $request->route()->getName(); // 拆分路由名称,如`foo.index`拆分为['foo', 'index'] [$resource, $method] = explode('.', $routeName); // 拼接合约类名(根据实际命名空间调整) $actionClass = "App\Contracts\Actions\\".ucfirst($resource).ucfirst($method)."ActionContract"; if (class_exists($actionClass) && is_subclass_of($actionClass, ActionContract::class)) { return app()->make($actionClass); } throw new \InvalidArgumentException("No action contract found for route: {$routeName}"); }
路由无需额外配置,只要合约类命名符合规则,就能自动匹配实例化。
方案4:容器上下文绑定(针对控制器)
如果不想在构造函数中处理,可利用Laravel容器的上下文绑定,给单个控制器的方法注入对应合约:
在App\Providers\AppServiceProvider的register方法中添加:
$this->app->when(FooController::class) ->needs(ActionContract::class) ->give(function ($app) { $routeName = $app->make(Request::class)->route()->getName(); switch ($routeName) { case 'foo.index': return new FooIndexActionContract(); case 'foo.show': return new FooShowActionContract(); // 其他分支 } });
这种方式仅针对单个控制器做映射,比全局switch更清晰。
内容的提问来源于stack exchange,提问作者Clayton Engle
相关产品推荐
相关产品推荐

