Skip to content

Commit 65fcd2d

Browse files
committed
feat: Add a callback to be called on transaction failure
1 parent 1e37ba6 commit 65fcd2d

File tree

4 files changed

+51
-6
lines changed

4 files changed

+51
-6
lines changed

src/Illuminate/Database/Concerns/ManagesTransactions.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ trait ManagesTransactions
2020
*
2121
* @throws \Throwable
2222
*/
23-
public function transaction(Closure $callback, $attempts = 1)
23+
public function transaction(Closure $callback, $attempts = 1, ?Closure $onFailureCallback = null)
2424
{
2525
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
2626
$this->beginTransaction();
@@ -37,7 +37,7 @@ public function transaction(Closure $callback, $attempts = 1)
3737
// exception back out, and let the developer handle an uncaught exception.
3838
catch (Throwable $e) {
3939
$this->handleTransactionException(
40-
$e, $currentAttempt, $attempts
40+
$e, $currentAttempt, $attempts, $onFailureCallback
4141
);
4242

4343
continue;
@@ -78,11 +78,12 @@ public function transaction(Closure $callback, $attempts = 1)
7878
* @param \Throwable $e
7979
* @param int $currentAttempt
8080
* @param int $maxAttempts
81+
* @param Closure|null $onFailureCallback
8182
* @return void
8283
*
8384
* @throws \Throwable
8485
*/
85-
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
86+
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts, ?Closure $onFailureCallback)
8687
{
8788
// On a deadlock, MySQL rolls back the entire transaction so we can't just
8889
// retry the query. We have to throw this exception all the way out and
@@ -108,6 +109,10 @@ protected function handleTransactionException(Throwable $e, $currentAttempt, $ma
108109
return;
109110
}
110111

112+
if ($onFailureCallback !== null) {
113+
$onFailureCallback($e);
114+
}
115+
111116
throw $e;
112117
}
113118

src/Illuminate/Database/ConnectionInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public function prepareBindings(array $bindings);
135135
*
136136
* @throws \Throwable
137137
*/
138-
public function transaction(Closure $callback, $attempts = 1);
138+
public function transaction(Closure $callback, $attempts = 1, ?Closure $onFailureCallback = null);
139139

140140
/**
141141
* Start a new database transaction.

src/Illuminate/Database/SqlServerConnection.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ public function getDriverTitle()
3131
*
3232
* @throws \Throwable
3333
*/
34-
public function transaction(Closure $callback, $attempts = 1)
34+
public function transaction(Closure $callback, $attempts = 1, ?Closure $onFailureCallback = null)
3535
{
3636
for ($a = 1; $a <= $attempts; $a++) {
3737
if ($this->getDriverName() === 'sqlsrv') {
38-
return parent::transaction($callback, $attempts);
38+
return parent::transaction($callback, $attempts, $onFailureCallback);
3939
}
4040

4141
$this->getPdo()->exec('BEGIN TRAN');
@@ -55,6 +55,10 @@ public function transaction(Closure $callback, $attempts = 1)
5555
catch (Throwable $e) {
5656
$this->getPdo()->exec('ROLLBACK TRAN');
5757

58+
if ($a === $attempts && $onFailureCallback !== null) {
59+
$onFailureCallback($e);
60+
}
61+
5862
throw $e;
5963
}
6064

tests/Integration/Database/DatabaseTransactionsTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,42 @@ public function testTransactionsDoNotAffectDifferentConnections()
105105
$this->assertTrue($secondObject->ran);
106106
$this->assertFalse($thirdObject->ran);
107107
}
108+
109+
public function testOnErrorCallbackIsCalled()
110+
{
111+
$executed = false;
112+
try {
113+
DB::transaction(function () {
114+
throw new \Exception;
115+
}, 1, function () use (&$executed) {
116+
$executed = true;
117+
});
118+
} catch (\Throwable) {
119+
// Ignore the exception
120+
}
121+
122+
$this->assertTrue($executed);
123+
}
124+
125+
public function testOnErrorCallbackIsCalledWithDeadlockRetry()
126+
{
127+
$executed = false;
128+
$attempts = 0;
129+
130+
try {
131+
DB::transaction(function () use (&$attempts) {
132+
$attempts += 1;
133+
throw new \Exception('has been chosen as the deadlock victim');
134+
}, 3, function () use (&$executed) {
135+
$executed = true;
136+
});
137+
} catch (\Throwable) {
138+
// Ignore the exception
139+
}
140+
141+
$this->assertSame(3, $attempts);
142+
$this->assertTrue($executed);
143+
}
108144
}
109145

110146
class TestObjectForTransactions

0 commit comments

Comments
 (0)