249 lines
7.7 KiB
PHP
249 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace Illuminate\Database;
|
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
class DatabaseTransactionsManager
|
|
{
|
|
/**
|
|
* All of the committed transactions.
|
|
*
|
|
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
|
|
*/
|
|
protected $committedTransactions;
|
|
|
|
/**
|
|
* All of the pending transactions.
|
|
*
|
|
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
|
|
*/
|
|
protected $pendingTransactions;
|
|
|
|
/**
|
|
* The current transaction.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $currentTransaction = [];
|
|
|
|
/**
|
|
* Create a new database transactions manager instance.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->committedTransactions = new Collection;
|
|
$this->pendingTransactions = new Collection;
|
|
}
|
|
|
|
/**
|
|
* Start a new database transaction.
|
|
*
|
|
* @param string $connection
|
|
* @param int $level
|
|
* @return void
|
|
*/
|
|
public function begin($connection, $level)
|
|
{
|
|
$this->pendingTransactions->push(
|
|
$newTransaction = new DatabaseTransactionRecord(
|
|
$connection,
|
|
$level,
|
|
$this->currentTransaction[$connection] ?? null
|
|
)
|
|
);
|
|
|
|
$this->currentTransaction[$connection] = $newTransaction;
|
|
}
|
|
|
|
/**
|
|
* Commit the root database transaction and execute callbacks.
|
|
*
|
|
* @param string $connection
|
|
* @param int $levelBeingCommitted
|
|
* @param int $newTransactionLevel
|
|
* @return array
|
|
*/
|
|
public function commit($connection, $levelBeingCommitted, $newTransactionLevel)
|
|
{
|
|
$this->stageTransactions($connection, $levelBeingCommitted);
|
|
|
|
if (isset($this->currentTransaction[$connection])) {
|
|
$this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent;
|
|
}
|
|
|
|
if (! $this->afterCommitCallbacksShouldBeExecuted($newTransactionLevel) &&
|
|
$newTransactionLevel !== 0) {
|
|
return [];
|
|
}
|
|
|
|
// This method is only called when the root database transaction is committed so there
|
|
// shouldn't be any pending transactions, but going to clear them here anyways just
|
|
// in case. This method could be refactored to receive a level in the future too.
|
|
$this->pendingTransactions = $this->pendingTransactions->reject(
|
|
fn ($transaction) => $transaction->connection === $connection &&
|
|
$transaction->level >= $levelBeingCommitted
|
|
)->values();
|
|
|
|
[$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition(
|
|
fn ($transaction) => $transaction->connection == $connection
|
|
);
|
|
|
|
$this->committedTransactions = $forOtherConnections->values();
|
|
|
|
$forThisConnection->map->executeCallbacks();
|
|
|
|
return $forThisConnection;
|
|
}
|
|
|
|
/**
|
|
* Move relevant pending transactions to a committed state.
|
|
*
|
|
* @param string $connection
|
|
* @param int $levelBeingCommitted
|
|
* @return void
|
|
*/
|
|
public function stageTransactions($connection, $levelBeingCommitted)
|
|
{
|
|
$this->committedTransactions = $this->committedTransactions->merge(
|
|
$this->pendingTransactions->filter(
|
|
fn ($transaction) => $transaction->connection === $connection &&
|
|
$transaction->level >= $levelBeingCommitted
|
|
)
|
|
);
|
|
|
|
$this->pendingTransactions = $this->pendingTransactions->reject(
|
|
fn ($transaction) => $transaction->connection === $connection &&
|
|
$transaction->level >= $levelBeingCommitted
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rollback the active database transaction.
|
|
*
|
|
* @param string $connection
|
|
* @param int $newTransactionLevel
|
|
* @return void
|
|
*/
|
|
public function rollback($connection, $newTransactionLevel)
|
|
{
|
|
if ($newTransactionLevel === 0) {
|
|
$this->removeAllTransactionsForConnection($connection);
|
|
} else {
|
|
$this->pendingTransactions = $this->pendingTransactions->reject(
|
|
fn ($transaction) => $transaction->connection == $connection &&
|
|
$transaction->level > $newTransactionLevel
|
|
)->values();
|
|
|
|
if ($this->currentTransaction) {
|
|
do {
|
|
$this->removeCommittedTransactionsThatAreChildrenOf($this->currentTransaction[$connection]);
|
|
|
|
$this->currentTransaction[$connection] = $this->currentTransaction[$connection]->parent;
|
|
} while (
|
|
isset($this->currentTransaction[$connection]) &&
|
|
$this->currentTransaction[$connection]->level > $newTransactionLevel
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all pending, completed, and current transactions for the given connection name.
|
|
*
|
|
* @param string $connection
|
|
* @return void
|
|
*/
|
|
protected function removeAllTransactionsForConnection($connection)
|
|
{
|
|
$this->currentTransaction[$connection] = null;
|
|
|
|
$this->pendingTransactions = $this->pendingTransactions->reject(
|
|
fn ($transaction) => $transaction->connection == $connection
|
|
)->values();
|
|
|
|
$this->committedTransactions = $this->committedTransactions->reject(
|
|
fn ($transaction) => $transaction->connection == $connection
|
|
)->values();
|
|
}
|
|
|
|
/**
|
|
* Remove all transactions that are children of the given transaction.
|
|
*
|
|
* @param \Illuminate\Database\DatabaseTransactionRecord $transaction
|
|
* @return void
|
|
*/
|
|
protected function removeCommittedTransactionsThatAreChildrenOf(DatabaseTransactionRecord $transaction)
|
|
{
|
|
[$removedTransactions, $this->committedTransactions] = $this->committedTransactions->partition(
|
|
fn ($committed) => $committed->connection == $transaction->connection &&
|
|
$committed->parent === $transaction
|
|
);
|
|
|
|
// There may be multiple deeply nested transactions that have already committed that we
|
|
// also need to remove. We will recurse down the children of all removed transaction
|
|
// instances until there are no more deeply nested child transactions for removal.
|
|
$removedTransactions->each(
|
|
fn ($transaction) => $this->removeCommittedTransactionsThatAreChildrenOf($transaction)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register a transaction callback.
|
|
*
|
|
* @param callable $callback
|
|
* @return void
|
|
*/
|
|
public function addCallback($callback)
|
|
{
|
|
if ($current = $this->callbackApplicableTransactions()->last()) {
|
|
return $current->addCallback($callback);
|
|
}
|
|
|
|
$callback();
|
|
}
|
|
|
|
/**
|
|
* Get the transactions that are applicable to callbacks.
|
|
*
|
|
* @return \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
|
|
*/
|
|
public function callbackApplicableTransactions()
|
|
{
|
|
return $this->pendingTransactions;
|
|
}
|
|
|
|
/**
|
|
* Determine if after commit callbacks should be executed for the given transaction level.
|
|
*
|
|
* @param int $level
|
|
* @return bool
|
|
*/
|
|
public function afterCommitCallbacksShouldBeExecuted($level)
|
|
{
|
|
return $level === 0;
|
|
}
|
|
|
|
/**
|
|
* Get all of the pending transactions.
|
|
*
|
|
* @return \Illuminate\Support\Collection
|
|
*/
|
|
public function getPendingTransactions()
|
|
{
|
|
return $this->pendingTransactions;
|
|
}
|
|
|
|
/**
|
|
* Get all of the committed transactions.
|
|
*
|
|
* @return \Illuminate\Support\Collection
|
|
*/
|
|
public function getCommittedTransactions()
|
|
{
|
|
return $this->committedTransactions;
|
|
}
|
|
}
|