diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index adcb627f4b67..3b5750b0def2 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1024,6 +1024,14 @@ public function transStatus(): bool return $this->transStatus; } + /** + * Checks whether this connection is inside an active transaction. + */ + public function inTransaction(): bool + { + return $this->transDepth > 0; + } + /** * Register a callback to run after the outermost transaction commits. * diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index 3b1adeb373c6..6815c433a5e3 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -115,6 +115,11 @@ public function query(string $sql, $binds = null); */ public function simpleQuery(string $sql); + /** + * Checks whether this connection is inside an active CodeIgniter-managed transaction. + */ + public function inTransaction(): bool; + /** * Register a callback to run after the outermost transaction commits. * diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index 90eacc90f5fb..adf922ecd07f 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -558,6 +558,72 @@ public function testGetSessionTimezoneWithoutTimezoneKey(): void $this->assertNull($result); } + public function testInTransactionReflectsManagedTransactionState(): void + { + $db = new MockConnection($this->options); + + $this->assertFalse($db->inTransaction()); + + $this->assertTrue($db->transBegin()); + $this->assertTrue($db->inTransaction()); + + $this->assertTrue($db->transBegin()); + $this->assertTrue($db->inTransaction()); + + $this->assertTrue($db->transCommit()); + $this->assertTrue($db->inTransaction()); + + $this->assertTrue($db->transCommit()); + $this->assertFalse($db->inTransaction()); + } + + public function testInTransactionReturnsFalseWhenTransactionsAreDisabled(): void + { + $db = new MockConnection($this->options); + + $db->transOff(); + + $this->assertFalse($db->transBegin()); + $this->assertFalse($db->inTransaction()); + } + + public function testInTransactionReturnsTrueInsideTransactionCallback(): void + { + $db = new MockConnection($this->options); + $state = null; + + $result = $db->transaction(static function (BaseConnection $connection) use (&$state): string { + $state = $connection->inTransaction(); + + return 'done'; + }); + + $this->assertSame('done', $result); + $this->assertTrue($state); + $this->assertFalse($db->inTransaction()); + } + + public function testInTransactionReturnsFalseInsideTransactionCallbacks(): void + { + $db = new MockConnection($this->options); + $commitState = null; + $rollbackState = null; + + $this->assertTrue($db->transBegin()); + $db->afterCommit(static function () use ($db, &$commitState): void { + $commitState = $db->inTransaction(); + }); + $this->assertTrue($db->transCommit()); + $this->assertFalse($commitState); + + $this->assertTrue($db->transBegin()); + $db->afterRollback(static function () use ($db, &$rollbackState): void { + $rollbackState = $db->inTransaction(); + }); + $this->assertTrue($db->transRollback()); + $this->assertFalse($rollbackState); + } + public function testAfterCommitCallbacksRemainQueuedWhenDriverCommitFails(): void { $callbacks = []; @@ -581,10 +647,12 @@ protected function _transCommit(): bool $this->assertFalse($db->transCommit()); $this->assertSame([], $callbacks); $this->assertSame(1, $db->transDepth); + $this->assertTrue($db->inTransaction()); $this->assertTrue($db->transCommit()); $this->assertSame(['committed'], $callbacks); $this->assertSame(0, $db->transDepth); + $this->assertFalse($db->inTransaction()); $this->assertTrue($db->transBegin()); $this->assertTrue($db->transCommit()); @@ -614,10 +682,12 @@ protected function _transRollback(): bool $this->assertFalse($db->transRollback()); $this->assertSame([], $callbacks); $this->assertSame(1, $db->transDepth); + $this->assertTrue($db->inTransaction()); $this->assertTrue($db->transRollback()); $this->assertSame(['rolled back'], $callbacks); $this->assertSame(0, $db->transDepth); + $this->assertFalse($db->inTransaction()); $this->assertTrue($db->transBegin()); $this->assertTrue($db->transRollback()); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index fc14df3799d7..b6cc3acac908 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -43,7 +43,7 @@ Interface Changes **NOTE:** If you've implemented your own classes that implement these interfaces from scratch, you will need to update your implementations to include the new methods or method changes to ensure compatibility. -- **Database:** ``CodeIgniter\Database\ConnectionInterface`` now requires the ``afterCommit()``, ``afterRollback()``, and ``transaction()`` methods. +- **Database:** ``CodeIgniter\Database\ConnectionInterface`` now requires the ``afterCommit()``, ``afterRollback()``, ``inTransaction()``, and ``transaction()`` methods. - **Logging:** ``CodeIgniter\Log\Handlers\HandlerInterface::handle()`` now requires a third parameter ``array $context = []``. Any custom log handler that overrides ``handle()`` - whether implementing ``HandlerInterface`` directly or extending a built-in handler class - must add the parameter to its ``handle()`` method signature. - **Security:** The ``SecurityInterface``'s ``verify()`` method now has a native return type of ``static``. @@ -198,6 +198,7 @@ Database - Added ``afterCommit()`` and ``afterRollback()`` transaction callbacks to database connections. These callbacks run after the outermost transaction commits or rolls back. See :ref:`transactions-transaction-callbacks`. - Added the ``transaction()`` method to database connections to run a callback inside a transaction. See :ref:`transactions-closure`. +- Added ``inTransaction()`` to database connections to check whether the connection is inside an active CodeIgniter-managed transaction. See :ref:`transactions-checking-transaction-state`. - Added ``trustServerCertificate`` option to ``SQLSRV`` database connections in ``Config\Database``. Set it to ``true`` to trust the server certificate without CA validation when using encrypted connections. Query Builder diff --git a/user_guide_src/source/database/transactions.rst b/user_guide_src/source/database/transactions.rst index 9924c494c415..f26ba7982a24 100644 --- a/user_guide_src/source/database/transactions.rst +++ b/user_guide_src/source/database/transactions.rst @@ -192,6 +192,27 @@ Rollback callbacks also run when CodeIgniter automatically rolls back an active transaction while handling a transaction failure or cleaning up an unfinished transaction. +.. _transactions-checking-transaction-state: + +Checking Transaction State +========================== + +.. versionadded:: 4.8.0 + +You may use ``inTransaction()`` to check whether the connection is currently +inside an active CodeIgniter-managed transaction. + +It returns ``false`` when no CodeIgniter-managed transaction is active, +including when transactions are disabled. + +This is useful for services or libraries that need to adapt their behavior when +they are called from inside an existing transaction. + +.. note:: ``inTransaction()`` reflects transactions started through + CodeIgniter's transaction methods. If you start or end transactions through + raw SQL or driver-specific APIs, CodeIgniter may not know about that + transaction state. + Disabling Transactions ======================