Help us understand the problem. What is going on with this article?

なぜテストでWithoutMiddlewareするとRoute Model Bindingが無効化されるのか確認してみた

はじめに

Laravel Advent Calendar 2019 - Qiita の 3日目 の記事です。

今回は、Route Model Bindingしているコントローラに関連したテストが落ちる現象で小一時間ハマったのでそのことを記事にしてみました。

Route Model Bindingとは

突然ですが、getパラメータでユーザーからIDを受けとった後、該当するユーザーのEmailを返す例だとこんな感じになるかと思います。

// Route Model Bindingなしバージョン
Route::get('api/users/{id}', function (int $id) {
    $user = Entity\User::find($id);
    return $user->email;
});

Route Model Binding使って書くとこんな感じにかけます。

// Route Model Bindingありバージョン
Route::get('api/users/{user}', function (Entity\User $user) {
    return $user->email;
});

コントローラーの中で該当するユーザーのモデルを取得する処理を書かなくていいのは便利ですよね! :)

公式の説明はこちらになります。
https://laravel.com/docs/6.x/routing#route-model-binding

解決方法

テストコードの中で use WithoutMiddleware が記述されているとMiddlewareが全て無効化されるのですが、そのせいでRoute Model Bindingの機能も無効化されているようでした。下記のように無効化するMiddlewareを絞れば解決しました。

$this->withoutMiddleware([VerifyCsrfToken::class, YourAwesomeMiddleware::class]);

こちらの解決方法は以前にも @gomaaa さんがQiitaにまとめてくださっているので詳しくはそちらをご覧ください。

Route Model Bindingしているルートに対してテストするときのコツ

せっかくなのでちょっとだけ深堀りしてみた

どのmiddlewareがRoute Model Bindingの役割を担っているのか気になったので調べてみました。

Laravelのバージョンはv6.5.1です。

1. /app/Http/Kernel.phpを確認したところ$routeMiddleware の配列内に下記の記述を発見

'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,

こちらはLaravel5.3で追加されたRoute Model Bindingの機能を担うMiddlewareとのこと。こちらのリンクを参考にさせていただきました。[Laravel]ミドルウェアを整理してLaravelを軽くする - Qiita

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $this->router->substituteBindings($route = $request->route());

        $this->router->substituteImplicitBindings($route);

        return $next($request);
    }

どうも substituteImplicitBindings() メソッドが怪しい...

2.コードを追っていくと Illuminate\Routing\Router.phpsubstituteImplicitBindingsの実際のコードを発見

    /**
     * Substitute the implicit Eloquent model bindings for the route.
     *
     * @param  \Illuminate\Routing\Route  $route
     * @return void
     *
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
     */
    public function substituteImplicitBindings($route)
    {
        ImplicitRouteBinding::resolveForRoute($this->container, $route);
    }

resolveForRoute()の中身

    /**
     * Resolve the implicit route bindings for the given route.
     *
     * @param  \Illuminate\Container\Container  $container
     * @param  \Illuminate\Routing\Route  $route
     * @return void
     *
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
     */
    public static function resolveForRoute($container, $route)
    {
        $parameters = $route->parameters();

        foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
            if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
                continue;Container.php
            }

            $parameterValue = $parameters[$parameterName];

            if ($parameterValue instanceof UrlRoutable) {
                continue;
            }

            $instance = $container->make($parameter->getClass()->name);

            if (! $model = $instance->resolveRouteBinding($parameterValue)) {
                throw (new ModelNotFoundException)->setModel(get_class($instance), [$parameterValue]);
            }

            $route->setParameter($parameterName, $model);
        }
    }

$instance = $container->make($parameter->getClass()->name); でLaravelのDIの仕組みを使ってEloquentのモデルを取得した後、

Illuminate/Database/Eloquent/Model.phpresolveRouteBinding()を実行して実際のデータを取得していたのです。

    /**
     * Retrieve the model for a bound value.
     *
     * @param  mixed  $value
     * @return \Illuminate\Database\Eloquent\Model|null
     */
    public function resolveRouteBinding($value)
    {
        return $this->where($this->getRouteKeyName(), $value)->first();
    }

まとめと反省

  • テストでMiddlewareの処理を無効にしたいときは無効にしたいMiddlewareだけを無効にするのが良さそう
  • 助けてもらいながらコードを読んだので今度は一人で読めるようになる

おわりに

最後まで読んでくださりありがとうございました!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした