diff --git a/src/Concerns/ManagesTransactions.php b/src/Concerns/ManagesTransactions.php index 6403cc45d..289d4d6ca 100644 --- a/src/Concerns/ManagesTransactions.php +++ b/src/Concerns/ManagesTransactions.php @@ -9,8 +9,12 @@ use MongoDB\Driver\Exception\RuntimeException; use MongoDB\Driver\Session; use Throwable; +use TypeError; +use function assert; +use function get_debug_type; use function MongoDB\with_transaction; +use function sprintf; /** * @internal @@ -78,15 +82,27 @@ public function rollBack($toLevel = null): void } /** - * Static transaction function realize the with_transaction functionality provided by MongoDB. + * Static transaction function realize the {@see with_transaction} functionality provided by MongoDB. * - * @param int $attempts + * @param int $attempts + * @param array|Closure|null $options */ - public function transaction(Closure $callback, $attempts = 1, array $options = []): mixed + public function transaction(Closure $callback, $attempts = 1, array|Closure|null $options = null): mixed { + $options ??= []; + $onFailure = null; + /** $onFailure is a 3rd parameter introduced in Laravel 12.9.0 to {@see \Illuminate\Database\ConnectionInterface} */ + if ($options instanceof Closure) { + $onFailure = $options; + $options = []; + } elseif (isset($options['onFailure'])) { + assert($options['onFailure'] instanceof Closure, new TypeError(sprintf('Expected "onFailure" option to be a Closure or null, got %s', get_debug_type($options['onFailure'])))); + $onFailure = $options['onFailure']; + unset($options['onFailure']); + } + $attemptsLeft = $attempts; $callbackResult = null; - $throwable = null; $callbackFunction = function (Session $session) use ($callback, &$attemptsLeft, &$callbackResult, &$throwable) { $attemptsLeft--; @@ -110,6 +126,10 @@ public function transaction(Closure $callback, $attempts = 1, array $options = [ with_transaction($this->getSessionOrCreate(), $callbackFunction, $options); if ($attemptsLeft < 0 && $throwable) { + if ($onFailure) { + $onFailure($throwable); + } + throw $throwable; } diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php index f6a3cd509..506f0f88b 100644 --- a/tests/TransactionTest.php +++ b/tests/TransactionTest.php @@ -446,6 +446,52 @@ public function testRollBackWithoutSession(): void DB::rollback(); } + public function testOnErrorCallbackIsCalled() + { + $executed = 0; + try { + DB::connection('mongodb')->transaction(function () { + throw new class extends \MongoDB\Driver\Exception\RuntimeException { + protected $errorLabels = ['TransientTransactionError']; + }; + }, 1, function () use (&$executed) { + $executed++; + }); + + self::fail('Expected an exception to be thrown.'); + } catch (\MongoDB\Driver\Exception\RuntimeException) { + } + + $this->assertSame(1, $executed); + } + + public function testOnErrorCallbackIsCalledWithDeadlockRetry() + { + $executed = $attempts = 0; + + $connection = DB::connection('mongodb'); + self::assertInstanceOf(Connection::class, $connection); + + try { + $connection->transaction(function () use (&$attempts) { + $attempts += 1; + throw new class extends \MongoDB\Driver\Exception\RuntimeException { + protected $errorLabels = ['TransientTransactionError']; + }; + }, 3, [ + 'onFailure' => function () use (&$executed) { + $executed++; + }, + ]); + + self::fail('Expected an exception to be thrown.'); + } catch (\MongoDB\Driver\Exception\RuntimeException) { + } + + $this->assertSame(3, $attempts); + $this->assertSame(1, $executed); + } + private function getPrimaryServerType(): int { return DB::getMongoClient()->getManager()->selectServer()->getType();