OK I'm finding a solution for "NESTED" transactions in MySQL using PHP, and as you know in the MySQL documentation says that it's not possible to have transactions within transactions (Mysql transactions within transactions). I was trying to use the Database class propossed in http://php.net/manual/en/pdo.begintransaction.php but unfortunately that's wrong for me, because its counter scope is object level and not class level, to solve this issue I created this class (TransactionController) that has the counter (named $nest) static and it brings the class level required to make the transactions "linear" (with "linear" I'm saying: it aparently is nested but if you look quite it is not nested, then the transactions will work well, what do you think? (LOOK THE EXAMPLE AT THE END, CarOwner)
class TransactionController extends \\PDO {
public static $warn_rollback_was_thrown = false;
public static $transaction_rollbacked = false;
public function __construct()
{
parent :: __construct( ... connection info ... );
}
public static $nest = 0;
public function reset()
{
TransactionController :: $transaction_rollbacked = false;
TransactionController :: $warn_rollback_was_thrown = false;
TransactionController :: $nest = 0;
}
function beginTransaction()
{
$result = null;
if (TransactionController :: $nest == 0) {
$this->reset();
$result = parent :: beginTransaction();
}
TransactionController :: $nest++;
return $result;
}
public function commit()
{
$result = null;
if (TransactionController :: $nest == 0 &&
!TransactionController :: $transaction_rollbacked &&
!TransactionController :: $warn_rollback_was_thrown) {
$result = parent :: commit();
}
TransactionController :: $nest--;
return $result;
}
public function rollback()
{
$result = null;
if (TransactionController :: $nest >= 0) {
if (TransactionController :: $nest == 0) {
$result = parent :: rollback();
TransactionController :: $transaction_rollbacked = true;
}
else {
TransactionController :: $warn_rollback_was_thrown = true;
}
}
TransactionController :: $nest--;
return $result;
}
public function transactionFailed()
{
return TransactionController :: $warn_rollback_was_thrown === true;
}
// to force rollback you can only do it from $nest = 0
public function forceRollback()
{
if (TransactionController :: $nest === 0) {
throw new \PDOException();
}
}
}
class CarData extends TransactionController {
public function insertCar()
{
try {
$this->beginTransaction();
... (operations) ...
$this->commit();
}
catch (\PDOException $e) {
$this->rollback();
}
}
}
class PersonData extends TransactionController {
public function insertPerson( $person=null )
{
try {
$this->beginTransaction();
... (operations) ...
$this->commit();
}
catch (\PDOException $e) {
$this->rollback();
}
}
}
class CarOwnerData extends TransactionController {
public function createOwner()
{
try {
$this->beginTransaction();
$car = new CarData();
$car->insertCar();
$person = new PersonData();
$person->insertPerson();
... (operations) ...
$this->commit();
}
catch (\PDOException $e) {
$this->rollback();
}
}
}
$sellCar = new CarOwnerData();
$sellCar->createOwner();
UPDATE1: static attribute $warn_rollback_was_thrown was added to TransactionController in order to warn that the transaction was failed in some moment of the execution, but there wasn't rollbacked.
UPDATE2: When a transaction fails in some moment you can let the code still running to the end or STOP it definitively using forceRollback(), as an example of this see the following code:
<?php // inside the class PersonData
public function insertMultiplePersons( $arrayPersons )
{
try {
$this->beginTransaction();
if (is_array( $arrayPersons )) {
foreach ($arrayPersons as $k => $person) {
$this->insertPerson( $person );
if ($this->transactionFailed()) {
$this->forceRollback();
}
}
}
$this->commit();
}
catch (\PDOException $e) {
$this->rollback();
}
} ?>
As pointed out by @YourCommonSense in the comments, you aren't actually implementing nested transactions.
I'm not sure I like the idea of calling commit() anywhere in my code and it not actually committing anything.
Your whole solution seems to be an attempt to mitigate a design decision to put transaction code in your insert functions and forget about it.
You could separate your insert operations from the transaction logic and wrap these function calls in a separate function which does the transactions:
public/private function insertPerson( $person=null )
{
... (operations) ...
}
public function createPerson()
{
$person = new Person();
... (setup person) ...
$this->beginTransaction();
try {
$this->insertPerson($person);
$this->commit();
}
catch (\PDOException $e) {
$this->rollback();
}
}
If you are absolutely sure you need to always insert the person within a transaction, you could check you are within a transaction when it is called:
public/private function insertPerson($person=null)
{
if (!$this->hasActiveTransaction){ // Needs implementing
throw new Exception('Must be called within a transaction');
}
...(operations)...
}
In our projects, all the saving logic is within the Models and all transaction logic is at the Controller level.
I'm assuming you are aware that for a single statement there is no need for a transaction, as these are atomic operations, and that your code represents more complex cases.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With