Skip to content
This repository was archived by the owner on May 26, 2023. It is now read-only.

Commit e547534

Browse files
committed
Adding Savepoints
1 parent a833f4a commit e547534

File tree

6 files changed

+260
-36
lines changed

6 files changed

+260
-36
lines changed

lib/Pheasant/Database/Mysqli/Connection.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Pheasant\Database\Dsn;
66
use Pheasant\Database\FilterChain;
77
use Pheasant\Database\MysqlPlatform;
8+
use Pheasant\Database\Mysqli\SavePointStack;
89

910
/**
1011
* A connection to a MySql database
@@ -19,6 +20,8 @@ class Connection
1920
$_sequencePool,
2021
$_strict,
2122
$_selectedDatabase,
23+
$_savePointStack,
24+
$_events,
2225
$_debug=false
2326
;
2427

@@ -43,7 +46,45 @@ public function __construct(Dsn $dsn)
4346
if(!empty($this->_dsn->database))
4447
$this->_selectedDatabase = $this->_dsn->database;
4548

49+
$this->_events = new \Pheasant\Events();
4650
$this->_debug = getenv('PHEASANT_DEBUG');
51+
52+
// Setup a transaction stack
53+
$this->_savePointStack = new SavePointStack();
54+
55+
// Keep a copy of ourselves around
56+
$self = $this;
57+
58+
// The beforeTransaction event is where we will BEGIN or SAVEPOINT
59+
$this->_events->register('beforeTransaction', function() use($self) {
60+
// if `descend` returns null, there is nothing on the stack
61+
// so we should BEGIN a transaction instead of a numbered SAVEPOINT.
62+
$savepoint = $self->savePointStack()->descend();
63+
$self->execute($savepoint === null ? "BEGIN" : "SAVEPOINT {$savepoint}");
64+
});
65+
66+
// The afterTransaction replaces commitTransaction, and is where
67+
// we will COMMIT or RELEASE
68+
$this->_events->register('afterTransaction', function() use($self) {
69+
// if `pop` returns null, then the stack is now empty
70+
// so we should COMMIT instead of RELEASE.
71+
$savepoint = $self->savePointStack()->pop();
72+
73+
// If the savepoint is null, then we are committing the
74+
// transaction, and should fire the appropriate events.
75+
$callback_name = $savepoint === null ? 'Commit' : 'SavePoint';
76+
$self->events()->wrap($callback_name, $self, function($self) use($savepoint) {
77+
$self->execute($savepoint === null ? "COMMIT" : "RELEASE SAVEPOINT {$savepoint}");
78+
});
79+
});
80+
81+
// The rollbackTransaction event is fired when we need to ROLLBACK
82+
$this->_events->register('rollback', function() use($self) {
83+
// if `pop` returns null, then the stack is now empty
84+
// so we should ROLLBACK instead of ROLLBACK_TO.
85+
$savepoint = $self->savePointStack()->pop();
86+
$self->execute($savepoint === null ? "ROLLBACK" : "ROLLBACK TO {$savepoint}");
87+
});
4788
}
4889

4990
/**
@@ -248,4 +289,22 @@ public function selectedDatabase()
248289
{
249290
return $this->_selectedDatabase;
250291
}
292+
293+
/**
294+
* Returns the Event object
295+
* @return Event
296+
*/
297+
public function events()
298+
{
299+
return $this->_events;
300+
}
301+
302+
/**
303+
* Returns the transaction stack
304+
* @return SavePointStack
305+
*/
306+
public function savePointStack()
307+
{
308+
return $this->_savePointStack;
309+
}
251310
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Pheasant\Database\Mysqli;
4+
5+
/**
6+
* A Transaction Stack that keeps track of open savepoints
7+
*/
8+
class SavePointStack
9+
{
10+
private
11+
$_savePointStack = array()
12+
;
13+
14+
/**
15+
* Get the depth of the stack
16+
* @return integer
17+
*/
18+
public function depth()
19+
{
20+
return count($this->_savePointStack);
21+
}
22+
23+
/**
24+
* Decend deeper into the transaction stack and return a unique
25+
* transaction savepoint name
26+
* @return string
27+
*/
28+
public function descend()
29+
{
30+
$this->_savePointStack[] = current($this->_savePointStack) === false
31+
? null
32+
: 'savepoint_'.$this->depth();
33+
34+
return end($this->_savePointStack);
35+
}
36+
37+
/**
38+
* Pop off the last savepoint
39+
* @return string
40+
*/
41+
public function pop()
42+
{
43+
return array_pop($this->_savePointStack);
44+
}
45+
}

lib/Pheasant/Database/Mysqli/Transaction.php

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,19 @@ class Transaction
2020
public function __construct($connection=null)
2121
{
2222
$this->_connection = $connection ?: \Pheasant::instance()->connection();
23-
$this->_events = new \Pheasant\Events();
23+
$this->_events = new \Pheasant\Events(array(), $this->_connection->events());
2424
}
2525

2626
public function execute()
2727
{
2828
$this->results = array();
2929

3030
try {
31-
$this->_connection->execute('BEGIN');
32-
$this->_events->trigger('startTransaction', $this->_connection);
33-
$this->_connection->execute('COMMIT');
34-
$this->_events->trigger('commitTransaction', $this->_connection);
31+
$this->_events->wrap('Transaction', $this, function($self) {
32+
$self->events()->trigger('transaction', $self->connection());
33+
});
3534
} catch (\Exception $e) {
36-
$this->_connection->execute('ROLLBACK');
37-
$this->_events->trigger('rollbackTransaction', $this->_connection);
35+
$this->_events->trigger('rollback', $this->_connection);
3836
throw $e;
3937
}
4038

@@ -51,7 +49,7 @@ public function callback($callback)
5149
$args = array_slice(func_get_args(),1);
5250

5351
// use an event handler to dispatch to the callback
54-
$this->_events->register('startTransaction', function($event, $connection) use ($t, $callback, $args) {
52+
$this->_events->register('transaction', function($event, $connection) use ($t, $callback, $args) {
5553
$t->results []= call_user_func_array($callback, $args);
5654
});
5755

@@ -67,23 +65,35 @@ public function events()
6765
return $this->_events;
6866
}
6967

68+
/**
69+
* Get the connection object
70+
* @return Connection
71+
*/
72+
public function connection()
73+
{
74+
return $this->_connection;
75+
}
76+
7077
/**
7178
* Links another Events object such that events in it are corked until either commit/rollback and then uncorked
7279
* @chainable
7380
*/
7481
public function deferEvents($events)
7582
{
7683
$this->_events
77-
->register('startTransaction', function() use ($events) {
84+
->registerOne('beforeTransaction', function() use ($events) {
7885
$events->cork();
7986
})
80-
->register('commitTransaction', function() use ($events) {
81-
$events->uncork();
82-
})
83-
->register('rollbackTransaction', function() use ($events) {
87+
->registerOne('rollback', function() use ($events) {
8488
$events->discard()->uncork();
8589
})
8690
;
91+
92+
$this->connection()->events()->registerOne('afterCommit', function() use ($events) {
93+
$events->uncork();
94+
});
95+
96+
return $this;
8797
}
8898
/**
8999
* Creates a transaction and optionally execute a transaction

lib/Pheasant/Events.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Events
1010
{
1111
private
1212
$_handlers = array(),
13+
$_oneHandlers = array(),
1314
$_queue = array(),
1415
$_corked = false,
1516
$_upstream
@@ -77,9 +78,18 @@ private function _callbacksFor($event)
7778
{
7879
$events = isset($this->_handlers[$event]) ? $this->_handlers[$event] : array();
7980

81+
if(isset($this->_oneHandlers[$event]))
82+
$events = array_merge($events, $this->_oneHandlers[$event]);
83+
84+
if(isset($this->_oneHandlers['*']))
85+
$events = array_merge($events, $this->_oneHandlers['*']);
86+
8087
if(isset($this->_handlers['*']))
8188
$events = array_merge($events, $this->_handlers['*']);
8289

90+
// Clear the events that should only be run once
91+
$this->_oneHandlers[$event] = array();
92+
8393
return $events;
8494
}
8595

@@ -94,6 +104,19 @@ public function register($event, $callback)
94104
return $this;
95105
}
96106

107+
/**
108+
* Registers a handler for an event that is immediately removed
109+
* after it is executed.
110+
* @chainable
111+
*/
112+
public function registerOne($event, $callback)
113+
{
114+
$this->_oneHandlers[$event][] = $callback;
115+
116+
return $this;
117+
}
118+
119+
97120
/**
98121
* Unregisters an event handler based on event, or all
99122
* @chainable

tests/Pheasant/Tests/EventsTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,19 @@ public function testSaveInAfterCreateDoesntLoop()
197197
$do->test = "blargh";
198198
$do->save();
199199
}
200+
201+
public function testRegisterOneEventBinding()
202+
{
203+
$fired = array();
204+
205+
$events = new Events();
206+
$events->registerOne('fireOnceEvent', function() use(&$fired) {
207+
$fired[] = 1;
208+
});
209+
210+
$events->trigger('fireOnceEvent', new \stdClass());
211+
$events->trigger('fireOnceEvent', new \stdClass());
212+
213+
$this->assertCount(1, $fired);
214+
}
200215
}

0 commit comments

Comments
 (0)