LoginSignup
14
8

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-12-03

2021/06/24追記

現在は自動でテスト実行時にVerifyCsrfTokenをオフにしてくれるみたいなので、今からLaravel使い始める人はハマることはなさそうです。本記事は参考までに読んでもらえると助かります。
https://readouble.com/laravel/8.x/ja/csrf.html

Screen Shot 2021-06-24 at 15.57.05.png

はじめに

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だけを無効にするのが良さそう
  • 助けてもらいながらコードを読んだので今度は一人で読めるようになる

おわりに

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

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8