Laravel5.6でファイルアップロードの実装と躓いたところまとめ



業務でも自習でも初めてファイルアップロード機能を手を動かして実装してみたので手順をまとめておこうと思います。
実装中に躓いた箇所があったのでその対応方法も記載します。

# 動作環境について
OS:macOS High Sierra
version: 10.13.6
Docker(Engine):18.09.1


#### 対象ブラウザ
GoogleChrome:72以上

#### 環境
centOS:7.5
Laravel:5.6
PHP:7.2
MySQL:5.7
PostgreSQL:10.6(MySQLから変更しました。)
APache2.4

# 参考資料
[Laravelで画像ファイルアップロードをする簡単なサンプル](https://qiita.com/makies/items/0684dad04a6008891d0d)
[【Laravel5.6】画像ファイルアップロードについてのポイントまとめ](https://weblion303.net/1028/)
[Laravelのバリデーションで指定できる内容をざっくりまとめ直しました。](https://qiita.com/fagai/items/9904409d3703ef6f79a2)


# 画面構成について

全然スマートな画面じゃなくて恐縮ですが・・・。
管理画面にある各商品情報の右に「画像」ボタンを配置しています。


![スクリーンショット 2019-02-11 12.04.58.png](https://qiita-image-store.s3.amazonaws.com/0/238861/ef3ab1e3-344d-0e78-23d8-70ba01ad3928.png)

「画像」ボタンを押下するとファイルアップロードのモーダルが表示されます。

![スクリーンショット 2019-02-11 12.05.18.png](https://qiita-image-store.s3.amazonaws.com/0/238861/0f261f18-6417-99dd-7255-78537526069b.png)

アップロードに成功するとTOPページに各商品ごとの画像が表示される様になります。
*「-------」となっている商品はまだ画像のアップロードを行っていないことを示しています。

![スクリーンショット 2019-02-11 12.07.58.png](https://qiita-image-store.s3.amazonaws.com/0/238861/179efa8d-f4ef-1816-ea37-be0bfb697337.png)

下記では、この上記画像の動作を実現する方法をまとめています。

# View

モーダルのformタグ内は下記の通りになっています。
name属性が「select_id」のinputタグは「画像」ボタン押下時にjavascriptでvalueが入る様になっています。

```PHP
<form method="POST" action="{{ route('admin_upload_image') }}" enctype='multipart/form-data'>
    {{ csrf_field() }}
    <input type="hidden" name="select_id" value="">

    <div class="form-group row">
        <label for="goods_image" class="col-md-3 col-form-label text-md-left">{{ __('イメージ') }}</label>

        <div class="col-md-7">
            <input type="file" name="goods_image" value="" style="border:none;" >
            <small class="input_condidion">*jpg,png形式のみ</small></br>
            <small class="input_condidion">*最小画像サイズ:縦横100px</small></br>
            <small class="input_condidion">*最大画像サイズ:縦横600px</small>

            @if ($errors->has('goods_image'))
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $errors->first('goods_image') }}</strong>
                </span>
            @endif
        </div>
    </div>

    <div class="col-md-offset-3 text-center">
        <button type="button" class="btn btn-primary" data-dismiss="modal">閉じる</button>
        <input type="submit" class="btn btn-success" value="登録">
    </div>
</form>
```


# ルーティング

web.phpに下記の通りに記載しています。

```PHP
Route::group(['prefix' => 'admin'], function(){
.
.
.
    Route::post('goods/upload', 'Admin\HomeController@uploadGoodsImage')->name('admin_upload_image');
});
```


# Controller
Controllerの処理の前にバリデーションファイルを別途用意する予定でしたが、後述の躓いた箇所の影響もあり、他の人が書いてくださった内容に習わせて頂きました。
拡張子の指定が「mimes」、画像サイズの指定が「dimensions」をキーとして詳細を指定しています。


・画像の格納先を指定するstoreAs()について
⇨第1引数:保存するディレクトリ名,第2引数:アップロードするファイル名,第3引数:disksのドライバ名(config/filesystems.phpに記載)となっています。
今回の場合はgoodsディレクトリの中に各商品IDのディレクトリを作成し、その中にアップロードした時の同様のファイル名で格納する様にパラメーターを変数で指定しています。
第3引数は特に変更がなければ「public」のままで良いと思います。
変更する場合は「config/filesystems.php」を編集する必要があります。


```PHP
public function uploadGoodsImage(Request $request)
{
    // 選択した商品IDの取得
    $select_id = $request->input('select_id');
    // アップロードしたファイル名を取得
    $upload_name = $_FILES['goods_image']['name'];

    // アップロードするディレクトリ名を指定
    $up_dir = 'goods/' . $select_id;

    // アップロードしたファイルのバリデーション設定
    $this->validate($request, [
        'goods_image' => [
            'required',
            'file',
            'image',
            'mimes:jpeg,png',
            'dimensions:min_width=100,min_height=100,max_width=600,max_height=600',
        ]
    ]);

    //アップロードに成功しているか確認
    if ($request->file('goods_image')->isValid([])) {
        $filename = $request->file('goods_image')->storeAs($up_dir, $upload_name, 'public');

        // DBへファイル名登録処理
        $goods = \App\Models\Goods::findOrFail($select_id);
        // $filenameだとパスが含まれてしまう為、basename()で囲う
        $goods->image_name = basename($filename);
        // 更新(差分があればDBに登録)
        $goods->save();

        return redirect()->to('admin/home')->with('flashmessage', 'イメージ画像の登録が完了しました。');
    }
    else{
        return redirect()->to('admin/home')->with('flashmessage', 'イメージ画像の登録に失敗しました。');
    }
}
```

# シンボリックリンクの作成
下記コマンドで public/storage から storage/app/public にシンボリックリンクを貼ること出来ます。

```shell-session
[root@87c2be02241a shop]# php artisan storage:link
The [public/storage] directory has been linked.
```


# 画像アップロードの確認
各商品のIDを格納先として画像が保存されていることが確認出来ます。

```shell-session
[root@87c2be02241a shop]# ls public/storage/goods/4
sample_goods.png
[root@87c2be02241a shop]# ls storage/app/public/goods/4
sample_goods.png
```


# アップロードするファイルを表示させる時
各商品の画像名を取得して、nullでは無い場合はURLを指定して表示させています。

```PHP
@if($goods->image_name)
    <img src="{{ asset('storage/goods/' . $goods->id . '/' . $goods->image_name) }}" width="40" height="40" alt="no_goods_image" />
@else
    <p>--------</p>
@endif
```


# 躓いたところ
アップロードするとhome画面にリダイレクトしてしまう不具合が発生。
バリデーションで何かミスかなと調べてみました。

Controller部分内でdd()を使ってファイルが取得出来ているかを確認してみると、ファイルを取得出来ていませんでしrた。

本来下記の形でファイルを取得出来るのですが、nullになるばかり。$_FILESを確認してもアップロードされたファイルが無い状態でした。

```PHP
$file = $request->file('goods_image');
dd($file)
```

調べていく中でformタグに「enctype」属性が抜けていたのを気づいたので下記の通り追記してみたのですが、これもだめでした。

```PHP
enctype="multipart/form-data"
```

php.iniの設定が悪いのかと思い確認してみました。
ファイルアップロード関連で関係のありそうなのは下記の設定ですが確認してみるとアップロードしているファイルのサイズ的には問題無さそうでした。

```PHP
upload_max_filesize = 10M
file_uploads = On
max_file_uploads = 20
post_max_size = 8M
```

⇨念の為「post_max_size」と「upload_max_filesize」の上限を上げてみたのですがこれも結局だめでした。


その後調べを進めてみるとやはり「enctype="multipart/form-data"」が記載されていないのが原因とする記述が多かった為、もしかしてブラウザ上では表示されていないのかなと思いデベロッパーツールで確認してみると、原因がわかりました。

「enctype="multipart/form-data"」と表示されるべきところが下記の様に表示されていました。

```PHP
enctype=""multipart/form-data""
```

そこでviewファイルにて「enctype='multipart/form-data'」と書き直したところ、無事ファイルアップロードが出来ました。

後日改めてダブルクォーテーションで囲ってみたらブラウザでも「enctype="multipart/form-data"」と表示されていました。
viewファイルの構成を考えながら作っていた為、viewファイルのどこかで記述がミスがあったと思われます。
viewとファイルアップロードを同時平行で作っていたのがアダとなった形です。

取り急ぎでファイルアップロードの機能を試したい時に正常に動かない場合、上記対応で動かせることがわかりました。