Skip to content

Instantly share code, notes, and snippets.

@jesseschalken
Last active April 6, 2016 04:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jesseschalken/bd856a513848382821e46973c7acea4b to your computer and use it in GitHub Desktop.
Save jesseschalken/bd856a513848382821e46973c7acea4b to your computer and use it in GitHub Desktop.
Transaction API for nested transactions in SQL databases. PHP 5.3+

Usage

This library implements nesting of transactions so code that uses transactions can be safely and arbitrarily composed.

There is a stack of in-progress transactions. Calling $txn = new Transaction(...) adds a transaction to the top of the stack. Calling $txn->commit() removes a transaction from the stack (and releases the savepoint). Calling $txn->rollback() rolls back a transaction and all transactions above it on the stack.

The transaction at the bottom of the stack represents a real START TRANSACTION/COMMIT/ROLLBACK. Transactions above the root transaction represent a SAVEPOINT .../ROLLBACK TO .../RELEASE SAVEPOINT .... Committing the root transaction does not COMMIT until all transactions on the stack have been committed or rolled back.

Nothing happens when a Transaction object falls out of scope. A transaction that is never committed or rolled back remains stuck on the stack and prevents the root transaction from committing. A Transaction object which commits on __destruct() if it hasn't already been committed or rolled back may be preferred.

The TransactionManager class can be used directly or it can be delegated to from another object implementing ITransactionManager (eg. a database connection class).

function foo(ITransactionManager $db) {
    $txn = new Transaction($db);
    // ...
    blah($db);
    // ...
    if ($okay) {
        $txn->commit();
    } else {
        $txn->rollbck();
    }
}

function blah(ITransactionManager $db) {
    $txn = new Transaction($db);
    try {
        // ...
    } catch (\Exception $e) {
        $txn->rollback();
        throw $e;
    }
    $txn->commit();
}

foo(new TransactionManager(...));
<?php
namespace JesseSchalken;
interface ITransactionDriver {
/**
* @param string $string
* @return string
*/
public function quote($string);
/**
* @param string $sql
* @return void
*/
public function exec($sql);
}
interface ITransactionManager {
/**
* @return string
*/
public function begin();
/**
* @param string $id
* @return void
*/
public function commit($id);
/**
* @param string $id
* @return void
*/
public function rollback($id);
}
final class TransactionManager implements ITransactionManager {
/** @var string[] */
private $stack = array();
/** @var bool[] */
private $commit = array();
/** @var ITransactionDriver */
private $driver;
/** @var int */
private $next = 1;
public function __construct(ITransactionDriver $driver) {
$this->driver = $driver;
}
public function isInTransaction() {
return $this->stack ? true : false;
}
public function begin() {
$id = 'savepoint' . $this->next++;
if ($this->stack) {
$this->driver->exec("SAVEPOINT {$this->driver->quote($id)}");
} else {
$this->driver->exec("START TRANSACTION");
}
$this->stack[] = $id;
$this->commit[$id] = false;
return $id;
}
public function commit($id) {
$id = "$id";
$this->check($id);
$this->commit[$id] = true;
while ($this->stack) {
$id = $this->stack[count($this->stack) - 1];
if (!$this->commit[$id]) {
break;
} else if ($id === $this->stack[0]) {
$this->driver->exec("COMMIT");
} else {
$this->driver->exec("RELEASE SAVEPOINT {$this->driver->quote($id)}");
}
array_pop($this->stack);
}
}
public function rollback($id) {
$id = "$id";
$this->check($id);
if ($id === $this->stack[0]) {
$this->driver->exec("ROLLBACK");
} else {
$this->driver->exec("ROLLBACK TO {$this->driver->quote($id)}");
$this->driver->exec("RELEASE SAVEPOINT {$this->driver->quote($id)}");
}
while (array_pop($this->stack) !== $id) {
}
}
private function check($id) {
if (!isset($this->commit[$id])) {
throw new Exception("Invalid transaction: $id");
} else if ($this->commit[$id]) {
throw new Exception("Transaction is already committed: $id");
} else if (!in_array($id, $this->stack, true)) {
throw new Exception("Transaction is already rolled back: $id");
}
}
}
final class Transaction {
private $tm;
private $id;
public function __construct(TransactionManager $tm) {
$this->tm = $tm;
$this->id = $tm->begin();
}
public function commit() {
$this->tm->commit($this->id);
}
public function rollback() {
$this->tm->rollback($this->id);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment