【Laravel】ファイルアップロードの実装パターン集

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はLaravelでよく出るエラーと解決法をまとめました。

今回は、実務でほぼ必ず出てくる「ファイルアップロード」の実装パターンを、基本から一歩踏み込んだところまで解説します。

プロフィール画像、PDFの添付、CSVの取り込みなど、ファイルアップロードが絡む機能は本当に多いので、パターンとして押さえておくとどの案件でも応用が効きます。

ファイルアップロードの基本の流れ

アップロードに必要な3つの要素

Laravelでファイルアップロードを実装するときは、次の3点をセットで押さえておく必要があります。

  • フォーム側enctype="multipart/form-data" を指定したフォーム
  • バリデーション:ファイル形式・サイズなどのチェック
  • 保存処理Storage ファサードを使ったファイルの保存

このどれか1つでも欠けると、「送信自体はできるが保存されない」「保存はできるが危険なファイルも通ってしまう」といった問題につながります。

フォームの用意

まずはBlade側のフォームです。

<form action="{{ route('profile.upload') }}" method="POST" enctype="multipart/form-data">
    @csrf
    <input type="file" name="avatar">
    <button type="submit">アップロード</button>
</form>

enctype="multipart/form-data" を忘れると、ファイルの中身が正しく送信されないので注意してください。

私も初めてファイルアップロードを実装したとき、この enctype を書き忘れて「なぜかファイルがnullになる」と1時間ほど悩んだことがあります。

テキスト入力のフォームをコピーして作ったせいで、この属性の存在自体を忘れていたのが原因でした。

コントローラでの受け取りと保存

コントローラ側では、Request オブジェクトの file() メソッドでアップロードされたファイルを受け取ります。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProfileController extends Controller
{
    public function upload(Request $request)
    {
        $request->validate([
            'avatar' => 'required|file|image|max:2048',
        ]);

        $path = $request->file('avatar')->store('avatars', 'public');

        auth()->user()->update(['avatar_path' => $path]);

        return back()->with('success', 'アップロードが完了しました');
    }
}

store('avatars', 'public') の部分がポイントです。

第1引数の 'avatars' は保存先のディレクトリ、第2引数の 'public' はどのディスク(保存先の設定)を使うかを指定しています。

バリデーションで守るべきポイント

ファイル種類・サイズの制限

ファイルアップロードでは、想定外のファイルが送られてくることを前提にバリデーションを組む必要があります。

<?php
$request->validate([
    'avatar' => 'required|file|image|mimes:jpg,jpeg,png|max:2048',
    'document' => 'nullable|file|mimes:pdf|max:5120',
]);
  • file:アップロードされたファイルであることを検証する
  • image:画像ファイルであることを検証する
  • mimes:jpg,jpeg,png:許可する拡張子・MIMEタイプを限定する
  • max:2048:最大サイズをKB単位で指定する(この例では2MBまで)

拡張子チェックだけでは不十分な理由

❌ Before:拡張子の見た目だけで判定してしまう

<?php
// 独自に拡張子だけをチェックする実装(危険)
$originalName = $request->file('avatar')->getClientOriginalName();
$extension = pathinfo($originalName, PATHINFO_EXTENSION);

if (!in_array($extension, ['jpg', 'png'])) {
    return back()->withErrors(['avatar' => '対応していないファイル形式です']);
}

この方法は、ファイル名を malware.php から malware.jpg にリネームするだけで簡単にすり抜けてしまいます。

拡張子の文字列だけを見るチェックは、悪意のあるファイルに対してほぼ無力です。

✅ After:Laravelのmimesルールでファイルの中身ベースで検証する

<?php
$request->validate([
    'avatar' => 'required|file|image|mimes:jpg,jpeg,png|max:2048',
]);

Laravelの mimes ルールは、ファイル名の拡張子だけでなく、ファイルの中身(実際のMIMEタイプ)を見て検証してくれます。

自前でチェックロジックを書くよりも安全で、コード量も少なく済むので、基本的にはバリデーションルールに任せるのが得策です。

保存先ディスクの考え方

local と public の違い

Laravelのファイルストレージには「ディスク」という概念があり、config/filesystems.php で定義されています。

代表的なディスクの使い分けは次の通りです。

ディスク保存先特徴
localstorage/app/private外部から直接アクセスできない非公開領域
publicstorage/app/publicシンボリックリンク経由で外部公開できる領域
s3Amazon S3クラウドストレージ。大容量・複数サーバー構成向け

プロフィール画像のように「ユーザーに見せてよいファイル」は public ディスク、契約書PDFのように「本人以外に見せたくないファイル」は local ディスクを使う、というのが基本的な判断軸です。

public ディスクを使うときの注意点

public ディスクにファイルを保存しても、そのままではブラウザから直接アクセスできません。

シンボリックリンクを1度作成しておく必要があります。

php artisan storage:link

このコマンドを実行すると、public/storage というシンボリックリンクが作られ、storage/app/public 配下のファイルにブラウザから直接アクセスできるようになります。

❌ Before:storage:link を忘れて画像が表示されない

{{-- avatar_pathには "avatars/xxxx.jpg" が保存されている --}}
<img src="{{ asset('storage/' . $user->avatar_path) }}">

保存処理自体は成功しているのに、画像が404になって表示されない、というのはこの storage:link の実行忘れが原因であることが非常に多いです。

✅ After:デプロイ手順にstorage:linkを組み込む

php artisan storage:link

ローカル環境では最初の1回だけ実行すれば済みますが、本番環境にデプロイするたびにこのコマンドが必要になるケースもあるため、デプロイ手順書やデプロイスクリプトに組み込んでおくことをおすすめします。

応用:ファイル名の重複を防ぐ・複数ファイルに対応する

ファイル名を自動生成する

store() を使うと、Laravelが自動でランダムなファイル名を生成してくれるため、基本的にはファイル名の重複を気にする必要はありません。

<?php
$path = $request->file('avatar')->store('avatars', 'public');
// 例: avatars/9f2b3c1a4d5e6f7g8h9i0j.jpg のようなランダム名で保存される

もし元のファイル名を残しつつ重複を避けたい場合は、storeAs() を使って自分でファイル名を組み立てます。

<?php
$file = $request->file('document');
$fileName = now()->format('YmdHis') . '_' . $file->getClientOriginalName();

$path = $file->storeAs('documents', $fileName, 'public');

日時のプレフィックスを付けておくと、同じファイル名がアップロードされても上書きされにくくなります。

複数ファイルの同時アップロード

複数ファイルを一度にアップロードしたい場合は、フォームの name 属性に [] を付けます。

<form action="{{ route('gallery.upload') }}" method="POST" enctype="multipart/form-data">
    @csrf
    <input type="file" name="photos[]" multiple>
    <button type="submit">まとめてアップロード</button>
</form>

コントローラ側では、配列としてループ処理します。

<?php
public function upload(Request $request)
{
    $request->validate([
        'photos' => 'required|array|max:10',
        'photos.*' => 'file|image|max:2048',
    ]);

    $paths = [];
    foreach ($request->file('photos') as $photo) {
        $paths[] = $photo->store('gallery', 'public');
    }

    return back()->with('success', count($paths) . '件アップロードしました');
}

バリデーションルールの photos.* という書き方は、「配列の中身1つ1つ」に対してルールを適用するという意味です。

配列全体には arraymax:10(最大10ファイルまで)、各ファイルには file|image|max:2048 というように、階層ごとにルールを分けて書けます。

まとめ

この記事のポイント

  • ファイルアップロードには「フォームのenctype」「バリデーション」「保存処理」の3点セットが必要
  • 拡張子だけのチェックは危険なので、Laravelのmimesルールで中身ベースの検証をする
  • 保存先ディスクは公開してよいかどうかでlocalpublicを使い分ける
  • publicディスクを使う場合はphp artisan storage:linkを忘れずに実行する(画像が表示されない定番トラブル)
  • 複数ファイルアップロードはname="xxx[]"xxx.*のバリデーションルールで対応できる

次に読むべき記事

次回は、Vue・Inertiaと組み合わせた場合の画像アップロード実装を、フロントエンド側の実装も含めて解説します。

→ 次の記事:Vue・Inertia・Laravelでの画像アップロード実装

タイトルとURLをコピーしました