🥽

Laravel x phpunitで名前付きのルートを利用する

2025/01/29に公開

概要

LaravelのmiddlewareでURI、メソッドの判定に名前付きのルートを利用しています。
phpunitを利用して該当のmiddlewareをテストするときに、名前付きのルートが見つからずにエラーとなってしまいました。
解決にあたって色々なヒントはあったものの、まとまっている記事はなかったので備忘として書き残しておきます。

参考リポジトリ

今回の事象を再現するリポジトリを作成しました。
https://github.com/merutin/laravel-phpunit-namaed-route
なお、Roo Code(Roo Cline)でほぼ実装しています。コミットだけ自分でしてしまいましたが、全部任せれば良かったと後で気が付きました。

結論

以下のように設定することで名前付きのルートを利用できるようになりました。
Requestのクラスに設定する必要があるので、共通化しにくいですが、利用する機会は少ないので仕方ないかなと思います。

$request = Request::create($uri, 'GET', $parameters);
$request->setRouteResolver(function () use ($name) {
    $routes = Route::getRoutes()->getRoutes();
    foreach ($routes as $route) {
        if($route->named($name)) {
            return $route;
        }
    }
    return;
});

詳細

バージョン

php:8.2
laravel:11.31
phpunit:11.0.1

middlewareで名前付きのルートをキーとして分岐させる処理を作成していました。
分岐した先の処理が正しく実行されることを確認するために、phpunitを作成したのですが、リクエストを作成しただけでは名前が認識されませんでした。

$request = Request::createFromBase(Request::create($uri, 'GET', $parameters));
$middleware->handle($request);

middlewareの処理イメージは以下のような感じです。

public function handle(Request $request, Closure $next)
{
    // ここを通ってほしい
    if ($request->route() && $request->route()->getName()) {
        $request->merge(['namedRoute' => $request->route()->getName()]);
        return $next($request);
    }
    throw new Exception("could not get named route");    
}

名前付きのルートでテストできるように、何が起こっているのかを調べることにしました。

Requestの詳細

Requestの中身を見てみると、getRouteResolverというメソッドが呼ばれており、その中で名前付きのルートを取得しているようです。

https://github.com/illuminate/http/blob/master/Request.php#L638-L647

getRouteResolver のもとになる値は外からSetterで値の設定ができるようになっていました。

https://github.com/illuminate/http/blob/master/Request.php#L711-L716
https://github.com/illuminate/http/blob/master/Request.php#L724-L729

そのため、作成したRequestでsetRouteResolverをよびだしてあげればよさそうです。
引数がClosureとなっていますが、 引数なしで \Illuminate\Routing\Routeを返すようになっていればよさそうです。
まずは意図した通りで動くかの確認のために、Routeを作成して動かしてみます。

$request->setRouteResolver(function () {
    return Route::get('/test', function () {})->name('test.route');
});
$middleware->handle($request);

意図したとおりに動くようになります。
最終的に利用したいのはLaravelですでに定義済みのRouteなので、setRouteResolverの中で既存のRouteを返してあげる必要があります。

$name = "sample.index";
$request = Request::create($uri, 'GET', $parameters);
$request->setRouteResolver(function () use ($name) {
    $routes = Route::getRoutes()->getRoutes();
    foreach ($routes as $route) {
        if($route->named($name)) {
            return $route;
        }
    }
    return;
});
$middleware->handle($request);

Routeの一覧自体は Route::getRoutes()->getRoutes() で取得できるのですが、setRouteResolverのメソッドは単一のRouteを返してあげる必要があるので、名前付きのルートと今回呼び出したい名前を一致させる必要があります

※ 最初のgetRoutesは \Illuminate\Routing\RouteCollectionInterface 、2回目のRouteは \Illuminate\Routing\Route[] を返します。

\Illuminate\Routing\Route はnamedというメソッドを持っており、名前付きのルートが一致するかどうかのチェックをしているため、これで呼び出したいRouteを指定できるようになります。

https://github.com/illuminate/routing/blob/master/Route.php#L911-L924

最後に

名前付きのルートにしたところで、簡単にテストできるだろうと思って調べてみたのですが、意外とうまくやる方法がなくて苦戦しました。
Laravelはちゃんと使えてない感じがあるので、もっといい解決方法があれば教えてください。

参考リンク

https://qiita.com/potimoto/items/374febb2625ab4f027c9
https://blog.shimabox.net/2020/05/24/laravel-testing_classes_that_depend_on_the_request_class/
https://zenn.dev/shlia/scraps/bb85c4eaeb7f02

DELTAテックブログ

Discussion