在庫の増減

伝票の作成と保存で伝票を作成して保存できるようにはなりましたが、要件を満たしていない機能があるので、それらを順に追加していきましょう。

この章では以下の内容について学習できます。

増減の仕組み

在庫は、商品が売り上げられると減り、入荷や返品があると増えます。このサンプルでは入荷処理は行わないので、売上があると減り、返品や伝票削除があると増えるようにします。

商品ごとの処理なので、InvoiceItemの保存と削除に連動させます。まずは処理を書いてみましょう。そして後で、InvoiceItemの保存と削除のイベントハンドラから呼び出すようにします。

InvoiceItem保存の補助クラスとしてInvoiceItemSaveHandlerクラスを作成します。

class InvoiceItemSaveHandler
{
}

サーバーカーソルによる安全で高速な処理

保存時に行が売上なら在庫を減らすメソッドonSaveRowを作成します。このメソッドでは、すでにその商品の在庫レコードがあれば更新し、なければ新規作成して保存します。また、オブジェクトのキャッシュも更新します。

高速な検索と保存にはserverCursorを使用します。

class InvoiceItemSaveHandler
{
    public function onSaveRow(InvoiceItem $row)
    {
        if ($row->line_type === InvoiceItem::SALES) {
            // サーバーカーソルの取得
            $it = Stock::keyValue($row->product_code)->serverCursor();

            // 読み取れたか確認する
            if ($it->valid()) {
                // その商品の在庫レコードがある場合は、在庫オブジェクトを取得
                // このオブジェクトはトランザクションによってサーバーでロックされた最新のもの
                $stock = $it->current();
                $stock->quantity -= $row->quantity;
                $it->update($stock);
            } else {
                // その商品の在庫レコードがない場合は、新規作成する
                $stock = new Stock(['code' => $row->product_code]);
                $stock->quantity = 0 - $row->quantity;
                $it->insert($stock);
            }

            // 在庫オブジェクトのキャッシュを更新
            $stock->updateCache();
        }
    }
}

Note: サーバーカーソルは非常に強力で簡単です。

トランザクション内であれば、サーバーカーソルのカレントレコードはロックされるため、最新の情報であることが保証されます。値の増減などはこの値を元に行えば安全に正しく更新できます。

トランザクション外の場合は、$lockBiasパラメータを指定しないかぎり、読み取りでのロックはされません。

同じように行が削除されたときのメソッドonDeleteRowを作成します。

class InvoiceItemSaveHandler
{
    // (省略)

    public function onDeleteRow(InvoiceItem $row)
    {
        if ($row->line_type === InvoiceItem::SALES) {
            $it = Stock::keyValue($row->product_code)->serverCursor();

            // 伝票が削除される際には、在庫レコードは必ず存在するはずである。
            // レコードが無い場合は即時例外をスローする。
            $it->validOrFail();

            // このオブジェクトはトランザクションによってサーバーでロックされた最新のもの
            $stock = $it->current();
            $stock->quantity += $row->quantity;
            $it->update($stock);
            $stock->updateCache();
        }
    }
}

ハンドラの組み込み

InvoiceItemの保存と削除時のハンドラを定義して、先ほどのメソッドを呼び出すようにしましょう。

ハンドラの定義

ハンドラはsavingdeletingを使用します。以下のメソッドを定義すると、保存と削除時に自動で呼び出されます。

class InvoiceItem extends Model
{
    // (省略)

    public static function deleting(InvoiceItem $row)
    {
        return true;
    }

    public static function saving(InvoiceItem $row)
    {
        return true;
    }
}

処理の追加

これらのハンドラはスタティックなメソッドなので、InvoiceItemSaveHandlerのインスタンスを参照できるように、スタティックな変数を用意して事前にそれをセットしておきます。そしてそれぞれでonDeleteRowonSaveRowを呼び出します。

class InvoiceItem extends Model
{
    // (省略)

    public static $handler = null;

    public static function deleting(InvoiceItem $row)
    {
        self::$handler->onDeleteRow($row);
        return true;
    }

    public static function saving(InvoiceItem $row)
    {
        self::$handler->onSaveRow($row);
        return true;
    }
}

Note: deletingsavingのハンドラ内に直接在庫の増減を書いてもよいのですが、今回はInvoiceItemStockの2つのクラスにまたがる特定のイベントでの処理なので、別のクラスにしました。

このようにすると、明細保存時にさらに別のクラスに関わる処理を追加したくなったときにも便利です。

仕上げ

仕上げにInvoicesavedeleteをオーバーライドして、 InvoiceItemSaveHandlerの生成とInvoiceItem::$handlerへのセットを行います。

class Invoice extends Model
{
    // (省略)

    public function save($options = 0, $forceInsert = false)
    {
        InvoiceItem::$handler = new InvoiceItemSaveHandler();
        return parent::save($options, $forceInsert);
    }

    public function delete($options = null)
    {
        InvoiceItem::$handler = new InvoiceItemSaveHandler();
        return parent::delete($options);
    }

わざわざハンドラのインスタンスを保存の都度生成するのは、この後の処理でハンドラにメンバ変数を持つようにするためです。メンバ変数の初期化バグなどを防止するために都度生成にしています。

これで$invoice->saveWithTransaction()を呼び出すと在庫も増減されるようになりました。

ソースコード

ここまでのソースコードはGitHub Gistからダウンロードできます。

  1. 伝票の作成と保存
  2. 伝票合計の計算と保存