📝

[Laravel] アップロードファイルそのものへのアクセスを動的化する

2022/01/05に公開

Laravelにはストレージという機構があります。
ローカルのファイルもクラウドのファイルも一元的に取り扱うことができる便利な仕組みです。

今回はローカルに保存したファイルをHTML(bladeファイル)からアクセスしたいときはどうすればいいかを見ていきます。
HTMLで画像を表示させるには<img src="">タグを使います。
問題はsrc属性をどう記述するかです。
Laravelのファイルアップロード機構では通常はブラウザから直接アクセスできない場所にファイルが保存されます。

アプローチとしては以下が考えられます。

  1. ファイルの保存ディレクトリを公開領域に変更する
  2. 公開ディレクトリにシンボリックリンクを設定する
  3. src属性を動的化する

今回は3.を取り扱います。
オーバーヘッドは発生するものの、例えば認証されていない場合や非公開の情報へアクセスさせたくないなどのケースに対応できます。

前提となる仕様

  • お知らせ管理に登録している画像リソースに動的アクセスする。
  • お知らせ管理のモデル名は、Annoucementとする
  • Announcementモデルの対象テーブルはannouncementsとする
  • お知らせ管理に、ファイルアップロード項目を設定する
  • 管理項目に以下を設定する
    • 画像 (img_fname)※ファイル名を文字列で保存
    • 公開/非公開 (open)※0で非公開、 1で公開とします。
    • その他お知らせに使われる項目複数
  • 画像はstorage/app/announcement/に保存する

今回は管理画面からの登録についての説明は省略します。
テストデータとしてレコードの登録と画像の保存を行って、それにアクセスします。

準備

以下のディレクトリを作成して書き込み権限を与えます。

  • storage/app/announcement/

ルーティング設定

お知らせの画像にアクセスするためのルーティング設定を行います。
※今回は管理画面の構築やお知らせ詳細ページの表示は省略します。

// routes/web.php

Route::get('/announcement/file/{id}/{name_attr}', [Controllers\AnnouncementController::class, 'file'])->name('announcement.file');

例えばドメイン名/announcement/file/1/img_fnameにアクセスすると画像が表示される仕組みです。
idと画像保存先フィールド名を指定します。

モデル作成

artisanコマンドでモデルとマイグレーションファイルを作成します。

php artisan make:model Announcement -m

マイグレーションファイル追記

モデルを作った時点でマイグレーションファイルも作成されているので、これに追記していきます。
下記のように記述します。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAnnouncementsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('announcements', function (Blueprint $table) {
            $table->id();
            $table->tinyInteger('open')->default(0)->comment('公開/非公開');
            $table->text('publish')->nullable()->comment('日付');
            $table->text('title')->nullable()->comment('タイトル');
            $table->text('body')->nullable()->comment('本文');
            $table->text('img_fname')->nullable()->comment('画像ファイル名');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('announcements');
    }
}

open=1のときは公開データ、0のときは非公開データとします。
img_fnameには保存した画像のファイル名を保存します。
img_fname、open以外のフィールドは今回はなくても動作確認可能です。

お知らせ表示用コントローラーを作成し、その中に画像表示用メソッドを書く

お知らせ表示用のコントローラーを作成します。
その中に画像表示用のメソッドを作ります。
(通常は一覧表示用のメソッドと詳細ページのメソッドがあると思います。それとは別に画像表示用のメソッドを足すイメージです。)
※今回は画像表示の処理だけ記述します。

画像表示はルーティング、モデル、コントローラーの記述だけで動作可能です。
ビューは必要ありません。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests\AnnouncementRequest;
use Illuminate\Support\Facades\Storage;
use App\Models\Announcement;

class AnnouncementController extends Controller
{
    private $data;
    private $attrs;

    public const STORAGE_DIR = 'announcement';

       public function index()
    {
        // 一覧表示処理
    }
       
    public function show()
    {
        // 詳細表示処理
    }
    
    public function file($id, $name_attr)
    {
        $announcement = Announcement::where(['open' => 1])->findOrFail($id);

        $dir = self::STORAGE_DIR;
        $fname = $announcement->$name_attr;
        $path = $dir . '/' . $fname;
        $mimeType = Storage::mimeType($path);
        $response = response(Storage::get($path))->header('Content-Type', $mimeType);

        return $response;
    }
}

要点は下記です。

  • 条件を付けてモデル取得する
  • Storage::get()でファイルを取得
  • ビューオブジェクトではなく、レスポンスオブジェクトを返す

open=1でなければ(非公開のデータであれば)1件取得できないようになっています。
その後、Not Foundとなります。
このように条件によってアクセスをコントロールできます。

1件取得成功後は、設定したストレージディレクトリからStorage::get()メソッドで実ファイルを取得しにいきます。
最終的に画像リクエストへのレスポンスオブジェクトを返すようになっています。

テストデータの作成

動的化の処理自体はできているのですが、確認のためにテストデータを設定します。
レコードの登録と画像の保存を行います。

Seederを使ってレコードを登録

database/seeders/AnnouncementSeeder.phpを作成し、以下の内容を記述します。

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Announcement;

class AnnouncementSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Announcement::truncate();

        $announcement_data = [];
        for ($i = 1; $i <= 2; $i++) {
            $announcement_data[] = [
                'title' => sprintf('タイトル%d', $i),
                'body' => sprintf('本文%d', $i),
                'publish' => date('Y-m-d'),
                'img_fname' => sprintf('img%08d.png', $i),
                'open' => ($i % 2), // id:2のデータは非公開
            ];
        }

        foreach ($announcement_data as $data) {
            $announcement = new Announcement();
            $announcement->title = $data['title'];
            $announcement->body = $data['body'];
            $announcement->publish = $data['publish'];
            $announcement->img_fname = $data['img_fname'];
            $announcement->open = $data['open'];
            $announcement->save();
        }
    }
}

テスト用のデータを2件作ります。
id1は公開データ、id2は非公開データという想定です。
id1のレコードにはimg00000001.png、id2のレコードにはimg00000002.pngという名前でimg_fnameフィールドに保存しておきます。
img_fname、open以外のフィールドは今回はなくても動作確認可能です。

準備が終わったら、artisanコマンドでSeederを実行させます。

php artisan db:seed --AnnouncementSeeder

画像を保存

2枚png画像を用意し、img00000001.png、img00000002.pngという名前(DBに登録したときの名前です)でstorage/app/announcementディレクトリに保存しておきます。
今回は適当なフリー画像をダウンロードして手動で保存しました。

ファイル名DBと揃っていれば何でも問題ありません。

確認

ここまでできたらブラウザのアドレスバーに画像へのURLを入力して確認してみましょう。
※ドメインは適宜変更

https://localhost/announcement/file/1/img_fname
これは公開データなので画像が表示されます。

https://localhost/announcement/file/2/img_fname
これは非公開データなのでNot Foundとなります。

以上、画像表示の動的化に成功しました。
実際にページに表示させる場合は、bladeファイルに例えば下記のように書けば画像表示できるでしょう。

<img src="/announcement/file/{{ $announcement->id }}/{{ $announcement->img_fname }}">

Discussion