こんにちは、かつコーチです。
前回はBladeベースでのファイルアップロードの実装パターンを解説しました。
今回はもう一歩進んで、Vue・Inertiaを使ったSPA(Single Page Application)構成での画像アップロードを扱います。
Inertia(イナーシャ)とは、LaravelのバックエンドとVue・Reactなどのフロントエンドをつなぐライブラリで、APIを別途用意しなくてもSPAのような画面遷移を実現できる仕組みです。
Vue・Inertia構成では、Bladeのフォーム送信とは勝手が違う部分があるので、そこを中心に見ていきます。
Blade構成とVue・Inertia構成の違い
フォーム送信の仕組みの違い
Bladeの場合は、<form> タグのブラウザ標準の送信機能に任せてページ全体をリロードしていました。
Vue・Inertia構成では、ページ全体をリロードせずに、JavaScript経由でサーバーとやりとりします。
Inertiaには useForm というヘルパーが用意されており、これを使うとファイルアップロードも含めてスムーズに実装できます。
事前に用意するもの
Inertiaで画像アップロードを実装する前提として、@inertiajs/vue3 パッケージと、Laravel側の inertiajs/inertia-laravel が導入済みであることを前提に進めます。
導入手順自体は認証編(Breeze/Jetstream)で標準的にセットアップされることが多いので、ここでは割愛します。
基本の実装:単一画像のアップロード
Vueコンポーネント側
まずはVueコンポーネント側です。
<script setup>
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
const form = useForm({
avatar: null,
});
const previewUrl = ref(null);
function handleFileChange(event) {
const file = event.target.files[0];
form.avatar = file;
previewUrl.value = file ? URL.createObjectURL(file) : null;
}
function submit() {
form.post(route('profile.avatar.update'), {
forceFormData: true,
onSuccess: () => {
form.reset('avatar');
},
});
}
</script>
<template>
<form @submit.prevent="submit">
<input type="file" accept="image/*" @change="handleFileChange">
<img v-if="previewUrl" :src="previewUrl" alt="プレビュー" width="120">
<div v-if="form.errors.avatar" class="error">
{{ form.errors.avatar }}
</div>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'アップロード中...' : 'アップロード' }}
</button>
</form>
</template>
ポイントは3つあります。
useFormでフォームの状態(値・エラー・送信中フラグ)をまとめて管理するURL.createObjectURL(file)で、サーバーに送る前にブラウザ上でプレビュー表示するform.post()にファイルが含まれる場合はforceFormData: trueを指定する
forceFormData が必要な理由
❌ Before:forceFormDataを指定せず送信してしまう
function submit() {
form.post(route('profile.avatar.update'));
}
Inertiaは通常、フォームの値をJSON形式で送信します。
しかし、ファイルオブジェクトはJSONに変換できないため、この書き方だとファイルの中身が正しくサーバーに届かず、コントローラ側で null として受け取ってしまいます。
私はInertia構成で初めて画像アップロードを実装したとき、この現象に遭遇しました。
「バリデーションエラーも出ないのに、なぜかファイルだけ保存されない」という状態で、原因を特定するまでかなり時間がかかりました。
✅ After:forceFormData: trueでmultipart/form-data送信を強制する
function submit() {
form.post(route('profile.avatar.update'), {
forceFormData: true,
});
}
forceFormData: true を指定すると、Inertiaが内部的にBladeフォームと同じ multipart/form-data 形式で送信してくれるようになり、ファイルが正しくサーバーに届くようになります。
「フォームにファイル入力が含まれる場合は必ず forceFormData: true を付ける」というのを、実装時のルールとして覚えておくとよいでしょう。
コントローラ側
サーバー側の受け取り方は、Blade構成のときとほぼ同じです。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProfileAvatarController extends Controller
{
public function update(Request $request)
{
$request->validate([
'avatar' => 'required|file|image|mimes:jpg,jpeg,png|max:2048',
]);
$path = $request->file('avatar')->store('avatars', 'public');
$request->user()->update(['avatar_path' => $path]);
return back()->with('success', 'プロフィール画像を更新しました');
}
}
Inertiaを使っていても、サーバー側は普通のLaravelのコントローラのままです。
Inertia特有の違いはフロントエンド側の送信方法にあり、バックエンドの受け取り方や保存処理は共通している、という点を押さえておくと理解しやすくなります。
つまずきやすいポイント:PUTメソッドでのファイル送信
症状:更新系(PUT)でファイルが届かない
編集画面などで form.put() を使って更新処理を送ると、ファイルが正しく届かないことがあります。
❌ Before:PUTメソッドでファイルを直接送る
function submit() {
form.put(route('profile.avatar.update'), {
forceFormData: true,
});
}
これはHTMLの仕様上、multipart/form-data を使ったファイル送信は POST メソッドしかサポートしていないために起きる問題です。
forceFormData: true を付けていても、メソッド自体が PUT だとブラウザ側の制約でファイルがうまく送信できないことがあります。
✅ After:_methodスプーフィングでPOSTとして送りつつ論理的にPUT扱いにする
function submit() {
form.post(route('profile.avatar.update'), {
forceFormData: true,
// Inertiaのuseformは内部でPOST送信時に_methodを付与してPUT相当として扱える
});
}
<?php
// routes/web.php
use App\Http\Controllers\ProfileAvatarController;
Route::post('/profile/avatar', [ProfileAvatarController::class, 'update'])
->name('profile.avatar.update');
ファイルを含むフォームは常に form.post() を使い、ルート側もPOSTメソッドとして定義しておくのが、Inertia構成における確実な対処法です。
「更新処理だからPUT」という思い込みにとらわれず、ファイルが絡む場合はPOSTで統一する、と覚えておくとトラブルを避けられます。
応用:アップロード進捗(プログレスバー)を表示する
onProgress で進捗を取得する
大きめの画像や動画をアップロードする場合、ユーザーに進捗を見せてあげると体験が良くなります。
Inertiaの useForm は onProgress コールバックを提供しています。
<script setup>
import { useForm } from '@inertiajs/vue3';
const form = useForm({
avatar: null,
});
function submit() {
form.post(route('profile.avatar.update'), {
forceFormData: true,
onProgress: (progress) => {
console.log(`アップロード進捗: ${progress.percentage}%`);
},
});
}
</script>
<template>
<div v-if="form.progress" class="progress-bar">
<div class="progress-bar-inner" :style="{ width: form.progress.percentage + '%' }"></div>
</div>
</template>
form.progress は送信中のみ値を持つオブジェクトで、percentage プロパティから進捗率を取得できます。
これをプログレスバーのCSSに反映させれば、送信中のユーザー体験をぐっと改善できます。
まとめ
この記事のポイント
- Vue・Inertia構成でファイルアップロードを行う場合、
useFormのpost()にforceFormData: trueを指定する必要がある - サーバー側(コントローラ)の受け取り方はBlade構成と同じで、バリデーションや保存処理を共通化できる
- ファイルを含むフォームの更新処理は
PUTではなくPOSTで送るのが安全 form.progressを使うとアップロードの進捗バーを簡単に実装できる- Inertia特有の違いはフロントエンドの送信方法にあり、バックエンドの考え方はBlade構成と地続きである
次に読むべき記事
次回は、Laravel 10から11で変わったポイントを、実際にアップデート作業で注意すべき点も含めて解説します。
→ 次の記事:Laravel10→11で変わったポイントを解説
