Laravel で https でサイトを表示しているのに、route() ヘルパーが http 始まりの URL を返す

# 問題

サイト自体は https でアクセス出来ていて、.env の APP_URL にはちゃんと https 始まりの URL を設定している。
この状態で

```php
route('admin.home') // ---> http://localhost/admin
config('app.url') // ---> https://localhost
```

と、なぜか route() ヘルパーのときだけ http になってしまい、Mixed Content が発生して正常に動作しなくなる。

config:cache とか Laravel の cache 機能は使っていない。
一応以下コマンドでクリアしてみても結果は変わらない。
```sh
$ php artisan cache:clear && php artisan view:clear && php artisan config:clear
```

# route ヘルパーの処理を追って、何が原因で http 始まりの URL を返しているか調査

route() ヘルパーはどんなことをしていて、どこの情報を参照しているのか？を地道に追っていく。

### vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
```php
    function route($name, $parameters = [], $absolute = true)
    {
        return app('url')->route($name, $parameters, $absolute);
    }
```

app('url') は Illuminate/Routing/UrlGenerator を返す。

### vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
```php
    public function route($name, $parameters = [], $absolute = true)
    {
        if (! is_null($route = $this->routes->getByName($name))) {
            return $this->toRoute($route, $parameters, $absolute);
        }

        throw new RouteNotFoundException("Route [{$name}] not defined.");
    }

    public function toRoute($route, $parameters, $absolute)
    {
        $parameters = collect(Arr::wrap($parameters))->map(function ($value, $key) use ($route) {
            $value = $value instanceof UrlRoutable && $route->bindingFieldFor($key)
                    ? $value->{$route->bindingFieldFor($key)}
                    : $value;

            return function_exists('enum_exists') && $value instanceof BackedEnum
                ? $value->value
                : $value;
        })->all();

        return $this->routeUrl()->to(
            $route, $this->formatParameters($parameters), $absolute
        );
    }
```
$this->routeUrl() は Illuminate/Routing/RouteUrlGenerator を返す。

### vendor/laravel/framework/src/Illuminate/Routing/RouteUrlGenerator.php
```php
    public function to($route, $parameters = [], $absolute = false)
    {
        $domain = $this->getRouteDomain($route, $parameters);

        // First we will construct the entire URI including the root and query string. Once it
        // has been constructed, we'll make sure we don't have any missing parameters or we
        // will need to throw the exception to let the developers know one was not given.
        $uri = $this->addQueryString($this->url->format(
            $root = $this->replaceRootParameters($route, $domain, $parameters),
            $this->replaceRouteParameters($route->uri(), $parameters),
            $route
        ), $parameters);

        if (preg_match_all('/{(.*?)}/', $uri, $matchedMissingParameters)) {
            throw UrlGenerationException::forMissingParameters($route, $matchedMissingParameters[1]);
        }

        // Once we have ensured that there are no missing parameters in the URI we will encode
        // the URI and prepare it for returning to the developer. If the URI is supposed to
        // be absolute, we will return it as-is. Otherwise we will remove the URL's root.
        $uri = strtr(rawurlencode($uri), $this->dontEncode);

        if (! $absolute) {
            $uri = preg_replace('#^(//|[^/?])+#', '', $uri);

            if ($base = $this->request->getBaseUrl()) {
                $uri = preg_replace('#^'.$base.'#i', '', $uri);
            }

            return '/'.ltrim($uri, '/');
        }

        return $uri;
    }
```
$this->url は Illuminate/Routing/UrlGenerator を返す。

### vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php
dump() でインスタンスの情報を確認したときに、ここで初めて http で始まっているアプリケーション URL を見つけた。
こいつが原因ぽい。
```sh
^ Illuminate\Routing\UrlGenerator {#198 ▼
   : 
  #cachedRoot: "http://localhost"
```

以下で cachedRoot が設定される。
```php
    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->request->root() を追う。
```php
$this->forcedRoot // --> null
$this->request->root() // --> http://localhost/admin
```
$this->request->root() は Illuminate\Http\Request を返す。

### vendor/laravel/framework/src/Illuminate/Http/Request.php
```php
    public function root()
    {
        return rtrim($this->getSchemeAndHttpHost().$this->getBaseUrl(), '/');
    }
```
$this->getSchemeAndHttpHost() が http 始まりの URL だった。
親クラスの Symfony の Request クラス Symfony\Component\HttpFoundation\Request で定義されている。

### vendor/symfony/http-foundation/Request.php
```php
    public function getSchemeAndHttpHost(): string
    {
        return $this->getScheme().'://'.$this->getHttpHost();
    }
```
$this->getScheme() が http を返す。
```php
    public function getScheme(): string
    {
        return $this->isSecure() ? 'https' : 'http';
    }

    public function isSecure(): bool
    {
        if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) {
            return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true);
        }

        $https = $this->server->get('HTTPS');

        return !empty($https) && 'off' !== strtolower($https);
    }
```
isSecure の後半の return 側。
```
$https = $this->server->get('HTTPS');
```
の値を最終的に返却している。
```php
var_dump($_SERVER['HTTPS'] ?? 'undefined'); // --> undefined
```

$_SERVER['HTTPS'] が設定されていない。なんで？？

$_SERVER['HTTPS'] については [こちら](https://www.php.net/manual/ja/reserved.variables.server.php)
> 'HTTPS'
> スクリプトが HTTPS プロトコルを通じて実行されている場合に 空でない値が設定されます。

ちなみにこの現象が発生しているのは お名前.com のレンサバ。
ほかのレンサバや VPS, AWS とかでは $_SERVER['HTTPS'] はきっちり on などを返す。

レンサバ側の大本の設定がなにか悪さをしていそう。
$_SERVER['HTTPS'] が設定されない原因は問い合わせたところ、
「設定されないからこっちで対応して」
といったシンプルな回答だった。

ググったらわかったけど、ロードバランサーを介して https -> http にリバースプロキシしている環境だとあるあるの事象みたい。

# 対応

どっちでも好きな方で。

## A 案. .htaccess などで対応

- .htaccess

```sh
    # $_SERVER['HTTPS'] が設定されない環境向け
    # ロードバランサーを介して https -> http にリバースプロキシしている環境とか
    SetEnvIf X-Forwarded-Proto https HTTPS=on
```

## B 案. Laravel プログラム側で対応

- app/Providers/AppServiceProvider.php

```php
    public function boot()
    {
        // レンサバの設定と相性が悪く、https でアクセスしているにも関わらず $_SERVER['HTTPS] が設定されず、
        // Laravel, Symfony 側の処理で http 始まりの URL で処理されてしまう。
        // config('app.url') が https 始まりか？http 始まりか？ をもとに強制的に scheme を設定する
        \Illuminate\Support\Facades\URL::forceScheme(\Str::startsWith(config('app.url'), 'https') ? 'https' : 'http');
```