Increase and decrease in stock quantity
We implemented simple create and save in Create and save invoices, but it does not satisfy the requirements. Let's add them one by one.
In this chapter, you can learn about the following contents:
- Use event handlers on
save
ordelete
. - How to use server cursor.
- Override
save
anddelete
.
Mechanism of increase and decrease
The stock number decreases when the product are sold, and increases with arrival or returns. This example does not have arrival process, we will implement sales, returns and deleting invoice.
It is linked with saving and deleting of InvoiceItem
. Let's write increase/decrease processes. After that we call it from event handers of InvoiceItem
.
Define support class InvoiceItemSaveHandler
.
class InvoiceItemSaveHandler
{
}
Safe and fast processing with server cursor
onSaveRow
decreases the stock number if this row is sales line. If there is a stock record of the product, this method updates it. Otherwise, this method save a new record. It also updates the object's cache.
Use serverCursor
for fast searching and saving.
class InvoiceItemSaveHandler
{
public function onSaveRow(InvoiceItem $row)
{
if ($row->line_type === InvoiceItem::SALES) {
// Get server cursor
$it = Stock::keyValue($row->product_code)->serverCursor();
// Check whether it can be read
if ($it->valid()) {
// If there is a stock record of the product, get Stock object.
// This object is the most recent and locked by transaction on the server.
$stock = $it->current();
$stock->quantity -= $row->quantity;
$it->update($stock);
} else {
// If there is no stock record of the product, create Stock object.
$stock = new Stock(['code' => $row->product_code]);
$stock->quantity = 0 - $row->quantity;
$it->insert($stock);
}
// Update Stock cache
$stock->updateCache();
}
}
}
Note: Server cursor is very powerful and easy to use.
In transaction, the current record of the server cursor is locked, so its value is the most recent. Increasing or decreasing the value can safely be updated with this value.
Out of transaction, reading does not lock any records without $lockBias
parameter.
Define onDeleteRow
in the same way.
class InvoiceItemSaveHandler
{
// (omitted)
public function onDeleteRow(InvoiceItem $row)
{
if ($row->line_type === InvoiceItem::SALES) {
$it = Stock::keyValue($row->product_code)->serverCursor();
// The stock record of the product must exists when the invoice is deleted.
// If there is no records, throw exception.
$it->validOrFail();
// This object is the most recent and locked by transaction on the server.
$stock = $it->current();
$stock->quantity += $row->quantity;
$it->update($stock);
$stock->updateCache();
}
}
}
Binding event handler
Define save and delete handlers on InvoiceItem
and call above methods.
Define hanlders
In this case, we use handlers saving
and deleting
. If you define following methods, they will be called automatically when saving or deleting.
class InvoiceItem extends Model
{
// (omitted)
public static function deleting(InvoiceItem $row)
{
return true;
}
public static function saving(InvoiceItem $row)
{
return true;
}
}
Add some process
These handlers are static method. We need to them to be able to use InvoiceItemSaveHandler
instance. Define a static variable and set instance to it before using. Then we can call onDeleteRow
or onSaveRow
.
class InvoiceItem extends Model
{
// (omitted)
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: Although you can write the increase or decrease process directly in deleting
or saving
handler, we made it to another class in this case. Because it is an event process which spans two classes of InvoiceItem
and Stock
.
This is also useful when you want to add related processing with another class.
The finishing touch
Override save
and delete
in Invoice
, to create InvoiceItemSaveHandler
instance and set it to InvoiceItem::$handler
.
class Invoice extends Model
{
// (omitted)
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);
}
The reason to create an instance of the handler class each time is that the handler will hold member variables in the next step. It is created every time to prevent bugs such as invalid initialization of member variables.
Now, the stock will be increased or decreased automatically when you call $invoice->saveWithTransaction()
.
Source code
The source code of this chapter can be downloaded from GitHub Gist.