2024-06-25

LaravelのRouteパラメータのバインディングの仕組み

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);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/Middleware/SubstituteBindings.php#L39

明示的バインディング

まずは、明示的なバインディング(上記の 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);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L2135-L2138

親パラメータがある場合

以下のように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);

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/ImplicitRouteBinding.php#L43

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]);
    }

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Routing/ImplicitRouteBinding.php#L49-L60

論理削除の設定がない場合は resolveChildRouteBinding() が呼ばれます。

public function resolveChildRouteBinding($childType, $value, $field)
{
    return $this->resolveChildRouteBindingQuery($childType, $value, $field)->first();
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L2074-L2077

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);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L2100-L2114

Relationに対するクエリが発行されるため、親モデルに紐づく子モデルの任意のIDのレコードが取得できます。

public function resolveRouteBindingQuery($query, $value, $field = null)
{
    return $query->where($field ?? $this->getRouteKeyName(), $value);
}
このエントリーをはてなブックマークに追加