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

本文へ

フッターへ

事例・ノウハウ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を使ったファイルアップロードの機能が実装できたと思います。
画像のバリデーションやリサイズなど他にも画像関連でやることはたくさんあると思いますので、順に確認していければと思います。

では、また。

この記事を書いた人

アーティス
アーティス
創造性を最大限に発揮するとともに、インターネットに代表されるITを活用し、みんなの生活が便利で、豊かで、楽しいものになるようなサービスやコンテンツを考え、創り出し提供しています。
この記事のカテゴリ

FOLLOW US

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