Edit this Page

Transactions and Commands

ORM component includes convenient way to property create or update multiple models in one transaction without being worry about different databases.

Example

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).

Commands

Internally, transactions work 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.

Command Events

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 and delete events of your models.

Primary and Foreign Keys

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

ActiveRecord and Transactions

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.

Multiple Commands

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();

Transaction Scope

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 and TransactionInterface to create project specific UoW implementation.