diff --git a/src/Medoo.php b/src/Medoo.php index 00cdb440..6a4c6c50 100644 --- a/src/Medoo.php +++ b/src/Medoo.php @@ -174,6 +174,27 @@ class Medoo */ public $errorInfo = null; + /** + * Call callback when the transaction has committed. + * + * @var callable[] + */ + public $onActionCommitted = []; + + /** + * Call callback when the transaction has rolled back. + * + * @var callable[] + */ + public $onActionRolledBack = []; + + /** + * Call callback when the transaction has finished. + * + * @var callable[] + */ + public $onActionFinished = []; + /** * Connect the database. * @@ -2102,16 +2123,84 @@ public function action(callable $actions): void if ($result === false) { $this->pdo->rollBack(); + $this->callActionRolledBack(); } else { $this->pdo->commit(); + $this->callActionCommitted(); } } catch (Exception $e) { $this->pdo->rollBack(); + $this->callActionRolledBack(); throw $e; } } } + /** + * Call callback when the transaction has committed. + * + * @return void + */ + protected function callActionCommitted(): void + { + foreach ($this->onActionCommitted as $callback) { + $callback($this); + } + foreach ($this->onActionFinished as $callback) { + $callback($this, true); + } + $this->onActionCommitted = $this->onActionRolledBack = $this->onActionFinished = []; + } + + /** + * Call callback when the transaction has rolled back. + * + * @return void + */ + protected function callActionRolledBack(): void + { + foreach ($this->onActionRolledBack as $callback) { + $callback($this); + } + foreach ($this->onActionFinished as $callback) { + $callback($this, false); + } + $this->onActionCommitted = $this->onActionRolledBack = $this->onActionFinished = []; + } + + /** + * Register a callback function and call it after the transaction is successfully committed. + * + * @param callable $callback `function(Medoo $medoo): void` + * @return void + */ + public function onActionCommitted(callable $callback): void + { + $this->onActionCommitted[] = $callback; + } + + /** + * Register a callback function and call it after the transaction is rolled back. + * + * @param callable $callback `function(Medoo $medoo): void` + * @return void + */ + public function onActionRolledBack(callable $callback): void + { + $this->onActionRolledBack[] = $callback; + } + + /** + * Register a callback function and call it when the transaction is finished. + * + * @param callable $callback `function(Medoo $medoo, bool $committed): void` + * @return void + */ + public function onActionFinished(callable $callback): void + { + $this->onActionFinished[] = $callback; + } + /** * Return the ID for the last inserted row. * diff --git a/tests/ActionTest.php b/tests/ActionTest.php new file mode 100644 index 00000000..f4d0c3e5 --- /dev/null +++ b/tests/ActionTest.php @@ -0,0 +1,188 @@ +database->pdo = new class extends PDO + { + public $testBeginTransaction = 0; + public $testRollBack = 0; + public $testCommit = 0; + + function __construct() + { + } + + function beginTransaction(): bool + { + $this->testBeginTransaction++; + return true; + } + + function rollBack(): bool + { + $this->testRollBack++; + return true; + } + + function commit(): bool + { + $this->testCommit++; + return true; + } + }; + } + + public function commitReturnsProvider(): array + { + return [ + 'return null' => [null], + 'return true' => [true], + 'return string' => ['string'], + 'return object' => [new \stdClass], + 'return 1' => [1], + 'return 0' => [0], + 'return array' => [[]] + ]; + } + + public function rollBackReturnsProvider(): array + { + return [ + 'return false' => [false], + 'throw exception' => [new \Exception] + ]; + } + + /** + * @covers ::action() + * @covers ::onActionCommitted() + * @covers ::onActionRolledBack() + * @covers ::onActionFinished() + * @covers ::callActionCommitted() + * @dataProvider commitReturnsProvider + */ + public function testActionCommit($return) + { + $onActionCommitted = 0; + $onActionRolledBack = 0; + $onActionFinished = 0; + + $this->database->action(function (Medoo $database) use ($return, &$onActionCommitted, &$onActionRolledBack, &$onActionFinished) { + $this->assertEquals($database, $this->database); + $database->onActionCommitted(function ($database) use (&$onActionCommitted) { + $onActionCommitted++; + $this->assertEquals($database, $this->database); + }); + $database->onActionCommitted(function () use (&$onActionCommitted) { + $onActionCommitted++; + }); + + $database->onActionRolledBack(function ($database) use (&$onActionRolledBack) { + $onActionRolledBack++; + $this->assertEquals($database, $this->database); + }); + $database->onActionRolledBack(function ($database) use (&$onActionRolledBack) { + $onActionRolledBack++; + }); + + $database->onActionFinished(function ($database, $commited) use (&$onActionFinished) { + $onActionFinished++; + $this->assertEquals($database, $this->database); + $this->assertEquals($commited, true); + }); + $database->onActionFinished(function () use (&$onActionFinished) { + $onActionFinished++; + }); + + return $return; + }); + + $this->assertEquals($onActionCommitted, 2); + $this->assertEquals($onActionRolledBack, 0); + $this->assertEquals($onActionFinished, 2); + + $this->assertEquals($this->database->pdo->testBeginTransaction, 1); + $this->assertEquals($this->database->pdo->testRollBack, 0); + $this->assertEquals($this->database->pdo->testCommit, 1); + } + + /** + * @covers ::action() + * @covers ::onActionCommitted() + * @covers ::onActionRolledBack() + * @covers ::onActionFinished() + * @covers ::callActionRolledBack() + * @dataProvider rollBackReturnsProvider + */ + public function testActionRollBack($return) + { + $onActionCommitted = 0; + $onActionRolledBack = 0; + $onActionFinished = 0; + $throwException = 0; + + try { + $this->database->action(function (Medoo $database) use ($return, &$onActionCommitted, &$onActionRolledBack, &$onActionFinished) { + $this->assertEquals($database, $this->database); + $database->onActionCommitted(function ($database) use (&$onActionCommitted) { + $onActionCommitted++; + $this->assertEquals($database, $this->database); + }); + $database->onActionCommitted(function () use (&$onActionCommitted) { + $onActionCommitted++; + }); + + $database->onActionRolledBack(function ($database) use (&$onActionRolledBack) { + $onActionRolledBack++; + $this->assertEquals($database, $this->database); + }); + $database->onActionRolledBack(function () use (&$onActionRolledBack) { + $onActionRolledBack++; + }); + + $database->onActionFinished(function ($database, $commited) use (&$onActionFinished) { + $onActionFinished++; + $this->assertEquals($database, $this->database); + $this->assertEquals($commited, false); + }); + $database->onActionFinished(function () use (&$onActionFinished) { + $onActionFinished++; + }); + + if ($return instanceof \Throwable) { + throw $return; + } + return $return; + }); + } catch (\Throwable $th) { + $throwException++; + } + + $this->assertEquals($onActionCommitted, 0); + $this->assertEquals($onActionRolledBack, 2); + $this->assertEquals($onActionFinished, 2); + + if ($return instanceof \Throwable) { + $this->assertEquals($throwException, 1); + } else { + $this->assertEquals($throwException, 0); + } + + $this->assertEquals($this->database->pdo->testBeginTransaction, 1); + $this->assertEquals($this->database->pdo->testRollBack, 1); + $this->assertEquals($this->database->pdo->testCommit, 0); + } +} diff --git a/tests/MedooTestCase.php b/tests/MedooTestCase.php index 1fae4d7b..159edf59 100644 --- a/tests/MedooTestCase.php +++ b/tests/MedooTestCase.php @@ -7,6 +7,7 @@ class MedooTestCase extends TestCase { + /** @var Medoo */ protected $database; public function setUp(): void @@ -42,7 +43,7 @@ public function expectedQuery($expected): string return preg_replace( '/(?!\'[^\s]+\s?)"([\p{L}_][\p{L}\p{N}@$#\-_]*)"(?!\s?[^\s]+\')/u', $identifier[$this->database->type] ?? '"$1"', - str_replace("\n", " ", $expected) + str_replace(["\r\n", "\n"], " ", $expected) ); }