Transactions
While Connection
has usual methods for controlling transactions and savepoints,
the suggested approach is to use atomic()
method. It takes a callable and executes it atomically, reducing
the need for boilerplate code.
Running a callable atomically
Using atomic()
to execute a callable
$connection->atomic(function () {
// do some stuff
});
is roughly equivalent to
$connection->beginTransaction();
try {
// do some stuff
$connection->commit();
} catch (\Throwable $e) {
$connection->rollback();
throw $e;
}
atomic()
calls can be nested, the inner call may create a savepoint
(this behaviour is controlled by a second argument to atomic()
and is disabled by default) and
thus be rolled back without affecting the whole transaction:
// note that connection object will be passed as an argument to callback
$stored = $connection->atomic(function (Connection $connection) {
storeSomeRecords();
try {
// We know that the function may fail due to some unique constraint violation
// and are perfectly fine with that, so request a savepoint for inner atomic block
$connection->atomic(function () {
populateSomeDictionaries();
}, true);
} catch (ConstraintViolationException $e) {
// even if the inner atomic() failed the outer atomic may proceed
}
return storeSomethingElse();
});
Note
The example above shows the correct way to catch errors with atomic, that is around atomic()
call.
As atomic()
looks at exceptions to know whether callback succeeded or failed, catching and
handling exceptions around individual queries will break that logic. If necessary, add another atomic()
call for these queries.
Internally atomic()
does the following
opens a transaction in the outermost
atomic()
call;creates a savepoint when entering an inner
atomic()
call;performs a callback, whatever it returns will be returned by
atomic()
;releases or rolls back to the savepoint when exiting an inner call;
commits or rolls back the transaction when exiting the outermost call.
If savepoint wasn’t created for an inner call, atomic()
will perform
the rollback when exiting the first parent call with a savepoint if
there is one, and the outermost call otherwise.
Note
If a transaction was already open before an outermost atomic()
call made with $savepoint = false
,
it will not be committed or rolled back on exit, you’ll have to do it explicitly. If an error happens,
atomic()
will, however, mark the transaction “for rollback only”.
Performing actions after transaction
Sometimes you need to perform an action related to the current database transaction, but only if the transaction successfully commits, e.g. send an email notification, or invalidate a cache. You may also need to do some cleanup after a rollback.
Connection
has methods for registering callbacks that will run after
commit and rollback: onCommit()
and onRollback()
. You can only
use these methods inside atomic()
, outside you’ll get
BadMethodCallException
.
$connection->atomic(function (Connection $connection) {
$connection->onCommit(function () {
sendAnEmail();
resetACache();
});
$connection->onRollback(function () {
resetSomeModelProperties();
clearSomeFiles();
});
});
Savepoints created by nested atomic()
calls are handled correctly.
If inner atomic()
call fails, and the transaction is rolled back to
savepoint, then onCommit()
callbacks registered within that call and
nested atomic()
calls will not run after transaction commit. Their
onRollback()
callbacks will run instead.
Callbacks are executed outside the transaction after a commit or
rollback. This means that an error in onCommit()
callback will not
cause a rollback.
Note
While Connection
takes reasonable precautions to run onRollback()
callbacks in case of implicit rollback
(lost connection to database while in transaction, script exit()
while in transaction), it is possible that
the script terminates in such a way that callbacks will not run.