LaravelのSubstituteBindingsのコードリーディングメモ。
まずは暗黙的なバインディング・明示的なバインディングの以下のようなシンプルなパターンで見ていきます。
Route::get('/hoges/{hoge}', function (Request $request, Hoge $hoge) {
return view('welcome');
});
Route::get('/foos/{foo}', function (Request $request, $foo) {
return view('welcome');
});
public function boot(): void
{
Route::bind('foo', function (string $value) {
return tap(new \stdClass(), function ($obj) use ($value) {
$obj->hello = $value;
});
});
}
Illuminate\Routing\Middleware\SubstituteBindings
でパラメータのバインドが行われます。
public function handle($request, Closure $next)
{
try {
$this->router->substituteBindings($route = $request->route());
$this->router->substituteImplicitBindings($route);
} catch (ModelNotFoundException $exception) {
if ($route->getMissing()) {
return $route->getMissing()($request, $exception);
}
throw $exception;
}
return $next($request);
}
明示的バインディング
まずは、明示的なバインディング(上記の foo
の方)を見ていきます。
Illuminate\Routing\Router::substituteBindings()
で明示的なバインドが行われます。
public function substituteBindings($route)
{
foreach ($route->parameters() as $key => $value) {
if (isset($this->binders[$key])) {
$route->setParameter($key, $this->performBinding($key, $value, $route));
}
}
return $route;
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/Router.php#L937-L946
\Illuminate\Routing\Route::parameters()
は { 'hoge' => 1 }
のようなarrayが入ります。
boot()
内で Illuminate\Routing\Router::bind()
を呼び出すと $binders
プロパティのキーに $binder
のClosureがセットされます。
public function bind($key, $binder)
{
$this->binders[str_replace('-', '_', $key)] = RouteBinding::forCallback(
$this->container, $binder
);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/Router.php#L1149-L1154
$binders
が存在する場合は \Illuminate\Routing\Route::setParameter()
を呼び出し、 performBinding()
で設定したClosureを呼び出して戻り値をセットしています。
public function setParameter($name, $value)
{
$this->parameters();
$this->parameters[$name] = $value;
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/Router.php#L989-L992 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/Route.php#L438-L443
暗黙的バインディング
暗黙的バインディングは Illuminate\Routing\Router::substituteImplicitBindings()
で行われます。
public function substituteImplicitBindings($route)
{
$default = fn () => ImplicitRouteBinding::resolveForRoute($this->container, $route);
return call_user_func(
$this->implicitBindingCallback ?? $default, $this->container, $route, $default
);
}
https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/Router.php#L957-L964
ImplicitRouteBinding::resolveForRoute()
が処理の根幹になりますが、長いので分割しつつ説明します。
$route->signatureParameters(['subClass' => UrlRoutable::class])
ではメソッドの引数の型から UrlRoutable クラスのサブクラスのパラメータを取得します。HogeはEloquentモデルを継承していて、EloquentモデルはUrlRoutableを継承しているので、Hogeのパラメータに対してループが回ります。
public static function resolveForRoute($container, $route)
{
$parameters = $route->parameters();
$route = static::resolveBackedEnumsForRoute($route, $parameters);
foreach ($route->signatureParameters(['subClass' => UrlRoutable::class]) as $parameter) {
if (! $parameterName = static::getParameterName($parameter->getName(), $parameters)) {
continue;
}
getParameterName()
でHogeパラメータの変数名 hoge
と同名のルートパラメータが定義されているかチェックします。今回の場合 /hoges/{hoge}
なので定義されている状態です。
$container->make()
でパラメータの型でインスタンス化します。
$parameterValue = $parameters[$parameterName];
if ($parameterValue instanceof UrlRoutable) {
continue;
}
$instance = $container->make(Reflector::getParameterClassName($parameter));
$parent = $route->parentOfParameter($parameterName);
Model::resolveRouteBinding()
が呼ばれモデルを取得し、ルートパラメータにセットします。
} elseif (! $model = $instance->{$routeBindingMethod}($parameterValue, $route->bindingFieldFor($parameterName))) {
throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
}
$route->setParameter($parameterName, $model);
resolveRouteBindingQuery
で主キーで検索して1番目のレコードを返します。
public function resolveRouteBindingQuery($query, $value, $field = null)
{
return $query->where($field ?? $this->getRouteKeyName(), $value);
}
親パラメータがある場合
以下のように2つバインドが入っていて、 scopeBindings
が定義されている場合は Hoge
の子レコードである Fuga
を取得しようとします。
Route::get('/hoges/{hoge}/fugas/{fuga}', function (Request $request, Hoge $hoge, Fuga $fuga) {
return view('welcome');
})->scopeBindings();
Illuminate\Routing\Route::parentOfParameter()
では親パラメータを取得します。今回の場合は Hoge
のデータが入ります。
$parent = $route->parentOfParameter($parameterName);
scopeBindings()
して $parent にデータが入っている場合は分岐を通り、子データの論理削除の設定の状況によっては Model::resolveSoftDeletableChildRouteBinding()
Model::resolveChildRouteBinding()
が呼ばれます。
if ($parent instanceof UrlRoutable &&
! $route->preventsScopedBindings() &&
($route->enforcesScopedBindings() || array_key_exists($parameterName, $route->bindingFields()))) {
$childRouteBindingMethod = $route->allowsTrashedBindings() && in_array(SoftDeletes::class, class_uses_recursive($instance))
? 'resolveSoftDeletableChildRouteBinding'
: 'resolveChildRouteBinding';
if (! $model = $parent->{$childRouteBindingMethod}(
$parameterName, $parameterValue, $route->bindingFieldFor($parameterName)
)) {
throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
}
論理削除の設定がない場合は resolveChildRouteBinding()
が呼ばれます。
public function resolveChildRouteBinding($childType, $value, $field)
{
return $this->resolveChildRouteBindingQuery($childType, $value, $field)->first();
}
protected function resolveChildRouteBindingQuery($childType, $value, $field)
{
$relationship = $this->{$this->childRouteBindingRelationshipName($childType)}();
$field = $field ?: $relationship->getRelated()->getRouteKeyName();
if ($relationship instanceof HasManyThrough ||
$relationship instanceof BelongsToMany) {
$field = $relationship->getRelated()->getTable().'.'.$field;
}
return $relationship instanceof Model
? $relationship->resolveRouteBindingQuery($relationship, $value, $field)
: $relationship->getRelated()->resolveRouteBindingQuery($relationship, $value, $field);
}
Relationに対するクエリが発行されるため、親モデルに紐づく子モデルの任意のIDのレコードが取得できます。
public function resolveRouteBindingQuery($query, $value, $field = null)
{
return $query->where($field ?? $this->getRouteKeyName(), $value);
}