I have a class with more than a hundred functions/methods in it. Many of them require to run MySQLi query within a transaction. So, in a simple form, most of the functions look like this:
class MyClass {
private $connection = ...; // mysqli connection
public function a () {
$this->connection->autocommit(FALSE);
// do something and run multiple queries
$this->connection->commit();
}
public function b () {
$this->connection->autocommit(FALSE);
// do something and run multiple queries
$this->connection->commit();
}
}
// to run the code:
$myclass = new MyClass();
$myclass->a(); // all queries are run inside a transaction
$myclass->b(); // all queries are run inside a transaction
The concept is simple: Every time I run a public function/method, it starts a transaction and commits before ending the function/method. Now, my problem is... what if I want to run a method within another method, while keeping both methods public and callable by callers outside the class??? For example:
class MyClass {
private $connection = ...; // mysqli connection
public function a () {
$this->connection->autocommit(FALSE);
// do something and run multiple queries
$this->connection->commit();
}
public function b () {
$this->connection->autocommit(FALSE);
// do something and run multiple queries
$this->a(); // running function a inside function b here
$this->connection->commit();
}
}
// to run the code:
$myclass = new MyClass();
$myclass->a(); // all queries are run inside a transaction
$myclass->b(); // transaction is not working as I initially wished. This function consists of more than one commit, hence, not everything is run inside one transaction
Using extra variables for manual if/else checking, I suppose I can solve the problem above with two functions. However, my problem here is that I have multiple, possibly nearly a hundred, functions/methods that act like the above. I don't think I can use a few if/else to handle such complicated problem of transaction spaghetti hell.
How might I resolve this?
P.S:
The above is just a simplification of my class. To make things even more complicated, in my class I didn't just use:
function func() {
$this->connection->autocommit(FALSE);
// do something
$this->connection->commit();
}
Instead, I did something like this:
function func() {
if ($result = $this->connection->query('SELECT @@autocommit')) {
$row = mysqli_fetch_row($result);
$originalAutoCommitValue = $row[0];
mysqli_free_result($result);
}
if (!$originalAutoCommitValue) {
$this->connection->autocommit(FALSE);
}
// do something...
$this->connection->commit();
$this->connection->autocommit($originalAutoCommitValue);
}
This will allow the function/method in my class to start a transaction, commit the transaction and return the auto commit state back to before. This is really clumsy, but I hope that the users of my class have their choice of whether to use transaction/commit outside the class. I hope this approach will not affect their original state of auto commit before using any of my class' methods.
UPDATE
I have added the following properties and two functions in the class:
private $connectionTransactionStack = 0;
private $connectionOriginalAutoCommitState = NULL;
private function startTransaction() {
if (is_null($this->connectionOriginalAutoCommitState)) {
if ($result = $this->connection->query('SELECT @@autocommit')) {
$row = mysqli_fetch_row($result);
$this->connectionOriginalAutoCommitState = $row[0];
mysqli_free_result($result);
}
}
if ($this->connectionOriginalAutoCommitState === 1) {
$this->connection->autocommit(FALSE);
}
$this->connectionTransactionStack++;
}
private function commitTransaction() {
$this->connectionTransactionStack--;
if ($this->connectionTransactionStack < 1) {
$this->connectionTransactionStack = 0;
if ($this->connectionOriginalAutoCommitState === 1) {
$this->connection->commit();
$this->connection->autocommit($this->connectionOriginalAutoCommitState);
}
}
}
So, now when I want to run original functions, I can just do this:
class MyClass {
public function a () {
$this->startTransaction();
// do something and run multiple queries
$this->commitTransaction();
}
public function b () {
$this->startTransaction();
// do something and run multiple queries
$this->a(); // running function a inside function b here
$this->commitTransaction();
}
}
As long as I commit all transactions if I started any in any methods, the non-final $this->commitTransaction() will not run a real mysqli->commit(), but the final $this->commitTransaction() will run mysqli->commit if necessary (depending on the initial state before the first run any methods inside my class). So if I want to run:
$this->a();
or
$this->b();
, it doesn't matter at all because there is a stack to count the transaction I would have started. However, in real, there is only one transaction to be started and committed.
Would this be a solution to my problem above?
One option might be to delete:
$this->connection->autocommit(FALSE);
$this->connection->commit();
in every function that is relevant (a,b) and then wrap them around your function calls.
class MyClass {
private $connection = ...; // mysqli connection
public function a () {
// do something and run multiple queries
}
public function b () {
// do something and run multiple queries
$this->a(); // running function a inside function b here
}
}
// to run the code:
$myclass = new MyClass();
$myclass->connection->autocommit(FALSE);
$myclass->a(); // all queries are run inside a transaction
$myclass->b();
$this->connection->commit();
UPDATE:
OR maybe better , use a wrapper-function for calling the functions inside the class:
$myclass->doTransaction('a');
$myclass->doTransaction('b');
function doTransaction($functionCall) {
//Do some sanitizing of the functionCall-variable
$this->connection->autocommit(FALSE);
call_use_func($functionCall);
$this->connection->commit();
}