diff --git a/src/PdoConnection.php b/src/PdoConnection.php index 6bafd6e..1a126c6 100644 --- a/src/PdoConnection.php +++ b/src/PdoConnection.php @@ -57,6 +57,16 @@ class PdoConnection */ private $reuseConnection = true; + /** + * @var int Number of open transactions + */ + private $transactionCount = 0; + + /** + * @var bool Flag that tells whether a rollback is needed + */ + private $rollbackNeeded = false; + /** * Creates a new Model object that establishes a new PDO connection using * the given database info, or the default configured info set in the database @@ -228,27 +238,52 @@ public function prepare($sql, $fetchMode = null) */ public function begin() { - return $this->connect()->beginTransaction(); + if (!$this->transactionCount++) { + return $this->connect()->beginTransaction(); + } + + return $this->transactionCount >= 0; } /** - * Rolls back and closes the transaction + * Rolls back and closes the transaction or mark this transaction set for rollback * - * @return boolean True if the transaction was successfully rolled back and closed, false otherwise + * @return boolean True if this is the outermost transaction and it was successfully rolled + * back and closed, false otherwise */ public function rollBack() { - return $this->connect()->rollBack(); + // Rollback if this is the outermost transaction, otherwise mark the set as erroring + if (--$this->transactionCount === 0) { + $this->rollbackNeeded = false; + return $this->connect()->rollBack(); + } + + $this->rollbackNeeded = true; + return false; } /** - * Commits a transaction + * Commits a transaction or rolls back an erroring transaction set * - * @return boolean True if the transaction was successfully commited and closed, false otherwise + * @return boolean True if the transaction was successfully commited, closed, and this is the outermost + * transaction or this is a non-erroring inner transaction, false otherwise */ public function commit() { - return $this->connect()->commit(); + $return = null; + if (--$this->transactionCount === 0) { + // Rollback if this transaction set is erroring, commit otherwise + if ($this->rollbackNeeded) { + $this->rollbackNeeded = false; + $this->connect()->rollback(); + $return = false; + } else { + $return = $this->connect()->commit(); + } + } + + return (isset($return) ? $return : $this->transactionCount >= 0 && !$this->rollbackNeeded); } /** diff --git a/tests/Unit/PdoConnectionTest.php b/tests/Unit/PdoConnectionTest.php index 2fdffb8..aee05a1 100644 --- a/tests/Unit/PdoConnectionTest.php +++ b/tests/Unit/PdoConnectionTest.php @@ -134,6 +134,7 @@ public function testBegin() /** * @covers ::rollBack * @covers ::__construct + * @covers ::begin * @covers ::getConnection * @covers ::setConnection * @covers ::connect @@ -141,12 +142,16 @@ public function testBegin() public function testRollBack() { $this->connection->setConnection($this->mockConnection('rollBack', true)); + $this->connection->begin(); $this->assertTrue($this->connection->rollBack()); + + $this->assertFalse($this->connection->rollBack()); } /** * @covers ::commit * @covers ::__construct + * @covers ::begin * @covers ::getConnection * @covers ::setConnection * @covers ::connect @@ -154,7 +159,70 @@ public function testRollBack() public function testCommit() { $this->connection->setConnection($this->mockConnection('commit', true)); + $this->connection->begin(); $this->assertTrue($this->connection->commit()); + + $this->assertFalse($this->connection->commit()); + } + + /** + * Passes a list of data sets to pass to testNestedTransacitons + * + * @return array A list of data sets to pass to testNestedTransacitons + */ + public function nestedTransactionData() + { + return array( + array(array('begin', 'begin', 'begin', 'commit', 'commit', 'commit'), true), + array(array('begin', 'begin', 'begin', 'rollback', 'rollback', 'rollback'), true), + array(array('begin', 'begin', 'begin', 'commit', 'commit', 'rollback'), true), + array(array('begin', 'begin', 'begin', 'rollback', 'commit', 'rollback'), true), + array(array('commit', 'begin', 'begin', 'commit'), true), + array(array('rollback', 'begin', 'begin', 'rollback'), true), + array(array('rollback', 'rollback', 'begin', 'begin'), true), + array(array('commit', 'commit', 'begin', 'begin'), true), + array(array('begin', 'begin', 'rollback', 'commit'), false), + array(array('begin', 'begin', 'begin', 'commit', 'rollback', 'commit'), false), + array(array('rollback', 'begin', 'begin', 'commit'), false), + array(array('begin', 'commit', 'commit'), false), + array(array('begin', 'rollback', 'rollback'), false) + ); + } + + /** + * @covers ::__construct + * @covers ::begin + * @covers ::commit + * @covers ::rollback + * @covers ::connect + * @covers ::getConnection + * @covers ::setConnection + * @dataProvider nestedTransactionData + * + * @param array $actions + * @param bool $return + */ + public function testNestedTransactions(array $actions, $return) + { + $transactions = 0; + $end = count($actions) - 1; + foreach ($actions as $index => $action) { + if ($action == 'begin') { + if ($transactions++ === 0) { + $this->connection->setConnection($this->mockConnection('beginTransaction', true)); + } + } else { + $transactions--; + if ($index === $end && $return) { + $this->connection->setConnection($this->mockConnection($action, true)); + } + } + + $actual = $this->connection->{$action}(); + if ($index === $end) { + $this->assertEquals($return, $actual); + } + } } /**