Skip to content

Commit c3b9323

Browse files
Merge pull request #12 from esmaeelghasemi/feature_transaction
Database Transaction Feature
2 parents 4a38b8a + 4e66edd commit c3b9323

File tree

14 files changed

+721
-0
lines changed

14 files changed

+721
-0
lines changed

docs/transaction.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Transaction
2+
3+
**Transaction** class has three properties:
4+
5+
1. `queries`
6+
2. `queryResultCollection`
7+
3. `connection`
8+
9+
The <b>Transaction</b> class contains several methods such as :
10+
11+
- `addQuery`
12+
- `resolveQueries`
13+
- `compile`
14+
15+
### __constructor
16+
17+
when we create an instance of this class it reserves a database connection using `reserveConnection` method and stores it
18+
in `connection` property, to use it
19+
for all transaction queries.
20+
21+
### addQuery
22+
23+
This method takes three arguments :
24+
25+
1. `name`<small>(string)</small>.
26+
2. `query`<small>(Select|Update|Delete|Insert)</small>.
27+
3. `callback`<small>(Closure)</small> by default its null.
28+
29+
This method push arguments as an array to **queries** property
30+
31+
**Returns: `Transaction`**
32+
33+
### resolveQueries
34+
35+
this method doesn't take any argument.
36+
37+
This method flow:
38+
39+
- if `queries` is empty, it commits the transaction and return a promise with result of last query.
40+
- if `queries` is not empty, it shifts the first item of this property and stores it in a variable.
41+
- we get the query string of the selected query and send a query to database with this query and by using the reserved
42+
connection.
43+
- if the result of query returned `false` we `rollback` the transaction and return a reject promise with
44+
`TransactionException` exception.
45+
- otherwise if selected query has callback, we call and check the callback to be true (we inject two argument to this
46+
callback, first the current query result and next the collection of all transaction query results).
47+
- it the callback is null, or it returns true we call the current method , and it starts from first flow.
48+
49+
**Returns: `Promise`**
50+
51+
### compile
52+
53+
This method doesn't take any argument.
54+
55+
This method flow:
56+
57+
- if `queries` is empty, it throws `TransactionException('There are no queries inside transaction.')`.
58+
- it starts the transaction.
59+
- if transaction started, it calls the `resolveQueries` method, otherwise it release the reserved connection.
60+
61+
**Returns: `Promise`**
62+
63+
64+
65+
66+
67+
68+

example/transaction.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
use Dotenv\Dotenv;
4+
use React\EventLoop\Loop;
5+
use Saraf\QB\QueryBuilder\Contracts\QueryResultCollectionContract;
6+
use Saraf\QB\QueryBuilder\Core\DBFactory;
7+
use Saraf\QB\QueryBuilder\Exceptions\DBFactoryException;
8+
use Saraf\QB\QueryBuilder\Helpers\QueryResult\QueryResult;
9+
10+
include "vendor/autoload.php";
11+
12+
// Loop
13+
$loop = Loop::get();
14+
15+
// Environments
16+
$env = Dotenv::createImmutable(__DIR__ . "/../");
17+
$env->load();
18+
19+
// Env Loader
20+
$DB_NAME = $_ENV['DB_NAME'];
21+
$DB_USER = $_ENV['DB_USER'];
22+
$DB_PASS = $_ENV['DB_PASS'];
23+
$DB_HOST = $_ENV['DB_HOST'];
24+
$DB_PORT_READ = $_ENV['DB_PORT_READ'];
25+
$DB_PORT_WRITE = $_ENV['DB_PORT_WRITE'];
26+
27+
28+
try {
29+
$dbFactory = new DBFactory(
30+
$loop,
31+
$DB_HOST,
32+
$DB_NAME,
33+
$DB_USER,
34+
$DB_PASS,
35+
$DB_PORT_WRITE,
36+
$DB_PORT_READ,
37+
5,
38+
5,
39+
2,
40+
2
41+
);
42+
} catch (DBFactoryException $e) {
43+
echo $e->getMessage();
44+
exit(1);
45+
}
46+
47+
$tr = $dbFactory->getQueryBuilder()->beginTransaction();
48+
$tr->addQuery(
49+
'selectUser',
50+
$dbFactory->getQueryBuilder()
51+
->select()
52+
->from("Users")
53+
->where('id', 1),
54+
function (QueryResult $result) {
55+
if ($result->count == 0) {
56+
return false;
57+
}
58+
return true;
59+
}
60+
);
61+
$tr->addQuery('selectBalance',
62+
$dbFactory->getQueryBuilder()
63+
->select()
64+
->from('Balances')
65+
->where('userId', 1)
66+
->where('symbol', 'IRT')
67+
);
68+
69+
$tr->addQuery('insertTransactions',
70+
$dbFactory->getQueryBuilder()
71+
->insert()
72+
->into('Transactions')
73+
->setColumns([
74+
'userId',
75+
'type',
76+
'amount'
77+
])
78+
->addRow([
79+
1,
80+
'WITHDRAW',
81+
100_000
82+
]),
83+
function (QueryResult $result, QueryResultCollectionContract $contract) {
84+
if ($contract->get('selectBalance')->rows[0]['balance'] < 100_000) {
85+
return false;
86+
}
87+
88+
return true;
89+
}
90+
);
91+
92+
$tr->addQuery('updateBalance',
93+
$dbFactory->getQueryBuilder()
94+
->update()
95+
->table('Balances')
96+
->setUpdate('balance', 'balance - ' . 100_000, false)
97+
->where('userId', 1)
98+
->where('symbol', 'IRT')
99+
);
100+
101+
$tr->compile()->then(function ($result) {
102+
var_dump($result);
103+
})->catch(function (\Throwable $throwable) {
104+
var_dump($throwable->getMessage());
105+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Saraf\QB\QueryBuilder\Clauses;
4+
5+
use Saraf\QB\QueryBuilder\Core\DBFactory;
6+
use Saraf\QB\QueryBuilder\Core\DBWorker;
7+
use Saraf\QB\QueryBuilder\Exceptions\TransactionException;
8+
use Saraf\QB\QueryBuilder\Helpers\QueryResult\QueryResult;
9+
use Saraf\QB\QueryBuilder\Helpers\QueryResult\QueryResultCollection;
10+
use function React\Promise\reject;
11+
use function React\Promise\resolve;
12+
13+
class Transaction
14+
{
15+
protected array $queries = [];
16+
protected QueryResultCollection $queryResultCollection;
17+
private ?DBWorker $connection = null;
18+
19+
public function __construct(
20+
protected ?DBFactory $dbFactory = null,
21+
)
22+
{
23+
$this->queryResultCollection = new QueryResultCollection();
24+
$this->connection = !is_null($this->dbFactory) ? $this->dbFactory->reserveConnection() : null;
25+
}
26+
27+
public function addQuery(string $name, Select|Update|Delete|Insert $query, ?\Closure $callback = null): static
28+
{
29+
$this->queries[] = compact('name', 'query', 'callback');
30+
return $this;
31+
}
32+
33+
/**
34+
* @throws TransactionException
35+
*/
36+
public function compile(): \React\Promise\PromiseInterface
37+
{
38+
if (count($this->queries) === 0) {
39+
throw new TransactionException('There are no queries inside transaction.');
40+
}
41+
42+
return $this->connection->query("START TRANSACTION")
43+
->then(function () {
44+
return $this->resolveQueries();
45+
})
46+
->finally(function () {
47+
$this->dbFactory->releaseConnection($this->connection);
48+
});
49+
}
50+
51+
protected function resolveQueries(): \React\Promise\PromiseInterface
52+
{
53+
if (count($this->queries) === 0) {
54+
$this->connection->query('COMMIT');
55+
return resolve($this->queryResultCollection->last()->toArray());
56+
}
57+
58+
$queryItem = array_shift($this->queries);
59+
60+
$query = $queryItem['query'];
61+
$callback = $queryItem['callback'];
62+
$name = $queryItem['name'];
63+
64+
return $query->compile()->getQuery()->then(function ($result) use ($query, $callback, $name) {
65+
$queryString = $result['query'];
66+
return $this->connection->query($queryString)
67+
->then(function ($result) use ($query, $callback, $name, $queryString) {
68+
if (!$result['result']) {
69+
$this->connection->query('ROLLBACK');
70+
return reject(throw new TransactionException('Transaction rolled back due to ' . $result['error']));
71+
}
72+
73+
$queryResult = new QueryResult(
74+
$result['result'],
75+
@$result['count'] ?? null,
76+
@$result['rows'] ?? [],
77+
@$result['affectedRows'] ?? null,
78+
@$result['insertId'] ?? null,
79+
);
80+
81+
if (is_null($callback) || $callback($queryResult, $this->queryResultCollection)) {
82+
$this->queryResultCollection->add($name, $queryResult);
83+
return $this->resolveQueries();
84+
}
85+
86+
$this->connection->query('ROLLBACK');
87+
return reject(throw new TransactionException("Transaction rolled back,callback for query {$queryString} doesn't return true"));
88+
});
89+
});
90+
}
91+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Saraf\QB\QueryBuilder\Contracts;
4+
5+
use Saraf\QB\QueryBuilder\Helpers\QueryResult\QueryResult;
6+
7+
interface QueryResultCollectionContract
8+
{
9+
public function add(string $name, QueryResult $queryResult): self;
10+
public function get(string $name): QueryResult;
11+
}

src/QueryBuilder/Core/DBFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
class DBFactory
1414
{
15+
use ReservableConnection;
16+
1517
private array $logs = [];
1618
private const MAX_CONNECTION_COUNT = 1000000000;
1719

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Saraf\QB\QueryBuilder\Core;
4+
5+
trait ReservableConnection
6+
{
7+
public function reserveConnection(): DBWorker
8+
{
9+
return array_pop($this->writeConnections);
10+
}
11+
12+
public function releaseConnection(DBWorker $connection): void
13+
{
14+
$this->writeConnections[] = $connection;
15+
}
16+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Saraf\QB\QueryBuilder\Exceptions;
4+
5+
class QueryResultException extends \Exception
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Saraf\QB\QueryBuilder\Exceptions;
4+
5+
class TransactionException extends \Exception
6+
{
7+
8+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Saraf\QB\QueryBuilder\Helpers\QueryResult;
4+
5+
class QueryResult
6+
{
7+
public function __construct(
8+
public bool $result,
9+
public ?int $count = null,
10+
public array $rows = [],
11+
public ?int $affectedRows = null,
12+
public ?int $insertId = null
13+
)
14+
{
15+
}
16+
17+
public function toArray(): array
18+
{
19+
return [
20+
'result' => $this->result,
21+
'count' => $this->count,
22+
'rows' => $this->rows,
23+
'affectedRows' => $this->affectedRows,
24+
'insertId' => $this->insertId
25+
];
26+
}
27+
}

0 commit comments

Comments
 (0)