352 lines
9.4 KiB
PHP
352 lines
9.4 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Illuminate\Database\Concerns;
|
||
|
|
||
|
use Closure;
|
||
|
use Illuminate\Database\DeadlockException;
|
||
|
use RuntimeException;
|
||
|
use Throwable;
|
||
|
|
||
|
trait ManagesTransactions
|
||
|
{
|
||
|
/**
|
||
|
* Execute a Closure within a transaction.
|
||
|
*
|
||
|
* @param \Closure $callback
|
||
|
* @param int $attempts
|
||
|
* @return mixed
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
public function transaction(Closure $callback, $attempts = 1)
|
||
|
{
|
||
|
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
|
||
|
$this->beginTransaction();
|
||
|
|
||
|
// We'll simply execute the given callback within a try / catch block and if we
|
||
|
// catch any exception we can rollback this transaction so that none of this
|
||
|
// gets actually persisted to a database or stored in a permanent fashion.
|
||
|
try {
|
||
|
$callbackResult = $callback($this);
|
||
|
}
|
||
|
|
||
|
// If we catch an exception we'll rollback this transaction and try again if we
|
||
|
// are not out of attempts. If we are out of attempts we will just throw the
|
||
|
// exception back out, and let the developer handle an uncaught exception.
|
||
|
catch (Throwable $e) {
|
||
|
$this->handleTransactionException(
|
||
|
$e, $currentAttempt, $attempts
|
||
|
);
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$levelBeingCommitted = $this->transactions;
|
||
|
|
||
|
try {
|
||
|
if ($this->transactions == 1) {
|
||
|
$this->fireConnectionEvent('committing');
|
||
|
$this->getPdo()->commit();
|
||
|
}
|
||
|
|
||
|
$this->transactions = max(0, $this->transactions - 1);
|
||
|
} catch (Throwable $e) {
|
||
|
$this->handleCommitTransactionException(
|
||
|
$e, $currentAttempt, $attempts
|
||
|
);
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$this->transactionsManager?->commit(
|
||
|
$this->getName(),
|
||
|
$levelBeingCommitted,
|
||
|
$this->transactions
|
||
|
);
|
||
|
|
||
|
$this->fireConnectionEvent('committed');
|
||
|
|
||
|
return $callbackResult;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle an exception encountered when running a transacted statement.
|
||
|
*
|
||
|
* @param \Throwable $e
|
||
|
* @param int $currentAttempt
|
||
|
* @param int $maxAttempts
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
|
||
|
{
|
||
|
// On a deadlock, MySQL rolls back the entire transaction so we can't just
|
||
|
// retry the query. We have to throw this exception all the way out and
|
||
|
// let the developer handle it in another way. We will decrement too.
|
||
|
if ($this->causedByConcurrencyError($e) &&
|
||
|
$this->transactions > 1) {
|
||
|
$this->transactions--;
|
||
|
|
||
|
$this->transactionsManager?->rollback(
|
||
|
$this->getName(), $this->transactions
|
||
|
);
|
||
|
|
||
|
throw new DeadlockException($e->getMessage(), is_int($e->getCode()) ? $e->getCode() : 0, $e);
|
||
|
}
|
||
|
|
||
|
// If there was an exception we will rollback this transaction and then we
|
||
|
// can check if we have exceeded the maximum attempt count for this and
|
||
|
// if we haven't we will return and try this query again in our loop.
|
||
|
$this->rollBack();
|
||
|
|
||
|
if ($this->causedByConcurrencyError($e) &&
|
||
|
$currentAttempt < $maxAttempts) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
throw $e;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start a new database transaction.
|
||
|
*
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
public function beginTransaction()
|
||
|
{
|
||
|
foreach ($this->beforeStartingTransaction as $callback) {
|
||
|
$callback($this);
|
||
|
}
|
||
|
|
||
|
$this->createTransaction();
|
||
|
|
||
|
$this->transactions++;
|
||
|
|
||
|
$this->transactionsManager?->begin(
|
||
|
$this->getName(), $this->transactions
|
||
|
);
|
||
|
|
||
|
$this->fireConnectionEvent('beganTransaction');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a transaction within the database.
|
||
|
*
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
protected function createTransaction()
|
||
|
{
|
||
|
if ($this->transactions == 0) {
|
||
|
$this->reconnectIfMissingConnection();
|
||
|
|
||
|
try {
|
||
|
$this->getPdo()->beginTransaction();
|
||
|
} catch (Throwable $e) {
|
||
|
$this->handleBeginTransactionException($e);
|
||
|
}
|
||
|
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
|
||
|
$this->createSavepoint();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a save point within the database.
|
||
|
*
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
protected function createSavepoint()
|
||
|
{
|
||
|
$this->getPdo()->exec(
|
||
|
$this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle an exception from a transaction beginning.
|
||
|
*
|
||
|
* @param \Throwable $e
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
protected function handleBeginTransactionException(Throwable $e)
|
||
|
{
|
||
|
if ($this->causedByLostConnection($e)) {
|
||
|
$this->reconnect();
|
||
|
|
||
|
$this->getPdo()->beginTransaction();
|
||
|
} else {
|
||
|
throw $e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Commit the active database transaction.
|
||
|
*
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
public function commit()
|
||
|
{
|
||
|
if ($this->transactionLevel() == 1) {
|
||
|
$this->fireConnectionEvent('committing');
|
||
|
$this->getPdo()->commit();
|
||
|
}
|
||
|
|
||
|
[$levelBeingCommitted, $this->transactions] = [
|
||
|
$this->transactions,
|
||
|
max(0, $this->transactions - 1),
|
||
|
];
|
||
|
|
||
|
$this->transactionsManager?->commit(
|
||
|
$this->getName(), $levelBeingCommitted, $this->transactions
|
||
|
);
|
||
|
|
||
|
$this->fireConnectionEvent('committed');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle an exception encountered when committing a transaction.
|
||
|
*
|
||
|
* @param \Throwable $e
|
||
|
* @param int $currentAttempt
|
||
|
* @param int $maxAttempts
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
|
||
|
{
|
||
|
$this->transactions = max(0, $this->transactions - 1);
|
||
|
|
||
|
if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ($this->causedByLostConnection($e)) {
|
||
|
$this->transactions = 0;
|
||
|
}
|
||
|
|
||
|
throw $e;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Rollback the active database transaction.
|
||
|
*
|
||
|
* @param int|null $toLevel
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
public function rollBack($toLevel = null)
|
||
|
{
|
||
|
// We allow developers to rollback to a certain transaction level. We will verify
|
||
|
// that this given transaction level is valid before attempting to rollback to
|
||
|
// that level. If it's not we will just return out and not attempt anything.
|
||
|
$toLevel = is_null($toLevel)
|
||
|
? $this->transactions - 1
|
||
|
: $toLevel;
|
||
|
|
||
|
if ($toLevel < 0 || $toLevel >= $this->transactions) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Next, we will actually perform this rollback within this database and fire the
|
||
|
// rollback event. We will also set the current transaction level to the given
|
||
|
// level that was passed into this method so it will be right from here out.
|
||
|
try {
|
||
|
$this->performRollBack($toLevel);
|
||
|
} catch (Throwable $e) {
|
||
|
$this->handleRollBackException($e);
|
||
|
}
|
||
|
|
||
|
$this->transactions = $toLevel;
|
||
|
|
||
|
$this->transactionsManager?->rollback(
|
||
|
$this->getName(), $this->transactions
|
||
|
);
|
||
|
|
||
|
$this->fireConnectionEvent('rollingBack');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Perform a rollback within the database.
|
||
|
*
|
||
|
* @param int $toLevel
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
protected function performRollBack($toLevel)
|
||
|
{
|
||
|
if ($toLevel == 0) {
|
||
|
$pdo = $this->getPdo();
|
||
|
|
||
|
if ($pdo->inTransaction()) {
|
||
|
$pdo->rollBack();
|
||
|
}
|
||
|
} elseif ($this->queryGrammar->supportsSavepoints()) {
|
||
|
$this->getPdo()->exec(
|
||
|
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle an exception from a rollback.
|
||
|
*
|
||
|
* @param \Throwable $e
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \Throwable
|
||
|
*/
|
||
|
protected function handleRollBackException(Throwable $e)
|
||
|
{
|
||
|
if ($this->causedByLostConnection($e)) {
|
||
|
$this->transactions = 0;
|
||
|
|
||
|
$this->transactionsManager?->rollback(
|
||
|
$this->getName(), $this->transactions
|
||
|
);
|
||
|
}
|
||
|
|
||
|
throw $e;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the number of active transactions.
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
public function transactionLevel()
|
||
|
{
|
||
|
return $this->transactions;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute the callback after a transaction commits.
|
||
|
*
|
||
|
* @param callable $callback
|
||
|
* @return void
|
||
|
*
|
||
|
* @throws \RuntimeException
|
||
|
*/
|
||
|
public function afterCommit($callback)
|
||
|
{
|
||
|
if ($this->transactionsManager) {
|
||
|
return $this->transactionsManager->addCallback($callback);
|
||
|
}
|
||
|
|
||
|
throw new RuntimeException('Transactions Manager has not been set.');
|
||
|
}
|
||
|
}
|