Laravel 6.x 【画像アップロード】 トランザクションを使用する

#制作環境

Windows 10
Laravel : 6.18.35
Laravel/ui : 1.0
Laravel-mix : 5.0.1
Bootstrap : 4.0.0
MDBootstrap : 4.19.1
chart.js : 2.9.3
XAMPP
PHP : 7.4.3
Visual Studio Code


#はじめに

この記事はプログラミングをはじめたばかりの素人が、できたことをメモするのに利用しています。
内容には誤りがあるかもしれません。

**<font color="blue">以前作成した内容を修正しながらすすめていますので、まずは下記記事を見てください。</font>**

[Laravel 6.x 【画像アップロード】 ディレクトリの作成、ファイルの保存、データベースへの登録、表示](https://qiita.com/Charry/items/11d55ab7b1451f68d056)

<font color="red">機能の実装、確認が目的の為必要最低限しか記述していません。
なので、バリデーションやデザイン等は無視しています。</font>

#関連記事

[Laravel 6.x 【画像アップロード】 ディレクトリの作成、ファイルの保存、データベースへの登録、表示](https://qiita.com/Charry/items/11d55ab7b1451f68d056)
[Laravel 6.x 【画像アップロード】 ファイルの削除、データベースへのレコード削除](https://qiita.com/Charry/items/423cd11c04ea38dad17f)


#ページイメージ

シンプルに登録・削除用のフォームがあり、下に登録した画像が表示されるようにしてます。

![hp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/2d275a02-1492-a476-4fcf-649c6f1584d8.jpeg)


#今回のトランザクションの実装について

本来であれば複数のテーブルを同時に変更する際、両方のテーブルの整合性を保つ為にトランザクションを使用するのですが、今回使用するテーブルは1つだけです。
できるだけシンプルに内容がわかるようにする為にあえてそうしてます。

制作過程でやった事や確認したことについても掲載してます。

#制作概要

以前作成した、コントローラのstore処理を変更して作成していきます。

処理の内容としては、

```PHP:FileUpController.php
public function store(Request $request) {
// ①ファイル名の新規作成

// ここからトランザクション内部の処理

// ②データベースに保存する内容の設定
// ③データベースへの登録とファイルの保存
// ④ファイルの保存に失敗した場合に例外を投げてロールバックさせる

// ここまでトランザクション内部の処理

⑤リダイレクト
}
```

こちらの構成で作成していきます。

<font color="blue">下記は以前の記事で作成した、コントローラのstore処理の内容です。</font>
※これから作成するコードではありません。

```PHP:FileUpController.php
    public function store(Request $request)
    {
        $path = $request->img->store('public/images');
        $filename = basename($path);

        $data = new FileImage;
        $data->file_name = $filename;
        $data->save();

        return redirect('/file')->with('success', '登録完了しました。');
    }
```

それでは作成していきます。

#①ファイル名の新規作成

データベースにはファイルのパスではなく、ファイル名(sample.png)のみを保存するようにしてます。

以前のコードでは、データベースへの登録より先にファイルの保存を行っていました。

```PHP:FileUpController.php
$path = $request->img->store('public/images');
```

トランザクション処理をすることを前提にしている訳ではないので、先にこの処理を行っていますが、その他にも以下の理由で先にこの処理を行っていました。

* 保存と同時に新しくファイル名を付けてくれる為
* 新しく作成されたファイル名をデータベースに保存する為

ファイル名が先に決まってないと、処理がすすめられません。
なので、先に処理してます。

##storeメソッドについて

ファイルのstoreメソッドですが、とても便利なメソッドです。
保存とファイル名の作成を一度にやってくれます。
今回は別のメソッドを使用するので、詳しくはリファレンスを確認してください。

---
リファレンス
https://readouble.com/laravel/6.x/ja/filesystem.html?header=%25E3%2583%2595%25E3%2582%25A1%25E3%2582%25A4%25E3%2583%25AB%25E3%2582%25A2%25E3%2583%2583%25E3%2583%2597%25E3%2583%25AD%25E3%2583%25BC%25E3%2583%2589

##ヘルパ関数Str::random()の使用

今回はstoreメソッドを使用しないので、ファイル名は自前で作成します。

**<font color="red">★ポイント★</font>**
**元のファイル名を使用するのはセキュリティー的に問題があるため、新規でファイル名を作成します。**

**<font color="red">【注意事項】</font>**
<font color="red">一意な ID を生成するuniqidという関数がPHPにはありますが、リファレンスにも記載がありますが、暗号化としては使うなということなので、使わないようにしましょう。</font>

ファイル名の作成にはヘルパ関数のStr::random()を使用します。
これは、PHPのrandom_bytes関数を使用しています。

引数にはランダムな文字列の長さをバイト数で指定できます。

```PHP
Str::random(40);
```
---
PHPのrandom_bytes リファレンス
https://www.php.net/manual/ja/function.random-bytes.php

PHPのuniqid リファレンス
https://www.php.net/manual/ja/function.uniqid

ヘルパ関数Str::random() リファレンス
https://readouble.com/laravel/6.x/ja/helpers.html#method-str-random


##拡張子の取得

ファイル名の最後に拡張子が必要なので、元のファイル名から拡張子だけをちょうだいしたいと思います。

拡張子をいただくのに、以下のメソッドを使用します。

```PHP
getClientOriginalExtension()
```

これを使用すると、オリジナルのファイルから拡張子の部分だけを抜き出すことができます。

例) 元のファイル名 sample.png
抜き出される拡張子 png

**<font color="red">【注意事項】</font>**
<font color="red">あくまで抜き出されるのは、拡張子の「png」部分だけです。
「.png」ではありません。</font>

##実際の記述

完成した記述が以下になります。

```PHP:FileUpController.php
// ヘルパ関数使用のため先頭に追記
use Illuminate\Support\Str;

public function store(Request $request)
{
    // ①ファイル名の新規作成
    // 元のファイルから拡張子を取得
    $fileExtension = $request->img->getClientOriginalExtension();
    // ランダムな文字列と拡張子を結合して新しいファイル名を作成
    $fileName = Str::random(40) . '.' . $fileExtension;


    // ここからトランザクション内部の処理

    // ②データベースに保存する内容の設定
    // ③データベースへの登録とファイルの保存
    // ④ファイルの保存に失敗した場合に例外を投げてロールバックさせる

    // ここまでトランザクション内部の処理

⑤リダイレクト
}
```

拡張子を取得する記述ですが、

```PHP
$fileExtension = $request->img->getClientOriginalExtension();
```

下記の記述でも結果は同じです。

```PHP
$fileExtension = $request->file['img']->getClientOriginalExtension();
```

#トランザクション内部処理 ②データベースに保存する内容の設定

トランザクションには、

```PHP
DB::transaction(function () {});
```

を使用します。

この方法は自動で、コミット・ロールバックを行ってくれます。

**<font color="red">★ポイント★</font>**
**<font color="blue">一応この方法が推奨されているようです。</font>**
**DB::transaction内でtry・・・catchを使用している方がいるようですが、自分でコミット・ロールバックを制御したい場合は、DB::beginTransaction();を使うのが望ましいようです。**

参考にしたサイト
https://www.366service.com/jp/qa/2a7f17cbbeb405001459b46e29da134e


---
トランザクションのリファレンス
https://readouble.com/laravel/6.x/ja/database.html?header=%25E3%2583%2587%25E3%2583%25BC%25E3%2582%25BF%25E3%2583%2599%25E3%2583%25BC%25E3%2582%25B9%25E3%2583%2588%25E3%2583%25A9%25E3%2583%25B3%25E3%2582%25B6%25E3%2582%25AF%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%25B3


##内容の設定
保存内容の設定のやり方は以前の記述と変わりませんが、変数名だけ変更します。

```PHP:以前の記述
$data = new FileImage;
$data->file_name = $filename;
```

##実際の記述

完成した記述が以下になります。

```PHP:FileUpController.php
use Illuminate\Support\Str;
// トランザクション使用のため先頭に追記
use DB;

public function store(Request $request)
{
    // ①ファイル名の新規作成
    $fileExtension = $request->img->getClientOriginalExtension();
    $fileName = Str::random(40) . '.' . $fileExtension;


    // ここからトランザクション内部の処理
    DB::transaction(function () use ($request, $fileName) {

        // ②データベースに保存する内容の設定
        // モデルのインスタンス化(実態化)
        $fileImageTable = new FileImage;
        // テーブルのカラム(ファイル名)に登録する新規のファイル名を設定する
        $fileImageTable->file_name = $fileName;


        // ③データベースへの登録とファイルの保存
        // ④ファイルの保存に失敗した場合に例外を投げてロールバックさせる
    });

    // ここまでトランザクション内部の処理

⑤リダイレクト
}
```

##トランザクションの記述について

```PHP
    DB::transaction(function () use ($request, $fileName) {
       // 処理
    });
```

**<font color="red">★ポイント★</font>**
**<font color="blue">トランザクション外の変数を使用する時は、useを使用して指定しないと使えません。</font>**
ここでは、\$requestと①で作成したファイル名を使用する為、\$requestと$fileNameを渡してます。

#トランザクション内部処理 ③データベースへの登録とファイルの保存

データの保存方法自体は、変数名を変更しているだけで以前と変わりません。
ただ、データベースの保存ができたら画像ファイルも保存するようにする為、ifによる条件分岐を行っています。

```PHP
if ($fileImageTable->save()) {
    // データベースへの保存がうまくいった後の処理
}
```

##画像ファイルの保存処理
ファイルの保存には任意でファイル名を指定できるstoreAsメソッドを使用します。

使い方はstoreメソッドとあまり変わりません。
今回の場合はstoreの部分をstoreAsに変更するだけで使用できます。

---

storeAsのリファレンス
https://readouble.com/laravel/6.x/ja/requests.html?header=%25E3%2582%25A2%25E3%2583%2583%25E3%2583%2597%25E3%2583%25AD%25E3%2583%25BC%25E3%2583%2589%25E3%2583%2595%25E3%2582%25A1%25E3%2582%25A4%25E3%2583%25AB%25E3%2581%25AE%25E4%25BF%259D%25E5%25AD%2598

##実際の記述

完成した記述が以下になります。

```PHP:FileUpController.php
use Illuminate\Support\Str;
use DB;

public function store(Request $request)
{
    // ①ファイル名の新規作成
    $fileExtension = $request->img->getClientOriginalExtension();
    $fileName = Str::random(40) . '.' . $fileExtension;


    // ここからトランザクション内部の処理
    DB::transaction(function () use ($request, $fileName) {

        // ②データベースに保存する内容の設定
        $fileImageTable = new FileImage;
        $fileImageTable->file_name = $fileName;

        // ③データベースへの登録とファイルの保存
        // データベースの登録がうまくいったら(trueなら)
        if ($fileImageTable->save()) {
            // storage>app>public>imagesにファイル名を$fileNameで保存
            // $filePathには相対パス public/images/ファイル名.拡張子が代入されます
            $filePath = $request->img->storeAs('public/images', $fileName);
        }

        // ④ファイルの保存に失敗した場合に例外を投げてロールバックさせる
    });

    // ここまでトランザクション内部の処理

⑤リダイレクト
}
```

**<font color="red">【注意事項】</font>**
<font color="red">save()は返り値にboolean(true又はfalse)を返してくれますが、create()の場合などは返り値が違うので注意が必要です。</font>

参考にしたサイト
https://qiita.com/HorikawaTokiya/items/679b5d3b1cfe1c3b2f71

#トランザクション内部処理 ④ファイルの保存に失敗した場合に例外を投げてロールバックさせる

画像保存に失敗した場合、ロールバックが行われるように自前で例外(Exception)を作成します。

##Storage::missing()の使用
ロールバックの条件分岐を作成するのに、Storage::missing()を使用します。
Storage::missing()でファイルの存在を確認し、ファイルがなければ(保存に失敗してるので)例外を投げてロールバックさせます。

##StorageファサードとFileファサード
上で記載した、Storage::missingは

```PHP
// 先頭に必ず追記
use Illuminate\Support\Facades\Storage;

Storage::missing('filePath');
```

Fileファサードを使用しても同じ結果が得られます。

```PHP
// 先頭に必ず追記
use Illuminate\Support\Facades\File;

File::missing('filePath');
```

書き方こそ一緒ですが、その位置づけは違います。

簡単にまとめるとこんな感じ
※素人のまとめなので、間違いがあるかもしれません。

|ファサード名|位置づけ|
|:---|:---|
|File|PHPのファイル処理系関数のラップ版|
|Storage|強力なファイルシステムの抽象化版|

見た目にはFileファサードの方が見やすいですが、Storageファサードに慣れていた方がいいようです。

参考にしたサイト
https://reffect.co.jp/laravel/laravel-storage-manipulation-master
https://www.sejuku.net/blog/63671
https://www.366service.com/jp/qa/657d0d18e7b302e241cb219b82b79ea2

##番外編 Storage::exists()

ファイルの存在確認ですが、
Storage::existsでもファイルの存在確認が行えます。

違いは以下の通りで、返り値が逆になります。

|メソッド名|ファイルがある時の返り値|ファイルがない時の返り値|
|:---|:---|:---|
|exists|true|false|
|missing|false|true|

---
リファレンス
https://readouble.com/laravel/6.x/ja/filesystem.html?header=%25E3%2583%2595%25E3%2582%25A1%25E3%2582%25A4%25E3%2583%25AB%25E5%258F%2596%25E5%25BE%2597


##実際の記述

完成した記述が以下になります。

```PHP:FileUpController.php
use Illuminate\Support\Str;
use DB;
// 例外を投げるために先頭に追記
use Exception;

public function store(Request $request)
{
    // ①ファイル名の新規作成
    $fileExtension = $request->img->getClientOriginalExtension();
    $fileName = Str::random(40) . '.' . $fileExtension;


    // ここからトランザクション内部の処理
    DB::transaction(function () use ($request, $fileName) {

        // ②データベースに保存する内容の設定
        $fileImageTable = new FileImage;
        $fileImageTable->file_name = $fileName;

        // ③データベースへの登録とファイルの保存
        if ($fileImageTable->save()) {
            $filePath = $request->img->storeAs('public/images', $fileName);
        }

        // ④ファイルの保存に失敗した場合に例外を投げてロールバックさせる
        // 保存したファイルの存在確認 存在しなけばtrueが返る
        if (Storage::missing($filePath)) {
            // 例外を投げてロールバックさせる
            throw new Exception('画像ファイルの保存に失敗しました。');
        }
    });

    // ここまでトランザクション内部の処理

⑤リダイレクト
}
```

#⑤リダイレクト

ここはまったく変更なしです。

```PHP
return redirect('/file')->with('success', '登録完了しました。');
```

#完成形

```PHP:FileUpController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Models\FileImage;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use DB;
use Exception;

class FileUpController extends Controller
{
    public function store(Request $request)
    {
        $fileExtension = $request->img->getClientOriginalExtension();
        $fileName = Str::random(40) . '.' . $fileExtension;

        DB::transaction(function () use ($request, $fileName) {

            $fileImageTable = new FileImage;
            $fileImageTable->file_name = $fileName;

            if ($fileImageTable->save()) {
                $filePath = $request->img->storeAs('public/images', $fileName);
            }

            if (Storage::missing($filePath)) {
                throw new Exception('画像ファイルの保存に失敗しました。');
            }
        });

        return redirect('/file')->with('success', '登録完了しました。');
    }
}
```