グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



Laravel×Inertia×Vue3でファイルアップロード機能を作ってみた

5月も後半となり初夏の訪れを感じる時期となりましたが、みなさまはいかがお過ごしでしょうか?
私は、相変わらずバックエンドとフロントエンドを行き来するせわしない日々を送っています。

今回は、Laravel×Inertia×Vue3でファイルアップロードの機能を作ってみます。

前提

  • ubuntu v22.04
  • PHP v8.3
  • Laravel v10.48
  • Vite v5.1
  • Vue v3.4
  • Inertia v1.0

準備

こちらの記事での開発環境を用意した上で進んでください。

現状は以下の通りです。

routes

# /routes/web.php

// ...省略...

Route::resource('news', NewsController::class)
    ->except(['store', 'update']);
Route::post('news/create', [NewsController::class, 'store'])
    ->name('news.store');
Route::put('news/{news}/edit', [NewsController::class, 'update'])
    ->name('news.update');

コンポーネント

<!-- resources/js/Pages/News/Create.vue -->

<script setup>
import { useForm } from '@inertiajs/vue3'

const form = useForm({
    title: null,
    body: null,
})

const submit = () => {
    form.post(route('news.store'));
}
</script>

<template>
    <form @submit.prevent="submit">
        <table>
            <tr>
                <th><label for="title">title:</label></th>
                <td>
                    <div v-if="form.errors.title">{{ form.errors.title }}</div>
                    <input id="title" type="text" v-model="form.title">
                </td>
            </tr>
            <tr>
                <th><label for="body">body:</label></th>
                <td>
                    <div v-if="form.errors.body">{{ form.errors.body }}</div>
                    <textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
                </td>
            </tr>
            <tr>
                <th></th>
                <td>
                    <button type="submit" :disabled="form.processing">登録</button>
                </td>
            </tr>
        </table>
    </form>
</template>
<!-- resources/js/Pages/News/Edit.vue -->

<script setup>
import { useForm } from '@inertiajs/vue3'

const props = defineProps({
    news: Object,
})

const form = useForm({
    title: props.news.title,
    body: props.news.body,
})

const submit = () => {
    form.put(route('news.update', props.news.id))
}
</script>

<template>
    <form @submit.prevent="submit">
        <table>
            <tr>
                <th><label for="title">title:</label></th>
                <td>
                    <div v-if="form.errors.title">{{ form.errors.title }}</div>
                    <input id="title" type="text" v-model="form.title">
                </td>
            </tr>
            <tr>
                <th><label for="body">body:</label></th>
                <td>
                    <div v-if="form.errors.body">{{ form.errors.body }}</div>
                    <textarea id="body" cols="22" rows="20" v-model="form.body"></textarea>
                </td>
            </tr>
            <tr>
                <th></th>
                <td>
                    <button type="submit" :disabled="form.processing">更新</button>
                </td>
            </tr>
        </table>
    </form>
</template>

Controller

# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{

    //...省略...

    public function create()
    {
        return Inertia::render('News/Create', []);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => ['required'],
            'body' => ['required'],
        ]);

        News::create($validated);

        return redirect()->route('news.index');
    }

    public function edit(News $news)
    {
        return Inertia::render('News/Edit', [
            'news' => $news,
        ]);
    }

    public function update(Request $request, News $news)
    {
        $validated = $request->validate([
            'title' =>'required',
            'body' => 'required',
        ]);

        $news->update($validated);

        return redirect()->route('news.index');
    }

    //...省略...

実装

では実際にファイルアップロードの機能を実装していきます。

dbの対象tableの修正

imageカラムを追加します。

sail php artisan make:migration add_image_to_news_table --table=news
# database/migrations/YYYY_mm_dd_xxxxxx_add_image_to_news_table.php

    //...省略...

    public function up(): void
    {
        Schema::table('news', function (Blueprint $table) {
            $table->string('image')->nullable(); //追加
        });
    }

    public function down(): void
    {
        Schema::table('news', function (Blueprint $table) {
            $table->dropColumn('image'); //追加
        });
    }
# app/Models/News.php

class News extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'body',
        'image', //追加
    ];
}
sail php artisan migrate

Create/Store

コンポーネント、Controllerの順で修正していきます。

コンポーネントの修正

<!-- resources/js/Pages/News/Create.vue -->

<script setup>
import { useForm } from '@inertiajs/vue3';

const form = useForm({
    title: null,
    body: null,
    image: null, //追加
})

const submit = () => {
    form.post(route('news.store'));
}
</script>

<template>
    <form @submit.prevent="submit">

        <!-- ...省略... -->

            <!-- 以下追加 -->
            <tr>
                <th><label for="image">image:</label></th>
                <td>
                    <input type="file" id="image" @input="form.image = $event.target.files[0]">
                </td>
            </tr>

        <!-- ...省略... -->

        </table>
    </form>
</template>

@input=""のイベントリスナーで選択されたファイルを$event.target.files[0]を通して取得しform.imageに格納しています。

Controllerの修正

# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{

    // ...省略...

    public function create()
    {
        return Inertia::render('News/Create', []);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => ['required'],
            'body' => ['required'],
        ]);

        // 以下追加
        if ($image = $request->file('image')) {
            $validated['image'] = $image->store('upload');
        }

        News::create($validated);

        return redirect()->route('news.index');
    }

$request->file()でファイルを取得し、$image->store()でファイルを保存しています。また、ファイルパスをdbに登録したいので、$validated['image']$image->store()の返り値を格納しています。

今回は割愛しますが、以下公式にもある通りdbに保存するファイルパスのサニタイズはしましょう。

Unprintable and invalid unicode characters will automatically be removed from file paths. Therefore, you may wish to sanitize your file paths before passing them to Laravel’s file storage methods. File paths are normalized using the League\Flysystem\WhitespacePathNormalizer::normalizePath method.

Edit/Update

編集画面で登録済みの画像を表示させたいので、まずは画像を読み込めるようにします。その後create/storeの工程と同じくコンポーネント、Controllerの順で修正していきます。

cd public
ln -s ../storage/app/upload

アップロードした画像を編集画面でアクセスできるように公開します。画像の格納先の storage/app/uploadからpublic/にシンボリックリンクを貼ります。

コンポーネントの修正

<!-- resources/js/Pages/News/Edit.vue -->

<script setup>
import { router, useForm } from '@inertiajs/vue3' //修正

const props = defineProps({
    news: Object,
})

const form = useForm({
    title: props.news.title,
    body: props.news.body,
    image: null, //追加
    registerd_image: props.news.image //追加
})

const submit = () => {
    //以下修正
    router.post(route('news.update', props.news.id), {
        _method: 'put',
        title: form.title,
        body: form.body,
        image: form.image,
        registerd_image: form.registerd_image,
    });
}
</script>

<template>
    <form @submit.prevent="submit">

        <!-- ...省略... -->

            <!-- 以下追加 -->
            <tr>
                <th><label for="image">image:</label></th>
                <td>
                    <img :src="`/${form.registerd_image}`" alt="">
                </td>
                <td>
                    <input type="file" id="image" @input="form.image = $event.target.files[0]">
                </td>
            </tr>

        <!-- ...省略... -->

    </form>
</template>

以前の記事ではform.put()でPUTメソッドを使ってリクエストを送信していました。 しかし、今回の記事では router.post(<route>, { _method: 'put' })でPOSTメソッドを使ってリクエストを送信しています。

この理由は、PUTメソッドを使用した場合、multipart/form-dataリクエストによるファイルアップロードが直接サポートされていないためです。それ故、POSTメソッドを使って送信し、_method属性で本来のメソッドであるPUTメソッドを指定しています。

公式にも明記されています。

## Multipart limitations

Uploading files using a multipart/form-data request is not natively supported in some server-side frameworks when using the PUT,PATCH, or DELETE HTTP methods. The simplest workaround for this limitation is to simply upload files using a POST request instead.

However, some frameworks, such as Laravel and Rails, support form method spoofing, which allows you to upload the files using POST, but have the framework handle the request as a PUT or PATCH request. This is done by including a _method attribute in the data of your request.

Controllerの修正

# app/Http/Controllers/NewsController.php

class NewsController extends Controller
{

    // ...省略...

    public function edit(News $news)
    {
        return Inertia::render('News/Edit', [
            'news' => $news,
        ]);
    }

    public function update(Request $request, News $news)
    {
        $validated = $request->validate([
            'title' =>'required',
            'body' => 'required',
            'registerd_image' => 'nullable'
        ]);

        // 以下追加
        if ($image = $request->file('image')) {
            $validated['image'] = $image->store('upload');

            if (!empty($validated['registerd_image'])) {
                unlink(storage_path('app/'). $validated['registerd_image']);
            }
        }
        unset($validated['registerd_image']);

        $news->update($validated);

        return redirect()->route('news.index');
    }

新しい画像がアップロードされていたら、新しい画像を登録しながら、登録済みの旧画像のdbのデータと実ファイルを削除します。
また、dbへの登録にregisterd_imageカラムは不要なのでunsetしてます。

おわりに

いかがだったでしょうか?

簡単にですが、InertiaとLaravelを使ったファイルアップロードの機能が実装できたと思います。
画像のバリデーションやリサイズなど他にも画像関連でやることはたくさんあると思いますので、順に確認していければと思います。

では、また。

この記事を書いた人

KJG
KJGソリューション事業部 システムエンジニア
大学4年時春に文系就職を辞め、エンジニアになることを決意し、独学でRuby、Ruby on Railsを学習。
約1年間の独学期間を経てアーティスへWebエンジニアとして入社。現在はWebエンジニアとして、主にシステムの開発・運用に従事している。
抽象的なもの、複雑なものを言語化して文章にするのが好きで得意。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします