From 7e891bd1ed5e50041a2d6ad267ef6c774cbf61ca Mon Sep 17 00:00:00 2001 From: Jud Date: Thu, 8 May 2014 22:49:22 -0400 Subject: [PATCH 1/9] Initial Savepoint Concept --- lib/Pheasant/Database/Mysqli/Connection.php | 12 +++++++ lib/Pheasant/Database/Mysqli/Transaction.php | 15 +++++++-- .../Database/Mysqli/TransactionStack.php | 33 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 lib/Pheasant/Database/Mysqli/TransactionStack.php diff --git a/lib/Pheasant/Database/Mysqli/Connection.php b/lib/Pheasant/Database/Mysqli/Connection.php index 5635d3a..6d8705c 100755 --- a/lib/Pheasant/Database/Mysqli/Connection.php +++ b/lib/Pheasant/Database/Mysqli/Connection.php @@ -5,6 +5,7 @@ use Pheasant\Database\Dsn; use Pheasant\Database\FilterChain; use Pheasant\Database\MysqlPlatform; +use Pheasant\Database\Mysqli\TransactionStack; /** * A connection to a MySql database @@ -19,6 +20,7 @@ class Connection $_sequencePool, $_strict, $_selectedDatabase, + $_transactionStack, $_debug=false ; @@ -44,6 +46,7 @@ public function __construct(Dsn $dsn) $this->_selectedDatabase = $this->_dsn->database; $this->_debug = getenv('PHEASANT_DEBUG'); + $this->_transactionStack = new TransactionStack(); } /** @@ -248,4 +251,13 @@ public function selectedDatabase() { return $this->_selectedDatabase; } + + /** + * Returns the transaction stack + * @return TransactionStack + */ + public function transactionStack() + { + return $this->_transactionStack; + } } diff --git a/lib/Pheasant/Database/Mysqli/Transaction.php b/lib/Pheasant/Database/Mysqli/Transaction.php index 8033ec3..f12daf4 100755 --- a/lib/Pheasant/Database/Mysqli/Transaction.php +++ b/lib/Pheasant/Database/Mysqli/Transaction.php @@ -28,12 +28,21 @@ public function execute() $this->results = array(); try { - $this->_connection->execute('BEGIN'); + $this->_connection->execute( + ($sp = $this->_connection->transactionStack()->descend()) === null + ? 'BEGIN' + : "SAVEPOINT {$sp}"); $this->_events->trigger('startTransaction', $this->_connection); - $this->_connection->execute('COMMIT'); + $this->_connection->execute( + ($sp = $this->_connection->transactionStack()->pop()) === null + ? 'COMMIT' + : "RELEASE SAVEPOINT {$sp}"); $this->_events->trigger('commitTransaction', $this->_connection); } catch (\Exception $e) { - $this->_connection->execute('ROLLBACK'); + $this->_connection->execute( + ($sp = $this->_connection->transactionStack()->pop()) === null + ? 'ROLLBACK' + : "ROLLBACK TO {$sp}"); $this->_events->trigger('rollbackTransaction', $this->_connection); throw $e; } diff --git a/lib/Pheasant/Database/Mysqli/TransactionStack.php b/lib/Pheasant/Database/Mysqli/TransactionStack.php new file mode 100644 index 0000000..9c70b1f --- /dev/null +++ b/lib/Pheasant/Database/Mysqli/TransactionStack.php @@ -0,0 +1,33 @@ +_transactionStack = array(); + } + + public function count(){ + return count($this->_transactionStack); + } + + public function descend(){ + $this->_transactionStack[] = current($this->_transactionStack) === false + ? null + : 'savepoint_'.count($this->_transactionStack); + + return end($this->_transactionStack); + } + + public function pop(){ + return array_pop($this->_transactionStack); + } +} From cb80c8e6faba3e743b38fbc4f98c720f682647dd Mon Sep 17 00:00:00 2001 From: Jud Date: Thu, 8 May 2014 22:57:47 -0400 Subject: [PATCH 2/9] Adding comments to the transaction stack --- .../Database/Mysqli/TransactionStack.php | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/Pheasant/Database/Mysqli/TransactionStack.php b/lib/Pheasant/Database/Mysqli/TransactionStack.php index 9c70b1f..0c0d8ac 100644 --- a/lib/Pheasant/Database/Mysqli/TransactionStack.php +++ b/lib/Pheasant/Database/Mysqli/TransactionStack.php @@ -8,26 +8,38 @@ class TransactionStack { private - $_transactionStack + $_transactionStack = array() ; - public function __construct(){ - $this->_transactionStack = array(); - } - - public function count(){ + /** + * Get the depth of the stack + * @return integer + */ + public function depth() + { return count($this->_transactionStack); } - public function descend(){ + /** + * Decend deeper into the transaction stack and return a unique + * transaction savepoint name + * @return string + */ + public function descend() + { $this->_transactionStack[] = current($this->_transactionStack) === false ? null - : 'savepoint_'.count($this->_transactionStack); + : 'savepoint_'.$this->depth(); return end($this->_transactionStack); } - public function pop(){ + /** + * Pop off the last savepoint + * @return string + */ + public function pop() + { return array_pop($this->_transactionStack); } } From 08b697ce1dd44e7afe3ab4a5def31d90801e4c4f Mon Sep 17 00:00:00 2001 From: Jud Date: Fri, 9 May 2014 00:15:31 -0400 Subject: [PATCH 3/9] Changing tests --- tests/Pheasant/Tests/TransactionTest.php | 60 +++++++++++++++--------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/tests/Pheasant/Tests/TransactionTest.php b/tests/Pheasant/Tests/TransactionTest.php index 0ba6697..b87453d 100755 --- a/tests/Pheasant/Tests/TransactionTest.php +++ b/tests/Pheasant/Tests/TransactionTest.php @@ -5,11 +5,21 @@ class TransactionTest extends \Pheasant\Tests\MysqlTestCase { + public function setUp() + { + parent::setUp(); + + $this->queries = array(); + $test = $this; + $this->connection()->filterChain()->onQuery(function($sql) use($test) { + $test->queries []= $sql; + return $sql; + }); + } + public function testBasicSuccessfulTransaction() { - $connection = \Mockery::mock('\Pheasant\Database\Mysqli\Connection'); - $connection->shouldReceive('execute')->with('BEGIN')->once(); - $connection->shouldReceive('execute')->with('COMMIT')->once(); + $connection = $this->connection(); $transaction = new Transaction($connection); $transaction->callback(function(){ @@ -17,16 +27,16 @@ public function testBasicSuccessfulTransaction() }); $transaction->execute(); + $this->assertEquals(count($this->queries), 2); + $this->assertEquals($this->queries[0], 'BEGIN'); + $this->assertEquals($this->queries[1], 'COMMIT'); $this->assertEquals(count($transaction->results), 1); $this->assertEquals($transaction->results[0], 'blargh'); } public function testExceptionsCauseRollback() { - $connection = \Mockery::mock('\Pheasant\Database\Mysqli\Connection'); - $connection->shouldReceive('execute')->with('BEGIN')->once(); - $connection->shouldReceive('execute')->with('ROLLBACK')->once(); - + $connection = $this->connection(); $transaction = new Transaction($connection); $transaction->callback(function(){ throw new \Exception('Eeeek!'); @@ -34,29 +44,33 @@ public function testExceptionsCauseRollback() $this->setExpectedException('\Exception'); $transaction->execute(); + + $this->assertEquals($this->queries[0], 'BEGIN'); + $this->assertEquals($this->queries[1], 'ROLLBACK'); } public function testCallbacksWithConnectionCalls() { - $sql = "SELECT * FROM table"; - $connection = \Mockery::mock('\Pheasant\Database\Mysqli\Connection'); - $connection->shouldReceive('execute')->with('BEGIN')->once(); - $connection->shouldReceive('execute')->with($sql)->once(); - $connection->shouldReceive('execute')->with('COMMIT')->once(); + $sql = "SELECT * FROM 'table'"; + $connection = $this->connection(); $transaction = new Transaction($connection); $transaction->callback(function() use ($connection, $sql) { $connection->execute($sql); }); + $this->setExpectedException('\Exception'); $transaction->execute(); + + $this->assertEquals(count($this->queries), 3); + $this->assertEquals($this->queries[0], 'BEGIN'); + $this->assertEquals($this->queries[1], $sql); + $this->assertEquals($this->queries[2], 'COMMIT'); } public function testCallbacksWithParams() { - $connection = \Mockery::mock('\Pheasant\Database\Mysqli\Connection'); - $connection->shouldReceive('execute')->with('BEGIN')->once(); - $connection->shouldReceive('execute')->with('COMMIT')->once(); + $connection = $this->connection(); $transaction = new Transaction($connection); $transaction->callback(function($param) { @@ -66,13 +80,14 @@ public function testCallbacksWithParams() $transaction->execute(); $this->assertEquals(count($transaction->results), 1); $this->assertEquals($transaction->results[0], 'blargh'); + + $this->assertEquals($this->queries[0], 'BEGIN'); + $this->assertEquals($this->queries[1], 'COMMIT'); } public function testDeferEventsFireOnCommit() { - $connection = \Mockery::mock('\Pheasant\Database\Mysqli\Connection'); - $connection->shouldReceive('execute')->with('BEGIN')->once(); - $connection->shouldReceive('execute')->with('COMMIT')->once(); + $connection = $this->connection(); $events = \Mockery::mock(); $events->shouldReceive('cork')->once(); @@ -85,13 +100,13 @@ public function testDeferEventsFireOnCommit() }); $transaction->execute(); + $this->assertEquals($this->queries[0], 'BEGIN'); + $this->assertEquals($this->queries[1], 'COMMIT'); } public function testDeferEventsFireOnRollback() { - $connection = \Mockery::mock('\Pheasant\Database\Mysqli\Connection'); - $connection->shouldReceive('execute')->with('BEGIN')->once(); - $connection->shouldReceive('execute')->with('ROLLBACK')->once(); + $connection = $this->connection(); $events = \Mockery::mock(); $events->shouldReceive('cork')->once()->andReturn($events); @@ -106,5 +121,8 @@ public function testDeferEventsFireOnRollback() $this->setExpectedException('\Exception'); $transaction->execute(); + + $this->assertEquals($this->queries[0], 'BEGIN'); + $this->assertEquals($this->queries[1], 'ROLLBACK'); } } From e6cfb03026d9b2348f99d092d87ece4021c4bdb1 Mon Sep 17 00:00:00 2001 From: Jud Date: Fri, 9 May 2014 00:19:12 -0400 Subject: [PATCH 4/9] Making assertions consistent --- tests/Pheasant/Tests/TransactionTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Pheasant/Tests/TransactionTest.php b/tests/Pheasant/Tests/TransactionTest.php index b87453d..f4ec635 100755 --- a/tests/Pheasant/Tests/TransactionTest.php +++ b/tests/Pheasant/Tests/TransactionTest.php @@ -27,9 +27,11 @@ public function testBasicSuccessfulTransaction() }); $transaction->execute(); + $this->assertEquals(count($this->queries), 2); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'COMMIT'); + $this->assertEquals(count($transaction->results), 1); $this->assertEquals($transaction->results[0], 'blargh'); } @@ -45,6 +47,7 @@ public function testExceptionsCauseRollback() $this->setExpectedException('\Exception'); $transaction->execute(); + $this->assertEquals(count($this->queries), 2); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'ROLLBACK'); } @@ -81,6 +84,7 @@ public function testCallbacksWithParams() $this->assertEquals(count($transaction->results), 1); $this->assertEquals($transaction->results[0], 'blargh'); + $this->assertEquals(count($this->queries), 2); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'COMMIT'); } @@ -100,6 +104,7 @@ public function testDeferEventsFireOnCommit() }); $transaction->execute(); + $this->assertEquals(count($this->queries), 2); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'COMMIT'); } @@ -122,6 +127,7 @@ public function testDeferEventsFireOnRollback() $this->setExpectedException('\Exception'); $transaction->execute(); + $this->assertEquals(count($this->queries), 2); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'ROLLBACK'); } From e98d575dafc602636c3d38337e9a0dbe2cba54ba Mon Sep 17 00:00:00 2001 From: Jud Date: Fri, 9 May 2014 00:42:13 -0400 Subject: [PATCH 5/9] Fixing tests --- tests/Pheasant/Tests/TransactionTest.php | 52 ++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/tests/Pheasant/Tests/TransactionTest.php b/tests/Pheasant/Tests/TransactionTest.php index f4ec635..05473c9 100755 --- a/tests/Pheasant/Tests/TransactionTest.php +++ b/tests/Pheasant/Tests/TransactionTest.php @@ -2,6 +2,7 @@ namespace Pheasant\Tests\Transaction; use \Pheasant\Database\Mysqli\Transaction; +use \Pheasant\Tests\Examples\Animal; class TransactionTest extends \Pheasant\Tests\MysqlTestCase { @@ -9,6 +10,11 @@ public function setUp() { parent::setUp(); + $migrator = new \Pheasant\Migrate\Migrator(); + $migrator + ->create('animal', Animal::schema()) + ; + $this->queries = array(); $test = $this; $this->connection()->filterChain()->onQuery(function($sql) use($test) { @@ -54,7 +60,7 @@ public function testExceptionsCauseRollback() public function testCallbacksWithConnectionCalls() { - $sql = "SELECT * FROM 'table'"; + $sql = "SELECT * FROM animal"; $connection = $this->connection(); $transaction = new Transaction($connection); @@ -62,7 +68,6 @@ public function testCallbacksWithConnectionCalls() $connection->execute($sql); }); - $this->setExpectedException('\Exception'); $transaction->execute(); $this->assertEquals(count($this->queries), 3); @@ -81,6 +86,7 @@ public function testCallbacksWithParams() }, 'blargh'); $transaction->execute(); + $this->assertEquals(count($transaction->results), 1); $this->assertEquals($transaction->results[0], 'blargh'); @@ -104,6 +110,7 @@ public function testDeferEventsFireOnCommit() }); $transaction->execute(); + $this->assertEquals(count($this->queries), 2); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'COMMIT'); @@ -124,11 +131,48 @@ public function testDeferEventsFireOnRollback() throw new \Exception("Llamas :( :)"); }); - $this->setExpectedException('\Exception'); - $transaction->execute(); + try { + $transaction->execute(); + } catch(\Exception $e) { + $exception = $e; + } + $this->assertInstanceOf('\Exception', $exception); $this->assertEquals(count($this->queries), 2); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'ROLLBACK'); } + + public function testNestedDeferEventsFireOnRollback() + { + $connection = $this->connection(); + + $events = \Mockery::mock(); + $events->shouldReceive('cork')->once()->andReturn($events); + $events->shouldReceive('discard')->once()->andReturn($events); + $events->shouldReceive('uncork')->once()->andReturn($events); + + $transaction = new Transaction($connection); + $transaction->deferEvents($events); + $transaction->callback(function() use($connection){ + $t = new Transaction($connection); + $t->callback(function(){ + throw new \Exception("Llamas :( :)"); + }); + $t->execute(); + }); + + try { + $transaction->execute(); + } catch(\Exception $e) { + $exception = $e; + } + + $this->assertInstanceOf('\Exception', $exception); + $this->assertEquals(count($this->queries), 4); + $this->assertEquals($this->queries[0], 'BEGIN'); + $this->assertEquals($this->queries[1], 'SAVEPOINT savepoint_1'); + $this->assertEquals($this->queries[2], 'ROLLBACK TO savepoint_1'); + $this->assertEquals($this->queries[3], 'ROLLBACK'); + } } From cbaf911ca33322a0de52cd5b36c8ceea9b4ef831 Mon Sep 17 00:00:00 2001 From: Jud Date: Fri, 9 May 2014 00:45:41 -0400 Subject: [PATCH 6/9] Make test more complex --- tests/Pheasant/Tests/TransactionTest.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/Pheasant/Tests/TransactionTest.php b/tests/Pheasant/Tests/TransactionTest.php index 05473c9..0fea4a3 100755 --- a/tests/Pheasant/Tests/TransactionTest.php +++ b/tests/Pheasant/Tests/TransactionTest.php @@ -156,10 +156,12 @@ public function testNestedDeferEventsFireOnRollback() $transaction->deferEvents($events); $transaction->callback(function() use($connection){ $t = new Transaction($connection); - $t->callback(function(){ - throw new \Exception("Llamas :( :)"); - }); - $t->execute(); + $t->callback(function() use($connection){ + $t = new Transaction($connection); + $t->callback(function() use($connection){ + throw new \Exception("Llamas :( :)"); + })->execute(); + })->execute(); }); try { @@ -169,10 +171,12 @@ public function testNestedDeferEventsFireOnRollback() } $this->assertInstanceOf('\Exception', $exception); - $this->assertEquals(count($this->queries), 4); + $this->assertEquals(count($this->queries), 6); $this->assertEquals($this->queries[0], 'BEGIN'); $this->assertEquals($this->queries[1], 'SAVEPOINT savepoint_1'); - $this->assertEquals($this->queries[2], 'ROLLBACK TO savepoint_1'); - $this->assertEquals($this->queries[3], 'ROLLBACK'); + $this->assertEquals($this->queries[2], 'SAVEPOINT savepoint_2'); + $this->assertEquals($this->queries[3], 'ROLLBACK TO savepoint_2'); + $this->assertEquals($this->queries[4], 'ROLLBACK TO savepoint_1'); + $this->assertEquals($this->queries[5], 'ROLLBACK'); } } From c2ddf98079cfa1107dddff858e381a47c09c242c Mon Sep 17 00:00:00 2001 From: Jud Date: Sat, 19 Jul 2014 13:26:55 -0400 Subject: [PATCH 7/9] Move transaction sql to the Connection object --- lib/Pheasant/Database/Mysqli/Connection.php | 42 ++++++++++++++++++++ lib/Pheasant/Database/Mysqli/Transaction.php | 33 ++++++++------- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/lib/Pheasant/Database/Mysqli/Connection.php b/lib/Pheasant/Database/Mysqli/Connection.php index 6d8705c..2cad3a2 100755 --- a/lib/Pheasant/Database/Mysqli/Connection.php +++ b/lib/Pheasant/Database/Mysqli/Connection.php @@ -21,6 +21,7 @@ class Connection $_strict, $_selectedDatabase, $_transactionStack, + $_events, $_debug=false ; @@ -45,8 +46,40 @@ public function __construct(Dsn $dsn) if(!empty($this->_dsn->database)) $this->_selectedDatabase = $this->_dsn->database; + $this->_events = new \Pheasant\Events(); + $this->_debug = getenv('PHEASANT_DEBUG'); + + // Setup a transaction stack $this->_transactionStack = new TransactionStack(); + + // Keep a copy of ourselves around + $self = $this; + + // The beforeStartTransaction event is where we will BEGIN or SAVEPOINT + $this->_events->register('beforeStartTransaction', function() use($self) { + // if `descend` returns null, there is nothing on the stack + // so we should BEGIN a transaction instead of a numbered SAVEPOINT. + $savepoint = $self->transactionStack()->descend(); + $self->execute($savepoint === null ? "BEGIN" : "SAVEPOINT {$savepoint}"); + }); + + // The afterStartTransaction replaces commitTransaction, and is where + // we will COMMIT or RELEASE + $this->_events->register('afterStartTransaction', function() use($self) { + // if `pop` returns null, then the stack is now empty + // so we should COMMIT instead of RELEASE. + $savepoint = $self->transactionStack()->pop(); + $self->execute($savepoint === null ? "COMMIT" : "RELEASE SAVEPOINT {$savepoint}"); + }); + + // The rollbackTransaction event is fired when we need to ROLLBACK + $this->_events->register('rollbackTransaction', function() use($self) { + // if `pop` returns null, then the stack is now empty + // so we should ROLLBACK instead of ROLLBACK_TO. + $savepoint = $self->transactionStack()->pop(); + $self->execute($savepoint === null ? "ROLLBACK" : "ROLLBACK TO {$savepoint}"); + }); } /** @@ -252,6 +285,15 @@ public function selectedDatabase() return $this->_selectedDatabase; } + /** + * Returns the Event object + * @return Event + */ + public function events() + { + return $this->_events; + } + /** * Returns the transaction stack * @return TransactionStack diff --git a/lib/Pheasant/Database/Mysqli/Transaction.php b/lib/Pheasant/Database/Mysqli/Transaction.php index f12daf4..5d1b238 100755 --- a/lib/Pheasant/Database/Mysqli/Transaction.php +++ b/lib/Pheasant/Database/Mysqli/Transaction.php @@ -20,7 +20,7 @@ class Transaction public function __construct($connection=null) { $this->_connection = $connection ?: \Pheasant::instance()->connection(); - $this->_events = new \Pheasant\Events(); + $this->_events = new \Pheasant\Events(array(), $this->_connection->events()); } public function execute() @@ -28,21 +28,11 @@ public function execute() $this->results = array(); try { - $this->_connection->execute( - ($sp = $this->_connection->transactionStack()->descend()) === null - ? 'BEGIN' - : "SAVEPOINT {$sp}"); - $this->_events->trigger('startTransaction', $this->_connection); - $this->_connection->execute( - ($sp = $this->_connection->transactionStack()->pop()) === null - ? 'COMMIT' - : "RELEASE SAVEPOINT {$sp}"); - $this->_events->trigger('commitTransaction', $this->_connection); + $this->_events->wrap('StartTransaction', $this, function($self) { + $self->events()->trigger('startTransaction', $self->connection()); + $self->events()->trigger('commitTransaction', $self->connection()); + }); } catch (\Exception $e) { - $this->_connection->execute( - ($sp = $this->_connection->transactionStack()->pop()) === null - ? 'ROLLBACK' - : "ROLLBACK TO {$sp}"); $this->_events->trigger('rollbackTransaction', $this->_connection); throw $e; } @@ -76,6 +66,15 @@ public function events() return $this->_events; } + /** + * Get the connection object + * @return Connection + */ + public function connection() + { + return $this->_connection; + } + /** * Links another Events object such that events in it are corked until either commit/rollback and then uncorked * @chainable @@ -83,10 +82,10 @@ public function events() public function deferEvents($events) { $this->_events - ->register('startTransaction', function() use ($events) { + ->register('beforeStartTransaction', function() use ($events) { $events->cork(); }) - ->register('commitTransaction', function() use ($events) { + ->register('afterStartTransaction', function() use ($events) { $events->uncork(); }) ->register('rollbackTransaction', function() use ($events) { From d28d750c725d34e4a87b9adef02eda668a23ac66 Mon Sep 17 00:00:00 2001 From: Jud Date: Sat, 20 Sep 2014 13:06:57 -0400 Subject: [PATCH 8/9] Changed TransactionStack to SavePointStack, before/afterStartTransaction to before/afterTransaction Also changed the methodology behind the commit event, which should only be called when the actual 'commit' query is sent and not when the savepoint is simply released. --- lib/Pheasant/Database/Mysqli/Connection.php | 57 +++++++++++-------- ...ransactionStack.php => SavePointStack.php} | 12 ++-- lib/Pheasant/Database/Mysqli/Transaction.php | 9 ++- 3 files changed, 43 insertions(+), 35 deletions(-) rename lib/Pheasant/Database/Mysqli/{TransactionStack.php => SavePointStack.php} (68%) diff --git a/lib/Pheasant/Database/Mysqli/Connection.php b/lib/Pheasant/Database/Mysqli/Connection.php index 2cad3a2..45d1248 100755 --- a/lib/Pheasant/Database/Mysqli/Connection.php +++ b/lib/Pheasant/Database/Mysqli/Connection.php @@ -5,7 +5,7 @@ use Pheasant\Database\Dsn; use Pheasant\Database\FilterChain; use Pheasant\Database\MysqlPlatform; -use Pheasant\Database\Mysqli\TransactionStack; +use Pheasant\Database\Mysqli\SavePointStack; /** * A connection to a MySql database @@ -20,7 +20,7 @@ class Connection $_sequencePool, $_strict, $_selectedDatabase, - $_transactionStack, + $_savePointStack, $_events, $_debug=false ; @@ -30,6 +30,10 @@ class Connection $timer=0 ; + private static + $transactionEventName = 'transaction' + ; + /** * Constructor * @param string a database uri @@ -47,38 +51,43 @@ public function __construct(Dsn $dsn) $this->_selectedDatabase = $this->_dsn->database; $this->_events = new \Pheasant\Events(); - $this->_debug = getenv('PHEASANT_DEBUG'); // Setup a transaction stack - $this->_transactionStack = new TransactionStack(); + $this->_savePointStack = new SavePointStack(); // Keep a copy of ourselves around $self = $this; - // The beforeStartTransaction event is where we will BEGIN or SAVEPOINT - $this->_events->register('beforeStartTransaction', function() use($self) { - // if `descend` returns null, there is nothing on the stack - // so we should BEGIN a transaction instead of a numbered SAVEPOINT. - $savepoint = $self->transactionStack()->descend(); - $self->execute($savepoint === null ? "BEGIN" : "SAVEPOINT {$savepoint}"); + // The beforeTransaction event is where we will BEGIN or SAVEPOINT + $this->_events->register('beforeTransaction', function() use($self) { + // if `descend` returns null, there is nothing on the stack + // so we should BEGIN a transaction instead of a numbered SAVEPOINT. + $savepoint = $self->savePointStack()->descend(); + $self->execute($savepoint === null ? "BEGIN" : "SAVEPOINT {$savepoint}"); }); - // The afterStartTransaction replaces commitTransaction, and is where + // The afterTransaction replaces commitTransaction, and is where // we will COMMIT or RELEASE - $this->_events->register('afterStartTransaction', function() use($self) { - // if `pop` returns null, then the stack is now empty - // so we should COMMIT instead of RELEASE. - $savepoint = $self->transactionStack()->pop(); - $self->execute($savepoint === null ? "COMMIT" : "RELEASE SAVEPOINT {$savepoint}"); + $this->_events->register('afterTransaction', function() use($self) { + // if `pop` returns null, then the stack is now empty + // so we should COMMIT instead of RELEASE. + $savepoint = $self->savePointStack()->pop(); + + // If the savepoint is null, then we are committing the + // transaction, and should fire the appropriate events. + $callback_name = $savepoint === null ? 'Commit' : 'Savepoint'; + $self->events()->wrap($callback_name, $self, function($self) use($savepoint) { + $self->execute($savepoint === null ? "COMMIT" : "RELEASE SAVEPOINT {$savepoint}"); + }); }); // The rollbackTransaction event is fired when we need to ROLLBACK - $this->_events->register('rollbackTransaction', function() use($self) { - // if `pop` returns null, then the stack is now empty - // so we should ROLLBACK instead of ROLLBACK_TO. - $savepoint = $self->transactionStack()->pop(); - $self->execute($savepoint === null ? "ROLLBACK" : "ROLLBACK TO {$savepoint}"); + $this->_events->register('rollback', function() use($self) { + // if `pop` returns null, then the stack is now empty + // so we should ROLLBACK instead of ROLLBACK_TO. + $savepoint = $self->savePointStack()->pop(); + $self->execute($savepoint === null ? "ROLLBACK" : "ROLLBACK TO {$savepoint}"); }); } @@ -296,10 +305,10 @@ public function events() /** * Returns the transaction stack - * @return TransactionStack + * @return SavePointStack */ - public function transactionStack() + public function savePointStack() { - return $this->_transactionStack; + return $this->_savePointStack; } } diff --git a/lib/Pheasant/Database/Mysqli/TransactionStack.php b/lib/Pheasant/Database/Mysqli/SavePointStack.php similarity index 68% rename from lib/Pheasant/Database/Mysqli/TransactionStack.php rename to lib/Pheasant/Database/Mysqli/SavePointStack.php index 0c0d8ac..354ef7e 100644 --- a/lib/Pheasant/Database/Mysqli/TransactionStack.php +++ b/lib/Pheasant/Database/Mysqli/SavePointStack.php @@ -5,10 +5,10 @@ /** * A Transaction Stack that keeps track of open savepoints */ -class TransactionStack +class SavePointStack { private - $_transactionStack = array() + $_savePointStack = array() ; /** @@ -17,7 +17,7 @@ class TransactionStack */ public function depth() { - return count($this->_transactionStack); + return count($this->_savePointStack); } /** @@ -27,11 +27,11 @@ public function depth() */ public function descend() { - $this->_transactionStack[] = current($this->_transactionStack) === false + $this->_savePointStack[] = current($this->_savePointStack) === false ? null : 'savepoint_'.$this->depth(); - return end($this->_transactionStack); + return end($this->_savePointStack); } /** @@ -40,6 +40,6 @@ public function descend() */ public function pop() { - return array_pop($this->_transactionStack); + return array_pop($this->_savePointStack); } } diff --git a/lib/Pheasant/Database/Mysqli/Transaction.php b/lib/Pheasant/Database/Mysqli/Transaction.php index 5d1b238..c101c80 100755 --- a/lib/Pheasant/Database/Mysqli/Transaction.php +++ b/lib/Pheasant/Database/Mysqli/Transaction.php @@ -28,12 +28,11 @@ public function execute() $this->results = array(); try { - $this->_events->wrap('StartTransaction', $this, function($self) { - $self->events()->trigger('startTransaction', $self->connection()); - $self->events()->trigger('commitTransaction', $self->connection()); + $this->_events->wrap('Transaction', $this, function($self) { + $self->events()->trigger('Transaction', $self->connection()); }); } catch (\Exception $e) { - $this->_events->trigger('rollbackTransaction', $this->_connection); + $this->_events->trigger('rollback', $this->_connection); throw $e; } @@ -50,7 +49,7 @@ public function callback($callback) $args = array_slice(func_get_args(),1); // use an event handler to dispatch to the callback - $this->_events->register('startTransaction', function($event, $connection) use ($t, $callback, $args) { + $this->_events->register('Transaction', function($event, $connection) use ($t, $callback, $args) { $t->results []= call_user_func_array($callback, $args); }); From 37fb625e16a387b67652a81f0bc4cf27a30a7a79 Mon Sep 17 00:00:00 2001 From: Jud Date: Sat, 20 Sep 2014 16:31:57 -0400 Subject: [PATCH 9/9] Adding the `registerOne` method to Event class. Allows an event to be bound and then removed. --- lib/Pheasant/Database/Mysqli/Connection.php | 6 +---- lib/Pheasant/Database/Mysqli/Transaction.php | 17 +++++++++------ lib/Pheasant/Events.php | 23 ++++++++++++++++++++ tests/Pheasant/Tests/EventsTest.php | 15 +++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/Pheasant/Database/Mysqli/Connection.php b/lib/Pheasant/Database/Mysqli/Connection.php index 45d1248..b78d0b4 100755 --- a/lib/Pheasant/Database/Mysqli/Connection.php +++ b/lib/Pheasant/Database/Mysqli/Connection.php @@ -30,10 +30,6 @@ class Connection $timer=0 ; - private static - $transactionEventName = 'transaction' - ; - /** * Constructor * @param string a database uri @@ -76,7 +72,7 @@ public function __construct(Dsn $dsn) // If the savepoint is null, then we are committing the // transaction, and should fire the appropriate events. - $callback_name = $savepoint === null ? 'Commit' : 'Savepoint'; + $callback_name = $savepoint === null ? 'Commit' : 'SavePoint'; $self->events()->wrap($callback_name, $self, function($self) use($savepoint) { $self->execute($savepoint === null ? "COMMIT" : "RELEASE SAVEPOINT {$savepoint}"); }); diff --git a/lib/Pheasant/Database/Mysqli/Transaction.php b/lib/Pheasant/Database/Mysqli/Transaction.php index c101c80..4aec285 100755 --- a/lib/Pheasant/Database/Mysqli/Transaction.php +++ b/lib/Pheasant/Database/Mysqli/Transaction.php @@ -29,7 +29,7 @@ public function execute() try { $this->_events->wrap('Transaction', $this, function($self) { - $self->events()->trigger('Transaction', $self->connection()); + $self->events()->trigger('transaction', $self->connection()); }); } catch (\Exception $e) { $this->_events->trigger('rollback', $this->_connection); @@ -49,7 +49,7 @@ public function callback($callback) $args = array_slice(func_get_args(),1); // use an event handler to dispatch to the callback - $this->_events->register('Transaction', function($event, $connection) use ($t, $callback, $args) { + $this->_events->register('transaction', function($event, $connection) use ($t, $callback, $args) { $t->results []= call_user_func_array($callback, $args); }); @@ -81,16 +81,19 @@ public function connection() public function deferEvents($events) { $this->_events - ->register('beforeStartTransaction', function() use ($events) { + ->registerOne('beforeTransaction', function() use ($events) { $events->cork(); }) - ->register('afterStartTransaction', function() use ($events) { - $events->uncork(); - }) - ->register('rollbackTransaction', function() use ($events) { + ->registerOne('rollback', function() use ($events) { $events->discard()->uncork(); }) ; + + $this->connection()->events()->registerOne('afterCommit', function() use ($events) { + $events->uncork(); + }); + + return $this; } /** * Creates a transaction and optionally execute a transaction diff --git a/lib/Pheasant/Events.php b/lib/Pheasant/Events.php index 63fb608..d731dc8 100755 --- a/lib/Pheasant/Events.php +++ b/lib/Pheasant/Events.php @@ -10,6 +10,7 @@ class Events { private $_handlers = array(), + $_oneHandlers = array(), $_queue = array(), $_corked = false, $_upstream @@ -77,9 +78,18 @@ private function _callbacksFor($event) { $events = isset($this->_handlers[$event]) ? $this->_handlers[$event] : array(); + if(isset($this->_oneHandlers[$event])) + $events = array_merge($events, $this->_oneHandlers[$event]); + + if(isset($this->_oneHandlers['*'])) + $events = array_merge($events, $this->_oneHandlers['*']); + if(isset($this->_handlers['*'])) $events = array_merge($events, $this->_handlers['*']); + // Clear the events that should only be run once + $this->_oneHandlers[$event] = array(); + return $events; } @@ -94,6 +104,19 @@ public function register($event, $callback) return $this; } + /** + * Registers a handler for an event that is immediately removed + * after it is executed. + * @chainable + */ + public function registerOne($event, $callback) + { + $this->_oneHandlers[$event][] = $callback; + + return $this; + } + + /** * Unregisters an event handler based on event, or all * @chainable diff --git a/tests/Pheasant/Tests/EventsTest.php b/tests/Pheasant/Tests/EventsTest.php index d4ff547..8a6e155 100755 --- a/tests/Pheasant/Tests/EventsTest.php +++ b/tests/Pheasant/Tests/EventsTest.php @@ -197,4 +197,19 @@ public function testSaveInAfterCreateDoesntLoop() $do->test = "blargh"; $do->save(); } + + public function testRegisterOneEventBinding() + { + $fired = array(); + + $events = new Events(); + $events->registerOne('fireOnceEvent', function() use(&$fired) { + $fired[] = 1; + }); + + $events->trigger('fireOnceEvent', new \stdClass()); + $events->trigger('fireOnceEvent', new \stdClass()); + + $this->assertCount(1, $fired); + } }