モデルを使う

完成したモデルを実際に使ってみましょう。

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

伝票処理

追加

2点の売上と入金をします。

try
{
    $cust = Customer::find(1);

    $inv = new Invoice;
    $inv->customer()->associate($cust);
    $inv->addSalesLine('APPLE1', 12);
    $inv->addSalesLine('ORANGE1', 2);
    $inv->addPaymentLine(4579, 'Cash');
    $inv->saveWithTransaction();
    // 残高の表示
    echo $cust->name . '様の残高は \\' . $inv->amount->balance . ' です。' . PHP_EOL;
} catch(Exception $e) {
    echo 'エラーが発生しました。詳細は「' . $e->message() . '」です。' . PHP_EOL;
}
Yamaha様の残高は \0 です。

読み取り

Invoiceレコードは明細の合計金額などの値を含んでいます。 InvoiceInvoiceItemは、トランザクションで書き込んだ一式を読み取ることで、矛盾の無いものとなります。

トランザクションで書き込んだ一式を読み取るには、以下のようにスナップショットを使って読み取る必要があります。

try
{
    DB::beginSnapshot();     // CONSISTENT_READの開始
    $inv = Invoice::find(1); // ここでInvoiceの読み取り
    if ($inv === null) {
        throw new Exception('伝票番号 1 は作成されていないか削除されました。');
    }
    $items = $inv->items;    // ここでInvoiceItemの読み取り
    $inv->endSnapshot();
    echo $inv->id . 'の伝票の行数は '. $items->count() . ' 行です。' . PHP_EOL;
} catch(Exception $e) {
    $inv->endSnapshot();
    echo 'エラーが発生しました。詳細は「' . $e->message() . '」です。' . PHP_EOL;
}

どのようなシチュエーションで矛盾が発生するのか説明しましょう。以下の図を見てください。

矛盾が発生する例

伝票を明細付きで読み取ると、Invoiceレコードへのアクセスと、InvoiceItemへのアクセスの2回の読み取りが発生します。このとき、別のユーザーが図のようなタイミングでその伝票を更新すると、InvoiceInvoiceItemでそれぞれ、変更前と変更後を読み取る可能性があります。

スナップショットを使った場合は、InnoDBのMVCC機能を使ってInvoiceInvoiceItemをスナップショットが開始された時点の一貫した内容で読み取ることができます。

スナップショットについてのより詳しい内容は、SDKの InnoDBのロックとその制御を参照してください。

Note: 読み取りで矛盾が起こる問題はTransactd PHP ORMに限った話ではありません。 SQLでのアクセスでもInvoiceInvoiceItemが個別のSQL文で読み取られる場合は同様です。 SQLにはbeginSnapshotはないので、BEGINもしくはSTART TRANSACTIONを使用します。

更新

先ほどの伝票の2番目の行を削除して確定します。

try
{
    $inv = Invoice::find(1);
    if ($inv === null) {
        throw new Exception('伝票番号 1 は作成されていないか削除されました。');
    }
    $inv->items->remove(1); // 2番目の行を削除
    $inv->saveWithTransaction();
    echo $inv->customer->name . '様の残高は \\' . $inv->amount->balance . 'です。' . PHP_EOL;
} catch(Exception $e) {
    echo 'エラーが発生しました。詳細は「' . $e->message() . '」です。' . PHP_EOL;
}
Yamaha様の残高は \-691 です。

削除

伝票番号1を削除します。

try
{
    $inv = Invoice::find(1);
    if ($inv === null) {
        throw new Exception('伝票番号 1 は作成されていないか削除されました。');
    }
    $inv->deleteWithTransaction();
    $echo '伝票番号 1 を削除しました。' . PHP_EOL;
} catch(Exception $e) {
    echo 'エラーが発生しました。詳細は「' . $e->message() . '」です。' . PHP_EOL;
}
伝票番号 1 を削除しました。

残高確認

この例では、customer_id = 3の日付順で最後の伝票を探します。

keyValueに1つ大きなcustomer_idを指定し、directionで後方から前方方向に検索することで最後の伝票を探します。

try
{
    $customer_id = 3;
    $invs = Invoice::index(1)->keyValue($customer_id + 1)
        ->where('customer_id', $customer_id)
        ->limit(1)->direction(Nstable::findBackForword)
        ->get();
    $inv = count($invs) ? $invs[0] : null;
    if ($inv === null) {
        throw new Exception('顧客番号 1 はまだ取引がありません。');
    }
    echo $inv->customer->name . '様の残高は \\' . $inv->amount->balance . 'です。' . PHP_EOL;
} catch(Exception $e) {
    echo 'エラーが発生しました。詳細は「' . $e->message() . '」です。' . PHP_EOL;
}
Yamaha様の残高は \0です。

得意先の1ヶ月分の取引を集計する

customer_id = 3の月間売上金額を集計してみましょう。

try
{
    $customer = Customer::find(3);
    $invs = $customer->transactions('2016-12-01', '2016-12-31');
    $total = 0;
    if (count($invs) > 0) {
        foreach($invs as $inv) {
            $total += $inv->amount->sales;
        }
    }
    echo $customer->name . '様の月間売上金額は \\' . $total . ' です。' . PHP_EOL;
} catch(Exception $e) {
    echo 'エラーが発生しました。詳細は「' . $e->message() . '」です。' . PHP_EOL;
}
Yamaha様の月間売上金額は \4240 です。

1日の売上と回収金額を求める

2016-12-02の売上と回収金額を求めます。

DailySummaryがあるので、たった1レコードのアクセスで求めることができます。

$date = '2016-12-02';
$summary = DailySummary::find($date);
if ($summary === null) {
    echo $date . 'の取引はありません。' . PHP_EOL;
} else {
    echo $date . '  売上 : \\' . $summary->sales . '  回収 : \\' . $summary->payment . PHP_EOL;
}
2016-12-02  売上 : \4240  回収 : \4579

特定の日付の伝票リスト

2016-12-02のすべての伝票をリストアップします。 DailySummaryのリレーションを使ってみます。

$date = '2016-12-02';
$summary = DailySummary::find($date);
if ($summary !== null) {
    $invoices = $summary->invoices;
}
foreach($invoices as $invoice) {
    // (省略)
}

指定した期間の売上合計を求める

2016-12の売上合計を求めてみます。 DailySummaryによって最大31レコードを読み取るだけで1ヶ月分がわかります。

$startDate = '2016-12-01';
$endDate = '2016-12-31';
$summaries = DailySummary::keyValue($startDate)->where('date', '<=', $endDate)->get();
$total = 0;
foreach($summaries as $summary) {
    $total += $summary->sales;
}
echo $startDate . ' 〜 ' . $endDate . ' の合計売上金額は \\' . $total . ' です。' . PHP_EOL;
2016-12-02 〜 2016-12-31 の合計売上金額は \4240 です。

価格確認

APPLE1の価格を調べます。

$code = 'APPLE1';
$product = Product::find('APPLE1');
echo $code . 'の価格は \\' . $product->price . ' です。' . PHP_EOL;
APPLE1の価格は \300 です。

在庫数確認

APPLE1の在庫数を調べます。

$code = 'APPLE1';
$stock = Stock::find('APPLE1');
if ($stock === null) {
    echo $code . 'の在庫はありません。' . PHP_EOL;
} else {
    echo $code . 'の在庫数は ' . $stock->quantity . '個です。' . PHP_EOL;
}
APPLE1の在庫数は 108個です。

在庫一覧

在庫一覧表を表示します。

$stocks = Stock::all();
foreach($stocks as $stock) {
    echo sprintf('%-15s : %8d', $stock->code, $stock->quantity) . PHP_EOL;
}
APPLE1          :      108
APPLE2          :      240
BIKE650         :        5
CAR123          :        1
ORANGE1         :       52
WATCH777        :       10

商品在庫一覧

商品在庫一覧を表示します。withメソッドを使って高速に処理します。

function showProductList($products)
{
    echo sprintf('%-15s %-15s %8s %8s', 'product_code', 'description',
                    'price', 'quantity') . PHP_EOL;
    echo '---------------------------------------------------' . PHP_EOL;
    foreach($products as $product) {
        echo sprintf('%-15s %-15s %8d %8d',
                    $product->code,
                    $product->description,
                    $product->price,
                    ($product->stock === null) ? 0 : $product->stock->quantity)
                    . PHP_EOL;
}

$products = Product::with('stock')->all();
showProductList($products);
product_code    description        price quantity
---------------------------------------------------
CAR123          CAR 123          5000000        1
BIKE650         BIKE 650          320000        5
WATCH777        WATCH 777         198000       10
APPLE1          APPLE FUJI           300      108
APPLE2          APPLE SHINANO        280      240
ORANGE1         ORANGE UNSYUU        320       52

JSONへの変換

伝票番号2をJSONにします。

$inv = Invoice:find(2);
$inv->items; // items のLazy load
$json = $inv->toJson();
echo $json;

フォーマットされたJSON

{
  "id": 2,
  "date": "2016-12-18",
  "update_at": "2016-12-19 16:35:24.627492",
  "amount": {
    "oldBlance": null,
    "sales": 198000,
    "tax": 15840,
    "payment": 10000,
    "balance": 203840,
    "className": "InvoiceAmount"
  },
  "className": "Invoice",
  "customer_id": 3,
  "note": "",
  "items": {
    "saveOprions": 6,
    "className": "Transactd\\Collection",
    "array": {
      "0": {
        "className": "InvoiceItem",
        "invoice_id": 2,
        "row": 1,
        "line_type": 0,
        "product_code": "WATCH777",
        "product_description": "WATCH 777",
        "price": 198000,
        "quantity": 1,
        "amount": 198000,
        "tax": 15840,
        "note": "",
        "update_at": "2016-12-19 16:35:24.630626"
      },
      "1": {
        "className": "InvoiceItem",
        "invoice_id": 2,
        "row": 2,
        "line_type": 1,
        "product_code": "PAYMENT",
        "product_description": "Cash",
        "price": 0,
        "quantity": 0,
        "amount": 10000,
        "tax": 0,
        "note": "",
        "update_at": "2016-12-19 16:35:24.630626"
      }
    }
  }
}

JSONからオブジェクトへの変換

上記で取得したJSON文字列からオブジェクトにし保存します。

$inv = Invoice::fromJson($json);
$inv->saveWithTransaction();
  1. 日計の記録
  2. ソースコード