【Laravel】ローカル環境でのプロフィール画像のデフォルト表示、編集（登録）機能のいろは【画像アップロード】

# 今回紹介するのは
今回の記事では、ユーザープロフィール画像のデフォルト表示と、その画像を任意の画像に変更する機能について紹介します。SNSでよくある機能ですね。

この記事ひとつで、
**・プロフィール画像のデフォルト表示**
**・任意のプロフィール画像の登録、編集、表示機能**
上記を実装することができます。


# 環境
```
$ composer -V
Composer version 2.0.11 2021-02-24 14:57:23

$ php artisan -V
Laravel Framework 6.20.44
```

# はじめに

### 1. 2つのpublicディレクトリ
まずは、大元となる画像アップロード機能の説明から入ります。
画像の保存は、ユーザーの名前を保存したりするのとは違って少し複雑です。

今回、肝となるのが``public``ディレクトリです。
![スクリーンショット 2022-04-08 20.27.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1317507/e6b29174-376e-5924-0be3-d8fea064aa54.png)

よく見てみると、``public``ディレクトリが２つあるんですよねえ。


分かりやすくするため、上にある``public``ディレクトリを**上のpublic**、下にある``public``ディレクトリを**下のstorage/app/public**と呼ぶことにします。（ややこしいから注意）

**上のpublic**が**公開ディレクトリ**、**下のstorage/app/public**が**非公開ディレクトリ**となっています。公開ディレクトリは誰でもアクセス可能でセキュリティが弱いので、画像を保存する際は、非公開ディレクトリである**下のstorage/app/public**に保存します。

公開ディレクトリ（**上のpublic**）からは読み込みができても、非公開ディレクトリ（**下のstorage/app/public**）からは直接読み込みができません。ならどうするか。

### 2. シンボリックリンク
それを解決するのがシンボリックリンクです。コマンドは
```
$ php artisan storage:link
```
これを実行すると、**上のpublic**下に``storage``ファイルが作成され、**上のpublic/storage**と**下のstorage/app/public**との間にリンクができます。

要するにシンボリックリンクを貼ることで、**上のpublic/storage**（公開ディレクトリ）から、**下のstorage/app/public**（非公開ディレクトリ）を覗き見ることが可能になったわけです。
結論としては、

**1. 画像の保存は下のstorage/app/public（非公開ディレクトリ）に。**
**2. シンボリックリンクを貼って、上のpublic/storage（公開ディレクトリ）経由で下のstorage/app/public（非公開ディレクトリ）の画像を読み込む。**

が画像アップロードの基本的な手法になります。

# 実装

:::note info
今回はユーザーアイコンのデフォルト表示、編集（登録）機能を想定しています。
:::

### 1. 下準備
#### ・デフォルト画像の作成
まずは、デフォルトで表示するための画像を作ります。
自分は下記のサイトでオリジナルを作成しました。

https://picrew.me/image_maker/2

出来上がった画像をPCに保存して、**上のpublic**下に移動させます。

![スクリーンショット 2022-04-09 10.52.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1317507/86bdf207-1796-3016-b2dc-53496fc8dff2.png)

画像の`default.png`がそれです。

#### ・Migrationファイル

Migrationファイルに以下を追加します。
```php:create_users_table.php
public function up()
    {
        Schema::create('users', function (Blueprint $table) {

            //省略

            $table->string('profile_image')->nullable()->comment('プロフィール画像'); //この行を追加
        });
    }
```
なぜ
```php
->nullable()
```
なのかは**5. View（表示画面)** で説明します。

### 2. Form（編集画面）

```php:edit.blade.php
<form method="POST" action="{{ route( 'user.update', Auth::user() )}}" enctype="multipart/form-data">
    @csrf
    @method('PATCH')

    //省略

    <div class="form-group">
        <label for="profile-image">
            @if ($user->profile_image === null)
               <img class="rounded-circle" src="{{ asset('default.png') }}" alt="プロフィール画像" width="100" height="100">
            @else
               <img class="rounded-circle" src="{{ Storage::url($user->profile_image) }}" alt="プロフィール画像" width="100" height="100">
            @endif
            <input id="profile-image" name="profile_image" type="file" class="form-control @error('profile-image') is-invalid @enderror" style="display:none;" value="" accept="image/png, image/jpeg">
        </label>
        @error('profile-image')
            <span class="invalid-feedback" role="alert">
                <strong>{{ $message }}</strong>
            </span>
        @enderror
    </div>

    //省略

</form>
```

ここで重要な部分は
・formタグの``enctype="multipart/form-data"``
・inputタグの``type="file"``、``accept="image/*"``
です。一つずつ説明していきます。

・``enctype="multipart/form-data"``
複数の種類のデータ（テキスト、ファイルなど）を一度に扱える形式で、フォームで画像を扱う時には必須。


・``type="file"``
画像なので、file形式を指定。

・``accept="image/*``"
選択できるファイルの形式を制限できる。今回は画像ファイルのみに制限。

```php
@if ($user->profile_image === null)
   <img class="rounded-circle" src="{{ asset('default.png') }}" alt="プロフィール画像" width="100" height="100">
@else
   <img class="rounded-circle" src="{{ Storage::url($user->profile_image) }}" alt="プロフィール画像" width="100" height="100">
@endif
```

ここは**5.View（表示画面）** と同じなので、そこで説明します。


### 3. Routing
```php:web.php
Route::resource('user', 'UserController', ['only' => ['edit', 'update']]);
```
ここは人によって違うかもですが、``UserController``の``update``アクションに渡すルーティングが記載されていれば問題ないです。

### 4. Controller
```php:UserController
public function update(Request $request, $id)
    {

        //バリデーションは省略

        // リクエストデータ （全て）を取得し、$updateUserに代入
        $updateUser = $request->all();

        // プロフィール画像の変更があった場合
        if ($request->profile_image != null) {
            // storeメソッドで一意のファイル名を自動生成しつつstorage/app/public/profilesに保存し、そのファイル名（ファイルパス）を$profileImagePathとして生成
            $profileImagePath = $request->profile_image->store('public/profiles');
            // $updateUserのprofile_imageカラムに$profileImagePath（ファイルパス）を保存
            $updateUser['profile_image'] = $profileImagePath;
        }

        // ログイン中のユーザの情報を取得し、$loginUserに代入
        $loginUser = Auth::user();
        // $updateUserのデータを受け取り、データベースへ保存
        $loginUser->fill($updateUser)->save();

        return redirect()->route('user.show', Auth::user());
    }
```


コメントアウトである程度説明していますが、重要な部分だけ詳しく説明します。


```php
// storeメソッドで一意のファイル名を自動生成しつつstorage/app/public/profilesに保存し、そのファイル名（ファイルパス）を$profileImagePathとして生成
$profileImagePath = $request->profile_image->store('public/profiles');
```

``$request->profile_image``は送信されてきた画像データです。これを``store``メソッドを使って**下のstorage/app/public/profiles**（非公開ディレクトリ）に保存しつつ、一意のIDをファイル名(ファイルパス)として生成します。
このとき、``profiles``ディレクトリは**下のstorage/app/public**下に自動で作成されます。
生成されたファイルパスを変数``$profileImagePath``の値とします。

```php
// $updateUserのprofile_imageカラムに$profileImagePath（ファイルパス）を保存
$updateUser['profile_image'] = $profileImagePath;
```
変数``$updateUser``のprofile_imageカラムに``$profileImagePath``（ファイルパス）を保存します。



### 5. View（表示画面）

プロフィール画像が登録されていない時はデフォルト画像である``default.png``を表示、登録（編集）されている時はその画像を表示するようにします。

```php:show.blade.php
<div>
    @if ($user->profile_image === null)
       <img class="rounded-circle" src="{{ asset('default.png') }}" alt="プロフィール画像" width="100" height="100">
    @else
       <img class="rounded-circle" src="{{ Storage::url($user->profile_image) }}" alt="プロフィール画像" width="100" height="100">
    @endif
</div>
```

**1. 下準備** の**Migrationファイル**へのカラム追加の際に``->nullable()``としたのは、プロフィール画像が登録されている時とされていない時（profile_imageカラムがnullであるのか、nullでないのか）をif文で分岐するためです。

#### ・画像が登録されていない時

```php
<img class="rounded-circle" src="{{ asset('default.png') }}" alt="プロフィール画像" width="100" height="100">
```

・``src="{{ asset('default.png') }}"``
``asset()``関数を使い、publicディレクトリ配下の素材（ここでは``default.png``）へのURLを生成します。それを``src="{{ }}"``で読み込んで表示させます。


#### ・画像が登録されている時

```php
<img class="rounded-circle" src="{{ Storage::url($user->profile_image) }}" alt="プロフィール画像" width="100" height="100">
```

・``src="{{ Storage::url($user->profile_image) }}"``
``url()``メソッドは指定したファイルのURLを取得することができる方法で、``Storage::url()``で外部からアクセス可能なURLを取得することができます。それを``src="{{ }}"``で読み込んで表示させます。


# 終わりに

以上が、ユーザーのプロフィール画像の表示、編集（登録）機能になります。
この通りにコードを書いてうまく実装できなかった場合は気軽にご質問ください。

# 参考記事
[Qiita：Laravel5.8 画像アップロード機能を仕組みから理解する](https://qiita.com/kei_Q/items/62cb8747280266956100)
[Qiita：Laravelで画像ファイルを保存したい](https://qiita.com/koru1893/items/1d2f522e20744b03e3ad)
[Qiita：input type="file"のaccept属性がかなり使える](https://qiita.com/macky120307/items/ca4efdbd46c0f7f9987a)
[Qiita：【Laravel】画面に画像を表示する方法。assetsの使い方とurlヘルパ関数との違いについて。](https://qiita.com/shizen-shin/items/ad72bb3b4350a4791afd)
[Laravel Storageでファイル操作マスター](https://reffect.co.jp/laravel/laravel-storage-manipulation-master#url)