diff --git a/model/connect/Database.php b/model/connect/Database.php index 30fe333c2..5113821ec 100644 --- a/model/connect/Database.php +++ b/model/connect/Database.php @@ -489,6 +489,42 @@ abstract class SS_Database { */ abstract public function supportsTransactions(); + /** + * Invoke $callback within a transaction + * + * @param callable $callback Callback to run + * @param callable $errorCallback Optional callback to run after rolling back transaction. + * @param bool|string $transactionMode Optional transaction mode to use + * @param bool $errorIfTransactionsUnsupported If true, this method will fail if transactions are unsupported. + * Otherwise, the $callback will potentially be invoked outside of a transaction. + * @throws Exception + */ + public function withTransaction( + $callback, $errorCallback = null, $transactionMode = false, $errorIfTransactionsUnsupported = false + ) { + $supported = $this->supportsTransactions(); + if(!$supported && $errorIfTransactionsUnsupported) { + throw new BadMethodCallException("Transactions not supported by this database."); + } + if($supported) { + $this->transactionStart($transactionMode); + } + try { + call_user_func($callback); + } catch (Exception $ex) { + if($supported) { + $this->transactionRollback(); + } + if($errorCallback) { + call_user_func($errorCallback); + } + throw $ex; + } + if($supported) { + $this->transactionEnd(); + } + } + /* * Determines if the current database connection supports a given list of extensions * diff --git a/tests/model/DatabaseTest.php b/tests/model/DatabaseTest.php index eaf5a803b..ee7172847 100644 --- a/tests/model/DatabaseTest.php +++ b/tests/model/DatabaseTest.php @@ -151,6 +151,44 @@ class DatabaseTest extends SapphireTest { $this->assertTrue($db->canLock('DatabaseTest'), 'Can lock again after releasing it'); } + public function testTransactions() { + $conn = DB::getConn(); + if(!$conn->supportsTransactions()) { + $this->markTestSkipped("DB Doesn't support transactions"); + return; + } + + // Test that successful transactions are comitted + $obj = new DatabaseTest_MyObject(); + $failed = false; + $conn->withTransaction(function() use (&$obj) { + $obj->MyField = 'Save 1'; + $obj->write(); + }, function() use (&$failed) { + $failed = true; + }); + $this->assertEquals('Save 1', DatabaseTest_MyObject::get()->first()->MyField); + $this->assertFalse($failed); + + // Test failed transactions are rolled back + $ex = null; + $failed = false; + try { + $conn->withTransaction(function() use (&$obj) { + $obj->MyField = 'Save 2'; + $obj->write(); + throw new Exception("error"); + }, function() use (&$failed) { + $failed = true; + }); + } catch ( Exception $ex) {} + $this->assertTrue($failed); + $this->assertEquals('Save 1', DatabaseTest_MyObject::get()->first()->MyField); + $this->assertInstanceOf('Exception', $ex); + $this->assertEquals('error', $ex->getMessage()); + } + + } class DatabaseTest_MyObject extends DataObject implements TestOnly {