Laravel 6での\Storage::disk('public')->url('')
のリンク切れの対処

Laravel 6でpublic diskからファイルの参照時にリンクがあっていない問題に遭遇したので対処方法を記す。
導入
Laravelのファイルストレージについては「File Storage」で説明されている。大きくprivateとpublicがある。
デフォルトのprivateの保存場所は [storage/app/xxx]
(xxx
はpublic
以外のディレクトリー) となっており,これはユーザーから見えないため,img
要素などのリンク先として指定できない。アプリケーション内でデータの中身を直接表示させる場合に使用する。
publicの保存場所は [storage/app/public/]
となっている。php artisan storage:link
コマンドを実行すると,public/storage
からstorage/app/public
にシンボリックリンクを貼ってくれるため,ユーザーからもアクセスできる。
このprivateとpublicの保存場所は,保存する場合は例えばstoreメソッドの第2引数やstoreAsメソッドの第3引数に'public'
をつけることで,デフォルトのprivate
からpublic
に変更できる。
今回はpublicに保存した画像ファイルの参照時にリンク切れを起こす問題に遭遇した。
publicの参照方法
public領域に保存されたファイルは実際には,http://localhost/storage/
以下のURLで参照される。
実際に参照する場合には2通りの方法がある。
url('storage')
\Storage::disk('public')->url('')
まず1点目は url('storage/')
で参照する方法だ。url
のヘルパー関数を使えば,.env
のAPP_URL
ごとに異なるドメイン部分を自動的に解決してくれる。ただし,この方法だとstorage
という固定文字列を指定する必要が出てくる。
実はこのstorage
という文字列はconfig/filesystems.php
のpublic->url
プロパティで以下のように決め打ちで指定されている。
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
こちらのstorageは,この設定ファイルを書き換えることで変更できる。しかし,参照時にstorageの文字列を直接書いてしまうと,config/filesystems.php
のpublic->url
の編集時にリンク切れを起こしてしまう。そのため,url('storage')
での参照はしないほうがいい。
そこで2点目の方法となる。こちらは\Storage::disk('public')->url('')
により,diskの種類を指定してからURLを参照している。これにより,storage
の固定文字列を使わずに済み,config/filesystems.php
のpublic->url
を編集しても影響を受けない。そのため,ファイルの参照時は常にこの2番目の方法を使うべきだろう。
\Storage::disk('public')->url('')
の問題
ただし,この方法にも問題がある。それは,ポート番号を反映できないという問題だ。Laravelのローカルの開発時では,Laravelの組み込みのWebサーバーをphp artisan serve
で起動して動作を確認することが多い。この際,デフォルトのポート番号は8000
となっており,アプリのURLはhttp://localhost:8000
となる。
この状態での\Storage::disk('public')->url('')
の返却値は,http://localhost/storage
となる。つまり,\Storage::disk('public')->url('')
でポート番号が反映できておらず,リンク切れが発生する。
この理由は先程掲載したconfig/filesystems.php
にある。public->url
の値をみると,'url' => env('APP_URL').'/storage',
となっている。つまり,APP_URLのドメインを参照している。肝心のAPP_URL
は.env
で定義されており,デフォルトではAPP_URL=http://localhost
となっている。.env
のAPP_URL
にポート番号がないのが原因となる。
ただ,url
ヘルパー関数だと問題なくポート番号も解決できている。それではと思い,config/filesystems.php
のenv('APP_URL').
をurl('').
に差し替えてみると,php artisan
の任意のコマンドを実行するとエラーが出る。
In UrlGenerator.php line 120: Argument 2 passed to Illuminate\Routing\UrlGenerator::__construct() must be an instance of Illuminate\Http\Request, null given, called in /home/senook en/project/product/qing/vendor/laravel/framework/src/Illuminate/Routing/Rou tingServiceProvider.php on line 68
調べたところ,Laravelのライフサイクルの都合でconfigファイルではurlヘルパー関数を使えないらしい (参考: Blog – Can’t use “url” helper function in config files in Laravel | BinBytes)。
対処方法
こうなると対策は以下の2通りとなる。
\Storage::disk('public')->url('')
のリンク切れの対処方法.env
でAPP_URL=http://localhost:8000
を指定。config/filesystems.php
のpublic->url
をenv('APP_URL').'/storage',
から'/storage',
に変更。
ソースコードを修正したくなければ,1の方法,修正してもよければ2の方法で対応する。
1点目は素直に,.env
のAPP_URL
にポート番号を指定するというものだ。これでローカルの開発環境は問題ない。ただ,これは開発環境をphp artisan serv
からApacheなど別の環境に移した場合に再設定が必要になる。ポート番号8000
はあくまでphp artisan serv
のデフォルトだからそうしているだけだからだ。そのため,個人的にはこの方法はよい方法と思わない。
2点目はconfig/filesystems.php
のenv('APP_URL')
を削除するというものだ。根本的な問題としては,ここでポート番号の存在しないAPP_URL
を参照しているのが問題だ。そこで,諸悪の根源のenv('APP_URL')
を削除する。こうすると,\Storage::disk('public')->url('')
の返却値は/storage
となる。先頭のドメイン部分が消えたルートパス表記となる。
実際のところ,アプリケーションは同一サーバーで実行することになるので,絶対パス指定だろうルートパス指定だろうがリンク先に違いはない。リンク切れになるくらいならばこちらのほうがよっぽどいい。
そもそもデフォルトでリンク切れになる挙動がおかしいと思ったので,Laravel本体にPull Requestを送ってみた ([6.x] Fix default disks public url for php artisan serve by senooken · Pull Request #5270 · laravel/laravel)。ただ,APP_URL
で指定しろと短いコメントが返ってきただけで,たいした説明もなく,即座にクローズされてしまった。
しかも他の人から「?」の反応までもらった。引き下がってコメントを追加しても何の返事もなく,印象が悪かった。他人のライブラリーを使っている自分が悪いのだが,Laravelに対する印象が悪くなるできごとだった。
ソースコード調査
このURLの生成について気になったのでメモを残す。
url
ヘルパー関数
デフォルトのurl
ヘルパー関数の処理を確認する。
vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
function url($path = null, $parameters = [], $secure = null)
{
if (is_null($path)) {
return app(UrlGenerator::class);
}
return app(UrlGenerator::class)->to($path, $parameters, $secure);
}
UrlGenerator#toを経由している。
vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.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::startsWith($root, 'http://') ? 'http://' : 'https://';
return preg_replace('~'.$start.'~', $scheme, $root, 1);
}
to
の中でformatRoot
があり,その中で$request->root()
がある。ここでRequest
のインスタンスからドメインを取得しているようだ。
\Storage::disk()->url()
Storage.disk
メソッドを定義しているStorage
クラスを確認する。
vendor/laravel/framework/src/Illuminate/Support/Facades/Storage.php
* @see \Illuminate\Filesystem\FilesystemManager
*/
class Storage extends Facade
{
このファイル内にdisk
メソッドの定義はなかったので,コメントにある通りFilesystemManager
を確認する。
vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemManager.php::disk
/**
* Get a filesystem instance.
*
* @param string|null $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function disk($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->disks[$name] = $this->get($name);
}
disk
ではFileSystem
インスタンスを返却している。
続いて,\Storage::disk()->url()
のFileSystem#url
メソッドを追いかける。
vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php
public function url($path)
{
$adapter = $this->driver->getAdapter();
...
} elseif ($adapter instanceof LocalAdapter) {
return $this->getLocalUrl($path);
} else {
getLocalUrl
メソッドを通っていた。
vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php
protected function getLocalUrl($path)
{
$config = $this->driver->getConfig();
// If an explicit base URL has been set on the disk configuration then we will use
// it as the base URL instead of the default path. This allows the developer to
// have full control over the base path for this filesystem's generated URLs.
if ($config->has('url')) {
return $this->concatPathToUrl($config->get('url'), $path);
}
getConfig
で取得したconfig
からurl
を参照している。
vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemManager.php
/**
* Get the filesystem connection configuration.
*
* @param string $name
* @return array
*/
protected function getConfig($name)
{
return $this->app['config']["filesystems.disks.{$name}"] ?: [];
}
ここがconfig/fileysstems.php
を参照している。
config/fileysstems.php
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
これで\Storage::disk('public')->url('')
がconfig/filesystems.php
を参照していることを特定できた。
結論
Laravel 6の\Storage::disk('public')->url('')
によるpublic diskへのアクセス時のリンク切れの問題について解説した。
ソースコードを追いかけたりPull Requestを出したり一つ一つ時間がかかった。だが,その分理解が深まった。
public diskへの参照時にまたリンク切れが発生した場合に参考にしたい。