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

Add SAVEPOINT (Fixes #56) #111

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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