Skip to content

Commit 1f2f326

Browse files
committed
feat(database): add closure transaction helper
Add ConnectionInterface::transaction() and implement it on BaseConnection as a small wrapper around the existing transaction API. - commit successful callback results and return callback values - roll back and rethrow callback exceptions - preserve existing false-return behavior for begin/status failures - support nested transaction state and transaction callbacks - document callback exception precedence and interface BC impact - add focused live and base connection coverage Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
1 parent 466d0f7 commit 1f2f326

7 files changed

Lines changed: 440 additions & 1 deletion

File tree

system/Database/BaseConnection.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,44 @@ public function afterRollback(callable $callback): static
10661066
return $this;
10671067
}
10681068

1069+
/**
1070+
* Run the callback inside a transaction.
1071+
*
1072+
* @param callable(self): mixed $callback
1073+
*/
1074+
public function transaction(callable $callback): mixed
1075+
{
1076+
if (! $this->transEnabled) {
1077+
return $callback($this);
1078+
}
1079+
1080+
if (! $this->transBegin()) {
1081+
return false;
1082+
}
1083+
1084+
try {
1085+
$result = $callback($this);
1086+
} catch (Throwable $e) {
1087+
try {
1088+
$this->transRollback();
1089+
} finally {
1090+
if ($this->transDepth > 0) {
1091+
$this->transStatus = false;
1092+
} elseif ($this->transStrict === false) {
1093+
$this->transStatus = true;
1094+
}
1095+
}
1096+
1097+
throw $e;
1098+
}
1099+
1100+
if (! $this->transComplete()) {
1101+
return false;
1102+
}
1103+
1104+
return $result;
1105+
}
1106+
10691107
/**
10701108
* Begin Transaction
10711109
*/

system/Database/ConnectionInterface.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ public function afterCommit(callable $callback): static;
133133
*/
134134
public function afterRollback(callable $callback): static;
135135

136+
/**
137+
* Run the callback inside a transaction.
138+
*
139+
* @param callable(self): mixed $callback
140+
*/
141+
public function transaction(callable $callback): mixed;
142+
136143
/**
137144
* Returns an instance of the query builder for this connection.
138145
*

tests/system/Database/BaseConnectionTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,40 @@ protected function _transRollback(): bool
624624
$this->assertSame(['rolled back'], $callbacks);
625625
}
626626

627+
public function testTransactionReturnsFalseWhenTransactionCannotBegin(): void
628+
{
629+
$callbackRan = false;
630+
631+
$db = new class ($this->options) extends MockConnection {
632+
protected function _transBegin(): bool
633+
{
634+
return false;
635+
}
636+
};
637+
638+
$result = $db->transaction(static function () use (&$callbackRan): void {
639+
$callbackRan = true;
640+
});
641+
642+
$this->assertFalse($result);
643+
$this->assertFalse($callbackRan);
644+
}
645+
646+
public function testTransactionRunsCallbackWhenTransactionsAreDisabled(): void
647+
{
648+
$db = new MockConnection($this->options);
649+
$db->transOff();
650+
651+
$result = $db->transaction(static function (BaseConnection $connection): string {
652+
$connection->afterCommit(static function (): void {});
653+
654+
return 'not wrapped';
655+
});
656+
657+
$this->assertSame('not wrapped', $result);
658+
$this->assertSame(0, $db->transDepth);
659+
}
660+
627661
public function testCallFunctionDoesNotDoublePrefixAlreadyPrefixedName(): void
628662
{
629663
$db = new class ($this->options) extends MockConnection {

0 commit comments

Comments
 (0)