九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、先日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
にしている場合特にフォルダを作成する必要はありません。
③テスト用
ここでは単純にcustomers
とattachments
を「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()
の中では選択されたファイルデータをVue
のphotos
に格納することになります。
②送信データを用意する
「送信する」ボタンがクリックされたときに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読書しています😂」