Laravel で生成される URL のルートを強制的に指定する方法

こんにちはー!むちょこです。

今日は Laravel の route() ヘルパなどで自動生成される URL のルート(*)を強制的に指定する方法をご紹介します。リバースプロキシを使っているときなどに便利です♪

*) アプリケーションのトップページにあたる URL

手順だけ知りたい方は目次から飛んでください。

環境

この記事は Laravel 10.x で検証しながら書いています。

他の下位バージョンでもおそらく同様の手順で指定できると思いますが、未検証です。

Laravel の仕組み

まずは Laravel の中身がどうなっているのをざっくりとご紹介していきます。

Laravel の route() ヘルパをはじめとした URL 自動生成機能は、そのままだとサーバ変数を利用した値が使用されます。そのため、ブラウザのアドレスバーに表示されているルートと違う値でリンクが生成されることがあります。

route(‘/login’) でブラウザのアドレスバーと違うルートで生成される例

ルートの取得

\Illuminate\Routing\UrlGenerator クラスの formatRoot() でルート URL が生成されています。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
    /**
     * Get the base URL for the request.
     *
     * @param  string  $scheme
     * @param  string|null  $root
     * @return string
     */
    
    public function formatRoot($scheme, $root = null)
    {
        if (is_null($root)) {
            if (is_null($this->cachedRoot)) {
                $this->cachedRoot = $this->forcedRoot ?: $this->request->root();
            }

            $root = $this->cachedRoot;
        }

        $start = str_starts_with($root, 'http://') ? 'http://' : 'https://';

        return preg_replace('~'.$start.'~', $scheme, $root, 1);
    }

ここで $this->forcedRoot (強制ルート)が指定されていなければ $this->request->root() の値が使用されます。

$this->request->root() の値は、\Symfony\Component\HttpFoundation\Request クラスの getSchemeAndHttpHost() からサーバ変数を元に取得しています。

強制ルートの設定

$this->forcedRoot の値は、同クラスの forceRootUrl() の第一引数に任意の値を渡すことで設定可能です。

vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
    /**
     * Set the forced root URL.
     *
     * @param  string|null  $root
     * @return void
     */
    public function forceRootUrl($root)
    {
        $this->forcedRoot = $root ? rtrim($root, '/') : null;

        $this->cachedRoot = null;
    }

また、 \Illuminate\Routing\UrlGenerator クラスはファサードである \Illuminate\Support\Facades\URL クラスを通して手軽に利用することができます。

vendor/laravel/framework/src/Illuminate/Support/Facades/URL.php
/**
 * 
 * @method static void forceRootUrl(string|null $root)
 *
 * @see \Illuminate\Routing\UrlGenerator
 */
 class URL extends Facade
 {
     /**
      * Get the registered name of the component.
      *
      * @return string
      */
     protected static function getFacadeAccessor()
     {
         return 'url';
     }
 }

手順

なんとなく仕組みが分かったところで、実際に forceRootUrl() を使って .env に設定した APP_URL の値を強制ルートにしていきます。

サービスプロバイダの実装

まずは artisan コマンドで URL に関するサービスプロバイダクラスを作成します。

コマンド
$ ./vendor/bin/sail php artisan make:provider URLServiceProvider

   INFO  Provider [app/Providers/URLServiceProvider.php] created successfully.

次に、生成されたクラスで forceRootUrl() の引数に APP_URL の値を渡します。

APP_URL の値は env(‘APP_URL’) でも呼び出せますが、 config() を使ったほうが拡張性が高くてオススメです。

app/Providers/URLServiceProvider.php
public function boot(): void
{
    \Illuminate\Support\Facades\URL::forceRootUrl(config('app.url'));
}

最後に、 config/app.php の providers キーに URLServiceProvider クラスを追加したら実装完了です!

config/app.php
'providers' => [
     App\Providers\UrlServiceProvider::class,
],
MEMO

サービスプロバイダについての詳細はこちらをご覧ください。

https://laravel.com/docs/10.x/providers

テスト

上記で実装した内容のテストコードは、簡易的ですがこんな感じで書きました。

まずは artisan コマンドでテストクラスを作成。

コマンド
$ ./vendor/bin/sail php artisan make:test URLServiceProviderTest

   INFO  Test [tests/Feature/URLServiceProviderTest.php] created successfully.

route() で生成された URL と APP_URL で作成した URL が一致することを確認するコードを実装。

tests/Feature/URLServiceProviderTest.php
    /**
     * route() で生成された URL と APP_URL を使って作成した URL が同じ = URL Root は APP_URL と同じ値
     *
     * @return void
     */
    public function test_url_root_is_the_same_value_as_app_url(): void
    {
        // \Illuminate\Routing\UrlGenerator::forceRootUrl() と同様の加工を行う
        $app_url =  rtrim(env('APP_URL'), '/');

        $this->assertSame(route('login'), "{$app_url}/login");
    }

テスト実行。

コマンド
$ ./vendor/bin/sail php artisan test tests/Feature/URLServiceProviderTest.php

   PASS  Tests\Feature\URLServiceProviderTest
  ✓ url root is the same value as app url                                                                                                                                             0.15s

  Tests:    1 passed (1 assertions)
  Duration: 0.20s

無事テスト通りました!これで完成です!

MEMO

はじめて PHPUnit を触る方は、よかったらこちらの記事もご覧ください。

Laravel ではじめる PHPUnit 入門