2021/06/24追記
現在は自動でテスト実行時にVerifyCsrfTokenをオフにしてくれるみたいなので、今からLaravel使い始める人はハマることはなさそうです。本記事は参考までに読んでもらえると助かります。
https://readouble.com/laravel/8.x/ja/csrf.html
はじめに
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.php
にsubstituteImplicitBindings
の実際のコードを発見
/**
* 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.php
の resolveRouteBinding()
を実行して実際のデータを取得していたのです。
/**
* 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だけを無効にするのが良さそう
- 助けてもらいながらコードを読んだので今度は一人で読めるようになる
おわりに
最後まで読んでくださりありがとうございました!