-
Notifications
You must be signed in to change notification settings - Fork 2
transactions
Connection
class has usual methods for controlling transactions and savepoints: beginTransaction()
, commit()
,
rollback()
, inTransaction()
, createSavepoint()
, releaseSavepoint()
, rollbackToSavepoint()
. It also provides a convenience method for running a callable atomically, reducing the need for boilerplate
code that deals with opening and closing transactions:
$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 thus be rolled back without affecting the whole transaction:
// note that connection object will be passed as an argument to callback
$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
}
storeSomethingElse();
});
Note: The example above shows the correct way to catch errors with atomic, that is around
atomic()
call. Asatomic()
looks at exceptions to know whether callback succeeded or failed, catching and handling exceptions around individual queries will break that logic. If necessary, add anotheratomic()
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;
- 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 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".
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 runonRollback()
callbacks in case of implicit rollback (lost connection to DB while in transaction, scriptexit()
while in transaction), it is possible that the script terminates in such a way that callbacks will not run.