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

Commit

Permalink
Adding Savepoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Jud committed Apr 25, 2015
1 parent a833f4a commit e547534
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 36 deletions.
59 changes: 59 additions & 0 deletions lib/Pheasant/Database/Mysqli/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Pheasant\Database\Dsn;
use Pheasant\Database\FilterChain;
use Pheasant\Database\MysqlPlatform;
use Pheasant\Database\Mysqli\SavePointStack;

/**
* A connection to a MySql database
Expand All @@ -19,6 +20,8 @@ class Connection
$_sequencePool,
$_strict,
$_selectedDatabase,
$_savePointStack,
$_events,
$_debug=false
;

Expand All @@ -43,7 +46,45 @@ 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->_savePointStack = new SavePointStack();

// Keep a copy of ourselves around
$self = $this;

// 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 afterTransaction replaces commitTransaction, and is where
// we will COMMIT or RELEASE
$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('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}");
});
}

/**
Expand Down Expand Up @@ -248,4 +289,22 @@ public function selectedDatabase()
{
return $this->_selectedDatabase;
}

/**
* Returns the Event object
* @return Event
*/
public function events()
{
return $this->_events;
}

/**
* Returns the transaction stack
* @return SavePointStack
*/
public function savePointStack()
{
return $this->_savePointStack;
}
}
45 changes: 45 additions & 0 deletions lib/Pheasant/Database/Mysqli/SavePointStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Pheasant\Database\Mysqli;

/**
* A Transaction Stack that keeps track of open savepoints
*/
class SavePointStack
{
private
$_savePointStack = array()
;

/**
* Get the depth of the stack
* @return integer
*/
public function depth()
{
return count($this->_savePointStack);
}

/**
* Decend deeper into the transaction stack and return a unique
* transaction savepoint name
* @return string
*/
public function descend()
{
$this->_savePointStack[] = current($this->_savePointStack) === false
? null
: 'savepoint_'.$this->depth();

return end($this->_savePointStack);
}

/**
* Pop off the last savepoint
* @return string
*/
public function pop()
{
return array_pop($this->_savePointStack);
}
}
36 changes: 23 additions & 13 deletions lib/Pheasant/Database/Mysqli/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,19 @@ 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()
{
$this->results = array();

try {
$this->_connection->execute('BEGIN');
$this->_events->trigger('startTransaction', $this->_connection);
$this->_connection->execute('COMMIT');
$this->_events->trigger('commitTransaction', $this->_connection);
$this->_events->wrap('Transaction', $this, function($self) {
$self->events()->trigger('transaction', $self->connection());
});
} catch (\Exception $e) {
$this->_connection->execute('ROLLBACK');
$this->_events->trigger('rollbackTransaction', $this->_connection);
$this->_events->trigger('rollback', $this->_connection);
throw $e;
}

Expand All @@ -51,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);
});

Expand All @@ -67,23 +65,35 @@ 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
*/
public function deferEvents($events)
{
$this->_events
->register('startTransaction', function() use ($events) {
->registerOne('beforeTransaction', function() use ($events) {
$events->cork();
})
->register('commitTransaction', 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
Expand Down
23 changes: 23 additions & 0 deletions lib/Pheasant/Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Events
{
private
$_handlers = array(),
$_oneHandlers = array(),
$_queue = array(),
$_corked = false,
$_upstream
Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions tests/Pheasant/Tests/EventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading

0 comments on commit e547534

Please sign in to comment.