前回の「個数管理」のコードに少し肉付けをして実際に近いケースを考慮してみます。

例えば、Eコマースにおいてチェックアウトの際にカートに追加したアイテムの在庫を確保するとします。しかし、カートには複数のアイテムが含まれるかもしれないので、すべてのアイテムにおいて在庫確保成功とはいかないかもしれません。在庫数の更新失敗のアイテムがあるときにはすでに在庫確保したアイテムも元に戻す必要あります。

Checkoutというクラスを作成し、reserveInventoryというメソッドを作成します。
DBのトランザクションとカスタムExceptionに注目してください。


namespace App;

use DB;
use Exception;
use App\Product;
use App\Exceptions\InventoryException;

use Illuminate\Database\Eloquent\Model;

class Checkout extends Model
{
    public static function reserveInventory($ids)
    {
        try {
            DB::beginTransaction();

            foreach($ids as $id  => $quantity) {
                $count = Product::query()
                    ->where('id', '=', $id)
                    ->whereRaw("inventory - $quantity >= 0")
                    ->decrement('inventory', $quantity);

                if ($count == 0) {
                    throw new InventoryException(); // 在庫がなくて在庫数を更新できなかったとき
                }
            }

            DB::commit();

        } catch (InventoryException $e) {
            DB::rollBack();

            return '購入しようとしている商品の在庫が十分にありません';

        } catch (Exception $e) {
            DB::rollBack();

            return 'DBエラーが発生しました' . $e->getMessage();
        }

        return '在庫確保成功!';
    }
}

reserveInventory()の引数は、product.idquantityの値の配列関数です。例えば、product.id = 1で購入個数1個なら、[ 1 => 2 ]

上で使用されている、InventoryExceptionの定義は、


namespace App\Exceptions;

use Exception;

class InventoryException extends Exception
{
    public function __construct($message = '') {
        parent::__construct($message);
    }
}

さて、tinkerで実行してみましょう。

まず、商品を2つ用意します。商品1の在庫は2で、商品2の在庫は1とします。

>>> App\Product::all()
=> Illuminate\Database\Eloquent\Collection {#3059
     all: [
       App\Product {#3083
         id: 1,
         name: "商品1",
         price: "1000",
         inventory: 2,
         created_at: "2020-03-27 19:35:37",
         updated_at: "2020-04-02 23:06:36",
       },
       App\Product {#3071
         id: 2,
         name: "商品2",
         price: "2000",
         inventory: 1,
         created_at: "2020-04-02 23:05:52",
         updated_at: "2020-04-02 23:05:52",
       },
     ],
   }

次に、カートのアイテムを用意して在庫確保します。

商品1を1個、商品2を1個購入です。

>>> $products = [ 1 => 1, 2 => 1 ];
=> [
     1 => 1,
     2 => 1,
   ]
>>> Cart::reserveInventory($products);
=> "在庫確保成功!"
>>> App\Product::all();
=> Illuminate\Database\Eloquent\Collection {#3060
     all: [
       App\Product {#3075
         id: 1,
         name: "商品1",
         price: "1000",
         inventory: 1,
         created_at: "2020-03-27 19:35:37",
         updated_at: "2020-04-02 23:08:53",
       },
       App\Product {#3078
         id: 2,
         name: "商品2",
         price: "2000",
         inventory: 0,
         created_at: "2020-04-02 23:05:52",
         updated_at: "2020-04-02 23:08:53",
       },
     ],
   }

商品1の在庫は1で、商品2の在庫は0となりました。うまく在庫数に反映されています。

更新失敗を見るために、もう1度在庫確保を実行してみます。

>>> Cart::reserveInventory($products);
=> "購入しようとしている商品の在庫が十分にありません"
>>> App\Product::all();
=> Illuminate\Database\Eloquent\Collection {#3070
     all: [
       App\Product {#3072
         id: 1,
         name: "商品1",
         price: "1000",
         inventory: 1,
         created_at: "2020-03-27 19:35:37",
         updated_at: "2020-04-02 23:08:53",
       },
       App\Product {#3082
         id: 2,
         name: "商品2",
         price: "2000",
         inventory: 0,
         created_at: "2020-04-02 23:05:52",
         updated_at: "2020-04-02 23:08:53",
       },
     ],
   }

今度は、在庫エラーが出ました。商品1は更新されますが、商品2においては在庫がないのでInventoryExceptionthrowされ、トランザクションのためにDBへの変更はロールバックされて商品1の変更ももとに戻ります。

以上は、シンプル化された例なので、改良点はたくさんありそうです。エラー文に商品名を入れるとか、在庫エラーを管理者にメールで伝えるとか。

By khino