【Laravel】ファイルのアップロード機能をつくる(ダウンロード可)

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、先日300記事を突破したので過去の内容をチェックしてみることにしました。

すると、「あれ、記事にしてなかったっけ❓❓」というものがありました。

それは・・・

Laravelでファイルをアップロードする機能

です。

過去にファイルアップロードを効率化する独自パッケージ「ClampBolt」を公開しているので、てっきり記事として公開しているものと勘違いしていました😂

そこで❗

今回はLaravel顧客データを保存&プロフィール画像も一緒にアップロードできる機能を作ってみたいと思います。

ぜひ楽しみながらやってみましょう❗

(なお、根本を理解していただきたいので、今回ClampBoltパッケージは使いませんm_ _)

「できるだけ他でも使いやすい形にします👍」

開発環境: Laravel 7.x

モデル&マイグレーションをつくる

まずはDB関連からやっていきます。
モデル、マイグレーションともに以下2つずつつくります。

  • Customer: 顧客データ
  • Attachment: アップロードされたファイル

顧客を管理する「Customer」

以下のコマンドでモデルとマイグレーションを一気に作成してください。

php artisan make:model -m Customer

①モデル

コマンドを実行したら、まずモデルの中身を変更しましょう。

/app/Customer.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    // リレーションシップ
    public function attachments() {

        return $this->hasMany('App\Attachment', 'parent_id', 'id')
            ->where('model', self::class);  // 「App\Customer」のものだけ取得

    }
}

この中でやっているのは、「1:多」のリレーションをつくることです。

ただし、通常と少し違うのがwhere()の部分です。
これは、Customerモデルに関連するデータだけ取得するためです。

つまり、より詳しく言うと、

  • 「attachments.parent_id」=「customers.id」
  • 「attachments.model」=「App\Customer」

のデータがhasMany()で結合されることになります。

※なお、この形で実装したのは他のモデルでファイル・アップロードする場合にも応用しやすいからです👍

②マイグレーション

そして、マイグレーションです。
ここはシンプルに顧客データに「名前」「メールアドレス」の項目をつくるだけです。

/database/migrations/****_**_**_******_create_customers_table.php

// 省略

public function up()
{
    Schema::create('customers', function (Blueprint $table) {
        $table->id();
        $table->string('name')->comment('名前');
        $table->string('email')->comment('メールアドレス');
        $table->timestamps();
    });
}

// 省略

アップロードしたファイルを管理する「Attachment」

続いてアップロードしたファイル管理をするAttachmentです。

①モデル

php artisan make:model -m Attachment

②マイグレーション

/database/migrations/****_**_**_******_create_attachments_table.php

// 省略

public function up()
{
    Schema::create('attachments', function (Blueprint $table) {
        $table->id();
        $table->integer('parent_id');
        $table->string('model')->comment('モデル名');
        $table->string('path')->comment('ファイルパス');
        $table->string('key')->comment('キー');
        $table->timestamps();
    });
}

// 省略

この中で設定している項目は次のとおりです。

  • parent_id: 各テーブルの「id」(例: customersのid)
  • model: モデル名(例:App\Customer)
  • key: 何のファイルかをグループ化するキー(例: profile_photos)

マイグレーション実行

では、以下のコマンドでマイグレーションを実行しましょう。

php artisan migrate

するとテーブルは次のようになります。

ルートをつくる

続いて、ルートでアクセスできるURLをつくっていきます。

/routes/web.php

Route::get('customer/create', 'CustomerController@create'); // 送信フォーム
Route::post('ajax/customer', 'CustomerController@store');   // Ajaxでデータを受け取る
Route::get('customer/test', 'CustomerController@test');     // テスト用

1行目は送信フォームが表示されるルートです。
そして、そのフォームから送信されたデータを受け取るのが2行目。
最後の3行目はテスト用になります。

コントローラーをつくる

では、ルートで設定したコントローラーを作っていきましょう。
以下のコマンドを実行してください。

php artisan make:controller CustomerController

そして、作成されたファイルを開いて中身を次のように変更します。

/app/Http/Controllers/CustomerController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CustomerController extends Controller
{
    public function create() { // 👈 フォームを表示 ・・・ ①

        return view('customer.create');

    }

    public function store(Request $request) { // 👈 Ajaxデータ受信 ・・・ ②

        // バリデーション
        $request->validate([
            'name' => 'required',
            'email' => 'required|email',
            'photos' => 'required',
            'photos.*' => 'image'
        ]);

        // 顧客データ保存
        $customer = new \App\Customer();
        $customer->name = $request->name;
        $customer->email = $request->email;
        $result = $customer->save();

        // ファイル保存
        if($result && $request->hasFile('photos')) {

            foreach($request->photos as $photo) { // 👈 送信されたファイルをひとつずつ保存

                $path = $photo->store('attachments'); // 👈 「storage/app/attachments」フォルダに保存
                $attachment = new \App\Attachment();
                $attachment->parent_id = $customer->id;
                $attachment->model = get_class($customer);
                $attachment->path = $path;
                $attachment->key = 'photos';
                $attachment->save();

            }

        }

        return ['result' => true];

    }

    public function test() { // 👈 テスト用

        // 保存ファイルのデータと連結して取得
        $customers = \App\Customer::with('attachments')->get();

        foreach($customers as $customer) {

            $attachments = $customer->attachments; // 👈 全ての関連ファイルを取得
            $photos = $attachments->filter(function($attachment){ // 👈 プロフィール写真のものだけ取得

                return ($attachment->key === 'photos');

            });

            echo $customer->name .'さんの写真(ファイルパス)';

            foreach($photos as $photo) {

                $path = storage_path('app/'. $photo->path); // 保存したファイルのパス
                dump($path);

            }

        }

    }

}

少しコードが長いので順に説明をしていきます。

①フォームを表示

ここで、顧客データ&プロフィール写真をアップロードするフォームを表示します。つまり、ここが実際にブラウザでアクセスしたときに実行される部分になります。(なお、ビューは次の項目でつくります)

②Ajaxデータ受信

ここでは、送信された顧客データ(今回は「名前」「メールアドレス」)とプロフィール写真を受け取ります。

データを受け取ったら、まずバリデーションで内容が正しいかをチェックします。

そして、先に新しい顧客データを作成し、その顧客IDを元にしてAttachmentデータも作成することになります。

※なお、保存されるフォルダは「/storage/app/attachments」で、storageフォルダの権限を777にしている場合特にフォルダを作成する必要はありません。

③テスト用

ここでは単純にcustomersattachmentsを「1:多」の関係で結合し、データ取得しています。

そして、ループでひとつずつcustomerデータを取得することになるのですが、ここで関連するattachmentsデータを取得、さらにキーが「photos」のものだけフィルターを掛けて取得しています。

結果として、アップロードしたファイルのパスが表示されることになります。

ビューをつくる

最後にビューで送信フォームをつくります。

<html>
<head>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
</head>
<body>
    <div id="app" class="p-3">
        <h1 class="mb-3">Laravelファイルをアップロードするサンプル</h1>
        <p class="bg-light p-3">顧客データをプロフィール写真を一緒に保存する場面を想定しています。</p>
        <div class="row">
            <div class="col-md-6">
                <div class="form-group">
                    <label>名前</label>
                    <input type="text" class="form-control" v-model="params.name">
                </div>
                <div class="form-group">
                    <label>メールアドレス</label>
                    <input type="text" class="form-control" v-model="params.email">
                </div>
                <div class="form-group">
                    <label>写真(複数選べます)</label>
                    <!-- ファイル選択部分 ・・・ ① -->
                    <input
                        ref="photo"
                        type="file"
                        class="form-control"
                        accept="image/gif,image/jpeg,image/png"
                        @change="onFileChange"
                        multiple>
                </div>
                <div class="text-right">
                    <button type="button" class="btn btn-primary" @click="onSubmit">送信する</button>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                params: {
                    name: '',
                    email: ''
                },
                photos: []
            },
            methods: {
                onFileChange(e) {

                    this.photos = e.target.files; // ファイルを変数に格納

                },
                onSubmit() {

                    // 送信データを用意する ・・・ ②
                    let formData = new FormData();
                    formData.append('name', this.params.name);
                    formData.append('email', this.params.email);

                    for(let photo of this.photos) {

                        formData.append('photos[]', photo);

                    }

                    axios.post('/ajax/customer', formData)
                        .then(response => {

                            if(response.data.result) {

                                alert('アップロード成功!');

                                // 入力データを初期化
                                this.params = {
                                    name: '',
                                    email: ''
                                };
                                this.$refs['photo'].value = '';

                            }

                        })
                        .catch(error => {

                            // ここでエラー処理
                            console.log(error);

                        });

                }
            }
        });

    </script>
</body>
</html>

この中で特に重要なのが2つです。

①ファイル選択部分

ファイルを選択するinputタグですが、ファイルが選択された時点でonFileChange()が実行されることになります。

そして、onFileChange()の中では選択されたファイルデータをVuephotosに格納することになります。

②送信データを用意する

送信する」ボタンがクリックされたときにAjax送信するパラメータを作っています。

コードを見ていただくとわかるとおり、axiosのよく見るパラメータ設定とは違い、FormDataをつかっています。

これは、ファイルを送信する場合は通常の送信方法ではうまくいかないからです。

テストしてみる

では、実際にテストしてみましょう❗
ブラウザで「http://*****/customer/create」にアクセスします。

画像のように「名前」「メールアドレス」を入力し、画像を選択して送信します。

すると、次のようなアラートが表示されました。

では、テーブルをチェックしてみましょう。

データは上手く保存されています。
次はstorageフォルダです。

こちらも上手くファイルが保存されています。

成功です😊✨

おまけ:データ削除されたら関連ファイルを自動で削除する

複雑になるので本編には含めませんでしたが、例えばcustomersからあるデータが削除されたら、関連するattachmentsのデータ、そしてアップロードされたファイルを削除する方法を「おまけ」としてご紹介します。

イベントをつくる

モデルのデータが削除されたときに実行されるCustomerDeletedというイベントファイルを作成します。

php artisan make:event CustomerDeleted

ファイルが作成されたら中身を次のように変更します。

// 省略

public function __construct(Customer $customer)
{
    foreach($customer->attachments as $attachment) {

        \Storage::delete($attachment->path); // 👈 ファイル削除
        $attachment->delete(); // 👈 attachmentデータ削除

    }
}

// 省略

モデルにイベントをセットする

次に、CustomerDeletedがデータ削除されたとき実行されるようにCustomerモデルにセットします。

以下の部分を追加してください。

<?php

namespace App;

use App\Events\CustomerDeleted; // 👈 追加
use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    // 👇 追加
    protected $dispatchesEvents = [
        'deleted' => CustomerDeleted::class
    ];

    // 省略
}

これで次のような場合、自動的に関連するattachmentsも関連ファイルも削除されることになります。

$customer = \App\Customer::first();
$customer->delete(); // 👈 自動でattachments, ファイルが削除される

教材ファイルをダウンロードする

今回実際に開発した教材ファイルを以下からダウンロードできます。

【Laravel】ファイルのアップロード機能

※ただし、マイグレーションなどはご自身で実行していただく必要があります。

おわりに

ということで今回はLaravelを使ってファイルをアップロードする方法をご紹介しました。

ファイルのアップロードは、よくクライアント様からもご依頼を受ける機能なので覚えておくと、きっと後で楽できると思いますよ👍

また、今回はattachmentsテーブルはどんなモデルからでも連携できる形にしましたので、customersテーブルだけでなくその他のテーブルからも使いやすいと思います。

ぜひ皆さんもチャレンジしてみてくださいね。

ではでは〜❗

「外出自粛なので、小説の主人公を
自分に置き換える擬似VR読書しています😂」

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
また、わかりにくい部分がありましたらお問い合わせからお気軽にご連絡ください。
(また、個人レッスンも承ってます👍)
このエントリーをはてなブックマークに追加       follow us in feedly