Laravelで画像をアップロードする方法

こんにちは、むちょこです。

今日はリクエストいただいたLaravelで画像をアップロードする方法について解説したいと思います☆

環境はPHP7、Laravel5.6です。

1. ルートを定義

今回は、/profileでプロフィール画像を登録する想定で進めます。

routes/web.php
Route::get('/profile', '[email protected]');
Route::post('/profile', '[email protected]');

Route::get()がフォームを表示するルート定義、
Route::post()が送信された値を受け取るためのルート定義です。

今回はフォームの表示と送信された値の受け取りを同じURLで行います。

もしこれらを別のURLにしたい場合は、それぞれの第一引数を適宜変更してください。

MEMO

本題から少し逸れますが、今回はログイン済みのユーザが本人のプロフィールを変更することを前提としているので、認証を必要とするグループ内でルート定義を行いました。

Route::group(['middleware' => 'auth'], function () {
    //ここでルート定義
}

2. アップロード用のフォームを作成

まずプロフィールを操作するコントローラを生成し、編集します。

コマンド
$ php artisan make:controller ProfileController
Controller created successfully.
app/Http/Controllers/ProfileController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProfileController extends Controller
{
    /**
     * プロフィール登録フォームの表示
     *
     * @return Response
     */
    public function index()
    {
        return view('profile/index');
    }
}

次にbladeファイルにアップロード用のフォームを用意します。

resources/views/profile/index.blade.php
<form method="POST" action="/profile" enctype="multipart/form-data" >
{{ csrf_field() }}
  <input type="file" name="photo">
  <input type="submit">
</form>

画像投稿に関わらず、フォームを利用するときは{{ csrf_field() }}でCSRFトークンの隠しフィールドを生成する必要があります。これはCSRFからアプリケーションを保護するために使われます。

MEMO

formのenctype属性にmultipart/form-dataを指定することも忘れずに!
これがないと、ファイルデータをアップロードすることができません。

これでフォームの準備ができました✨

フォーム表示画面

3. 画像ファイルのバリデーション

バリデーションは、コントローラに書く方法とカスタムリクエストクラスに書く方法の2種類があります。

どちらに書いても間違いではありませんが、私は特別な理由がない限りカスタムリクエストクラスを利用する方をオススメします☆

webアプリケーションは後から機能を追加することが少なからずあり、なるべく予め分離し可読性を上げておいた方が良いという考えです。

コマンド
$ php artisan make:request ProfileRequest
Request created successfully.

コマンド実行で生成されたリクエストクラスを編集します。

app/Http/Requests/ProfileRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ProfileRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'photo' => 'required|file|image|mimes:jpeg,png,jpg,gif|max:2048'
        ];
    }
}

authorizeメソッドでは、ユーザがデータを更新するための権限を持っているかどうかを確認するために使います。今回は特にないので何もせずtrueを返しています。

rulesメソッドにはバリデーションルールを記載します。
ルールの詳細はこんな感じです。

required
フィールドが入力データに存在しており、かつ空でないことをバリデートします。フィールドは以下の条件の場合、「空」であると判断されます。
値がnullである。
値が空文字列である。
値が空の配列か、空のCountableオブジェクトである。
値がパスのないアップロード済みファイルである。

https://readouble.com/laravel/5.7/ja/validation.html#rule-required

file
フィールドがアップロードに成功したファイルであることをバリデートします。


https://readouble.com/laravel/5.7/ja/validation.html#rule-file

image
フィールドで指定されたファイルが画像(jpg、png、bmp、gif、svg)であることをバリデートします。

https://readouble.com/laravel/5.7/ja/validation.html#rule-image

mimes:foo,bar,…
フィールドで指定されたファイルが拡張子のリストの中のMIMEタイプのどれかと一致することをバリデートします。

https://readouble.com/laravel/5.7/ja/validation.html#rule-mimes

max:
フィールドが最大値として指定された以下であることをバリデートします。sizeルールと同様の判定方法で、文字列、数値、配列、ファイルが評価されます。

https://readouble.com/laravel/5.7/ja/validation.html#rule-max

次に、今編集したProfileRequestクラスをコントローラに適用させましょう。

値を受け取るstoreメソッドの第一引数としてProfileRequestのインスタンスを指定するだけで、先ほどのバリデーションに通過した場合のみstoreメソッド内の処理が実行されるようになります。

app/Http/Controllers/ProfileControllers.php
use App\Http\Requests\ProfileRequest;

class ProfileController extends Controller
{

    /**
     * プロフィールの保存
     *
     * @param ProfileRequest $request
     * @return Response
     */
    public function store(ProfileRequest $request)
    {
    }
}

4. エラーを表示

エラーは自分で定義する必要はなく、Laravel側で処理してくれるので私たちは表示部分を書くだけでOKです。

バリデーションが通らないと、エラーのフラッシュメッセージが保存された状態で前の画面が表示されます。

エラーメッセージを表示したい部分にこんな感じで書いてください。

resources/views/profile/index.blade.php
@if ($errors->any())
<div class="alert alert-danger">
    <ul>
    @foreach ($errors->all() as $error)
        <li>{{ $error }}</li>
    @endforeach
    </ul>
</div>
@endif
エラーメッセージを表示
MEMO

メッセージの日本語化は、こちらを利用させていただいています☆

minoryorg/laravel-resources-lang-ja

5. 画像ファイルの保存

今回はローカルのstorageディレクトリにprofile_imagesディレクトリを作成(以下のプログラム実行時に自動で作成されます)し、”(ユーザID).jpg”というファイル名で登録することにします。

app/Http/Controllers/ProfileControllers.php
use Illuminate\Support\Facades\Auth;

class ProfileController extends Controller
{
    /**
     * プロフィールの保存
     *
     * @param ProfileRequest $request
     * @return Response
     */
    public function store(ProfileRequest $request)
    {
        $request->photo->storeAs('public/profile_images', Auth::id() . '.jpg');

        return redirect('profile')->with('success', '新しいプロフィールを登録しました');
    }
}

storeAsメソッドの第一引数に保存先のパス(/storage/appからの相対パス)、第二引数にファイル名を指定することで任意の場所・名前でファイルを保存することができます。

処理後は登録フォームがあるページにリダイレクトさせました。

MEMO

一意のファイル名を自動で生成する場合は、storeAsメソッドの代わりに
storeメソッドを使用してください:)

$request->photo->store('public/profile_images');

返り値でディスクルートからの相対ファイルパスを取得できます。

withメソッドの引数に指定したメッセージは、フラッシュ―データとしてセッションに保存されます。

resources/views/index.blade.php
@if (session('success'))
<div class="alert alert-success">
    {{ session('success') }}
</div>
@endif
成功メッセージ表示

6. 保存した画像を表示

最後に保存した画像を表示してあげましょう。

storage/appにアクセスするために、シンボリックリンクを貼ります。

コマンド
$ php artisan storage:link

プログラムは、単純にファイルが存在すれば表示するという形にします。

app/Http/Controllers/ProfileController.php
use Illuminate\Support\Facades\Storage;

class ProfileController extends Controller
{
    /**
     * プロフィール登録フォームの表示
     *
     * @return Response
     */
    public function index()
    {
        $is_image = false;
        if (Storage::disk('local')->exists('public/profile_images/' . Auth::id() . '.jpg')) {
            $is_image = true;
        }
        return view('profile/index', ['is_image' => $is_image]);
    }
}

existsメソッドを使ってファイルの存在を確認し、その結果をテンプレートに渡しています。

MEMO

ここでは分かりやすさを優先してパスやファイル名をそのまま書いていますが、2箇所以上で使用する場合は変数や関数でまとめてしてしまうのもオススメです。

class ProfileController extends Controller
{
    public static function getImagePath()
    {
        return 'public/profile_images';
    }

    public static function getImageFileName()
    {
        return Auth::id() . '.jpg';
    }
}

ファイルが存在していれば画像を表示します。

resources/views/profile/index.blade.php
@if ($is_image)
<figure>
    <img src="/storage/profile_images/{{ Auth::id() }}.jpg" width="100px" height="100px">
    <figcaption>現在のプロフィール画像</figcaption>
</figure>
@endif
完成