【Laravel / JavaScript(JQuery)】画像ファイルアップロードとプレビュー

【Laravel / JavaScript(JQuery)】画像ファイルアップロードとプレビュー

 

Laravelを使用して、画像ファイルのアップロードの実装方法です。

アップロード時にファイルが確認できるようにプレビューの表示もしましょう!

 

この記事では

1.画像ファイルのアップロードから保存の手順

 (form作成→保存に必要な内容作成→保存処理)

2.画像ファイルのアップロード時にプレビューを表示する方法

の順で解説します。

 

環境はLaravelの5系です。

 

1.画像ファイルのアップロードから保存の手順

 

まず画像投稿を行う部分のビューを作っておきます。

 

 

装飾はおいておき、form部分のコード例です。

<form action="(送信先)" method="post" enctype="multipart/form-data">
    {{ csrf_field() }}
    <input type="file" class="btn" name="file" id="images">

    <div class="action-button">
      <button type="button" class="cancel">キャンセル</button>
      <button type="submit" class="update">更新</button>
    </div>
</form>

肝となる部分は二つです。

 

一つ目はinputのファイル指定の部分です。

 <input type="file" class="btn" name="file" id="images">

画像ファイルのアップロードにはtype=”file”とします。また他のinputと同様にリクエストで受け取るためにnameで名前を付けておきましょう。

 

二つ目はformの属性のenctypeです。

<form action="(送信先)" method="post" enctype="multipart/form-data">

enctype=”multipart/form-data”と設定します。既に使ったことある方はおまじないのようなもので付けている方も多いと思います。

これを設定しない場合、ファイルの名前は送られるのですが、ファイル自体の中身の情報を送信できないので、サーバー側では添付ファイルの情報が送られていないことになり、ファイルの保存などの扱いができません。

 

 

さらに詳しく知りたい方は詳しく調べている方がいたので参考リンクを貼っておきます。(enctype=’multipart/form-data’ってなんだ?)

 

 

これでファイルの送信ができました、それではLaravelで保存に必要なものを書いていきましょう。

 

ファイル保存に必要なのは以下4つです。

  • 保存した画像のパスを保存する、string型のカラムが必要。
  • バリデーションの設定
  • 保存処理
  • シンボリックリンクの作成

 

 

これらを実現していきますが、ここでの前提条件は以下になります。

順番にみていきましょう。

 

①画像のパスを保存するstring型のカラムですが、ここではユーザーのプロフィールページのヘッダーカバー画像をアップロードすることを想定し、Userモデルのcover_filenameというカラムが画像パスを格納するカラムとします。

 

 

カラムを最初から作っていれば問題ないですが、カラムがなくて追加する場合は次のようにコマンドで追加します。

$php artisan make:migration add_cover_filename_to_users_table --table=users

すでにあるテーブルにカラムを追加する場合は–tableオプションで既存のテーブルを指定します。

 

それで生成された(生成日時)_add_users_table.phpを以下のように書いてマイグレートしてカラムを追加します。

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
             //カラムの追加 nullを許容
            $table->string('cover_filename')->after('remember_token')->nullable();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            // カラムの削除
            $table->dropColumn('cover_filename');
        });
    }
}

 

②formの送信先はupdateアクション(関数)とします。

update関数を次のように書きます。

public function upload(Request $request)
{
    $this->validate($request, [
        'file' => 'required|file|image|mimes:jpeg,png'
    ]);
    $filename = $request->file->store('public/cover');
    $user = User::find(Auth::user()->id);
    $user->cover_filename = basename($filename);
    $user->save();
    return redirect('/profile');
}

 

局所的に見ていきましょう!

まずバリデーションの部分です

'file' => 'required|file|image|mimes:jpeg,png'

・require: 入力必須

・file: アップロードされたものがファイルか

・image: ファイルが画像ファイルか

・mimes:jpeg,png : 拡張子をjpeg,pngに限定

としています。

 

 

$filename = $request->file->store('public/cover');

このstoreメソッドで保存しています。storeメソッドはファイルを自動で名前を付け直して保存し、引数で保存先のディレクトリを指定して、返値はファイルのパスを返します。(自分で名前を決めて保存したい場合はstoreAsという別のメソッドを使います。) 後述しますが保存先は ‘public/保存したいディレクトリ名’ という形にしましょう。

 

引数の保存先のディレクトリのパスは(アプリ名)/storage/appが起点となっており、’public/cover’と指定しているので、'(アプリ名のディレクトリ)/storage/app/public/cover’に保存されることになります。

 

 

$user->cover_filename = basename($filename);

basename()メソッドで、パスのうち、ファイル名だけを取得しています。つまりcover_filenameカラムではファイル名のみを保存しています。

 

これでsaveで保存したら、ファイルが保存され、画像のファイル名もカラムに格納されました!

 

 

次に画像表示になりますが、webからこの画像にアクセスできるようにしなければならないので、シンボリックリンクの作成を行います。

 

シンボリックリンクの作成は次のコマンドを実行するだけです。

$php artisan storage:link

シンボリックリンクとは、本来のファイルの保存場所とは別の場所にも保存し、別のところでもアクセスできるようにする、所謂「ショートカット」のようなものです。

 

シンボリックリンクが必要な理由は、画像が保存された先ほどの (アプリ名ディレクトリ)/storage/app/public/cover ディレクトリですが、このstorage以下のディレクトはwebからアクセスできないディレクトリです。

 

公開ディレクトリは(アプリ名のディレクトリ)/publicディレクトリになっています。つまりwebから画像ファイルにアクセスするにはpublicディレクトリ内にも画像を配置しなければならないということで、それをシンボリックリンクによって実現します。

 

 

上記のコマンドを実行することで、シンボリックリンクにアクセスすることで画像が表示できるようになりました。

 

 

シンボリックリンクにアクセスするためのURLはassetメソッドを使います。

asset('storage/xxxxx');

で保存した画像のシンボリックリンクへの「パス」が取得できます。

 

なぜstorageからのパスをかくか、またstoreで保存時にpublic以下にしたかについて詳しく説明します。ややこしい説明なしで実装したい方は飛ばして下に進んでください!

 

 

シンボリックリンクは先ほどのコマンドを実行したことで

(アプリ名)/public/storage から (アプリ名)/storage/app/public

を指すようになっています。

 

つまりstoreで保存した’storage/app/public/cover’のシンボリックリンクの作成先が(アプリ名)/public/storage/coverですので、パスの対応を考えると、assetの引数に指定するパスは公開public内のstorageから書くようにしなければならない、ということになります。

 

 

 

この辺詳しく理解しようとすると少々ややこしいですが、コードの記述の際には、

保存時:store(‘public/任意’)

表示時:asset(‘strage/任意’)

とすればよいだけですね!

 

 

ですので、以下のようにすることで、保存した画像を表示することができるようになります。

<img src="{{ asset('storage/cover/' . $user->cover_filename) }}" alt="" />

 

 

formの上に画像を表示してみます。最初のformのコードの始めの部分に画像を表示するように書き加えて画像ファイルをアップロードしてみましょう。

(例)

<div class="cover-image">カバー写真</div>
    <p style="height:200px;">
        <img class="preview-cover" src="{{ asset('storage/avatar/' . $user->cover_filename) }}" alt="" onerror='this.style.display = "none"' style="height: 200px;" />
    </p>
<form id="formCoverFile" action="profile/upload" method="post" enctype="multipart/form-data" style="margin-top:150px;">
    {{ csrf_field() }}
    <input type="hidden" name="file_type" value="cover">
    <input type="file" class="btn" name="file" id="images">
    <div class="action-button">
        <button type="button" class="cancel" id="modalBackgroundForCoverFile"/>キャンセル</button>
        <button type="submit" class="update" >更新</button>
    </div>
</form>

 

 

これで画像のアップロードと表示をすることができました!

 

 

2.画像ファイルのアップロード時にプレビューを表示する

 

ここからはJavaScript(jQuery)だけの処理になります!

以下が完成コードです。(上のHTMLのコードに対応しています)

$('form').on('change', 'input[type="file"]', event => {
    const file = event.target.files[0],
          reader = new FileReader(),
          $preview = $('.preview-cover'); // 表示する所

    // 画像ファイル以外は処理停止
    if(file.type.indexOf("image") < 0){
      return false;
    }
    // ファイル読み込みが完了した際に発火するイベントを登録
    reader.onload = function() {
        // .prevewの領域の中にロードした画像を表示
        $preview.attr('src',event.target.result);
    };

    reader.readAsDataURL(file);
});

解説していきます。

 

 

$('form').on('change', 'input[type="file"]', function(e) {

changeメソッドより、input要素に何かしら変化があれば(ここではファイルの入力をしたら)このメソッドが呼ばれます。直接inputを指定するのではなく、formを指定してデリゲートしていますが、直接指定してもかまいません。

 

ファイル入力すると、無名関数が実行されてeventにはinput要素の情報が入っています。

const file = event.target.files[0],
    reader = new FileReader(),

eventのtargetには「files」というプロパティがあります。これには「FileList」というファイルを配列のように管理するオブジェクトが設定されており、files[0]とすることで、入力された一つ目のファイルのFileオブジェクトを取得します。

 

次にnew FileReader()でファイル読み込みを行うためのオブジェクトを生成しています。

 

ファイルの表示の前に画像ファイルかどうかのチェックを行いましょう。

 // 画像ファイル以外は処理停止
if(file.type.indexOf('image') < 0){
  return false;
}

この部分ですね。

まず file の type というメソッドで、入力されたファイルの型すなわち「MIMEタイプ」を文字列で取得します。そしてメソッドチェーンでさらにそのMIMEタイプの文字列に、’image’という文字列があるかどうかをindexOfで調べ、あればその位置(0以上)を返し、なかったら-1を返すので、その条件判定で画像ファイルかどうかを判別しています。

 

 

MIMEタイプとは何か、という方のために、説明する前に見てもらいたいのが以下です。

 

JavaScript読み込み時にHTML5より前では必要であった、

<script type="text/javascript">
 ・・・
</script>

のtype、これこそがMIMEタイプのことです。text/javascriptというような型のことです。これは「メインタイプ/サブタイプ」という書き方になっています。

 

もし入力されたのが、jpgかpngの画像ファイルであれば

image/jpeg
image/png

というMIMEタイプが得られるので、共通するimageという文字列に着目してindexOfメソッドでimageを検索させています。このどちらかであれば0が返値になるので、これで画像ファイルでなければこの関数を終了させています。

 

 

次に進みます。

 // ファイル読み込みが完了した際に発火するイベントを登録
reader.onload = function() {
    // .prevewの領域の中にロードした画像を表示
    $preview.attr('src',event.target.result);
};

ファイルリーダーにはonloadというプロパティがあり、ファイルの読み込みが終わった時に呼び出すコールバック関数を保持します。

 

簡単に言えば、ファイルの読み込みが終わったらreaderのonloadというプロパティに入っている関数が実行されるということです。さらに言えば、ファイルの読み込みが終わったときに実行する関数をonloadに代入しています。

 

ファイルの読み込みが終わったら、ブラウザで読み込んだ画像ファイルのURLは(正確にはData URIスキームといいます)は event.target.result に格納されることになるので、attrメソッドでsrc属性にevent.target.resultを設定しています。これで画像が表示できるような設定になりました。

 

 

あとはファイルの読み込みを行うだけで、

reader.readAsDataURL(file);

でファイルの読み込みを行っています。

よって最後の流れは、この処理によってファイルの読み込みが行われevent.target.resultにデータのURLが格納されて、onloadプロパティの関数が実行され画像が表示されるという仕組みです。

 

 

これを素のJavaScriptで書いたのも置いておきます。

const $ = document,
      $form = $.querySelector('form');
$.querySelector('input[type="file"]').addEventListener('change', event => {
    const file = e.target.files[0],
          reader = new FileReader(),
          $preview =  $.querySelector(".preview-cover");

    if(file.type.indexOf('image') < 0){
      return false;
    }

    reader.onload = () => (
        // imgタグを作成
        $preview.setAttribute('src',  e.target.result);
    };
    reader.readAsDataURL(file);
});

 

以上になります。ありがとうございました。

 

 

参考リンク

https://readouble.com/laravel/5.7/ja/filesystem.html

https://qiita.com/makies/items/0684dad04a6008891d0d

https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types

https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL