ORM component includes convenient way to property create or update multiple models in one transaction without being worry about different databases.
We can demonstrate simple transaction example using following code:
$payment = new Payment();
$payment->user = $user;
$payment->amount = $amount;
$payment->user->balance -= $amount;
To store this records inside one transaction:
$transaction = new Transaction();
$transaction->store($payment);
$transaction->run();
Consider using atomic number accessor for money management
$user->balance->dec(10.0)
.
Internally, transactions works using set of commands generated by Records or other components, you can write you own CommandInterface
implementation or use existed abstractions:
$transaction->store($user);
$tr->addCommand(new CallbackCommand(function () {
//do something
}));
Any failure inside commands will automatically rollback this transaction.
If you want to handle Command flow, use methods onExecute
, onRollback
and onComplete
of your commands:
$command = new CallbackCommand(function () {
//do something
});
$command->onComplete(function () {
echo 'complete';
});
$command->onExecute(function () {
echo 'executed';
});
$command->onRollBack(function () {
echo 'rollback';
});
$tr = new Transaction();
$tr->addCommand($command);
$tr->run();
You can get access to commands generated by Records by handling
create
,update
anddelete
events of your models.
Please note, that transaction command will only be evaluated when method run
is called. Until then PK and FKs on your models will stay empty:
$tr = new Transaction();
$user = new User();
$tr->store($user);
dump($user->primaryKey()); //null
$tr->run();
dump($user->primaryKey()); //not empty
When you use Record
base class for you models, you are getting access to AR approach with save
method included into your model.
Internally, this method will instantiate transaction object and use it to store model data immediately. Though, you are still able to use AR approach with external transaction:
$transaction = new Transaction();
$user = new User();
$user->save($transaction);
$transaction->run();
Same is true for
delete
method.
Spiral Record and RecordEntity models support ability to snapshot model data and state for each save operation even for newly created objects:
$tr = new Transaction();
$user = new User();
$tr->store($user); //INSERT
$user->balance+=10;
$tr->store($user); //UPDATE
$tr->save();
Though, it is recommended to use proper UnitOfWork implementation you are able to use transaction as common command aggregator for your application. In order to do that, we can create middleware used to define transaction scope:
class TransactionMiddleware extends Service implements MiddlewareInterface
{
public function __invoke(Request $request, Response $response, callable $next)
{
$transaction = new Transaction();
$scope = $this->container->replace(Transaction::class, $transaction);
try {
return $next($request, $response);
} finally {
$transaction->run();
$this->container->restore($scope);
}
}
}
Do not forget to add middleware into http config or a desired route.
We can now request transaction instance using dependency or shortcut (if you have any):
protected function indexAction(Transaction $transaction)
{
$user = new User();
$user->name = 'Antony';
$transaction->store($user);
//No 'run' call here
}
Comment $transaction->run();
line in your middleware to execute this code in dry mode (no data will be pushed into database).
You can use
Transaction
andTransactionInterface
to create project specific UoW implementation.