master

laravel/framework

Last updated at: 29/12/2023 09:22

ManagesTransactions.php

TLDR

This file, ManagesTransactions.php, is a trait that provides methods for managing transactional database operations in the Illuminate\Database namespace. The trait includes methods for executing closures within a transaction, handling exceptions encountered during transactions, starting and committing transactions, rolling back transactions, and getting the number of active transactions. It also includes a method for executing a callback after a transaction commits.

Methods

transaction

This method executes a closure within a transaction. It takes a closure as a parameter and an optional number of attempts to retry the transaction in case of exceptions. If an exception occurs, the method will rollback the transaction and retry a specified number of times. If the maximum number of attempts is reached, the exception will be thrown. The method returns the result of the closure.

handleTransactionException

This protected method handles exceptions encountered when running a transacted statement. It takes the exception, the current attempt number, and the maximum number of attempts as parameters. If the exception is a deadlock exception and there are still transactions in progress, the method will roll back the transaction, decrement the number of transactions, and throw a DeadlockException. If the exception is a concurrency error and the current attempt is less than the maximum attempts, the method will return without throwing an exception. Otherwise, the exception will be thrown.

beginTransaction

This method starts a new database transaction. It creates a new transaction and increments the transaction count. It also fires the "beganTransaction" connection event.

createTransaction

This protected method creates a transaction within the database. If there are no transactions in progress, it begins a new transaction using the connection's PDO object. If there are already transactions in progress and the query grammar supports savepoints, it creates a savepoint within the transaction.

createSavepoint

This protected method creates a savepoint within the database. It executes a SQL statement to create a savepoint with a unique name based on the current transaction count.

handleBeginTransactionException

This protected method handles exceptions encountered when beginning a transaction. If the exception indicates a lost connection, it reconnects to the database and attempts to begin the transaction again. Otherwise, it throws the exception.

commit

This method commits the active database transaction. If the transaction level is 1, it fires the "committing" connection event and calls the commit() method on the connection's PDO object. It then updates the transaction count and calls the commit() method on the transactions manager if it is set. Finally, it fires the "committed" connection event.

handleCommitTransactionException

This protected method handles exceptions encountered when committing a transaction. If the exception is a concurrency error and the current attempt is less than the maximum attempts, the method returns without throwing an exception. If the exception is a lost connection error, the method sets the transaction count to 0. Otherwise, it throws the exception.

rollBack

This method rolls back the active database transaction. If a specific transaction level is provided, it rolls back to that level. Otherwise, it rolls back to the previous transaction level. If the provided transaction level is invalid, it returns without performing any rollback operations. After performing the rollback, it updates the transaction count, calls the rollback() method on the transactions manager if it is set, and fires the "rollingBack" connection event.

performRollBack

This protected method performs the actual rollback within the database. If the transaction level is 0, it rolls back the entire transaction using the connection's PDO object. If the query grammar supports savepoints, it executes a SQL statement to roll back to the specified savepoint.

handleRollBackException

This protected method handles exceptions encountered when rolling back a transaction. If the exception indicates a lost connection, it sets the transaction count to 0 and calls the rollback() method on the transactions manager if it is set. Otherwise, it throws the exception.

transactionLevel

This method returns the number of active transactions.

afterCommit

This method registers a callback to be executed after a transaction commits. If a transactions manager has been set, the method adds the callback to the transactions manager. Otherwise, it throws a RuntimeException indicating that the transactions manager has not been set.

END

<?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;
            }

            try {
                if ($this->transactions == 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
                );
            } catch (Throwable $e) {
                $this->handleCommitTransactionException(
                    $e, $currentAttempt, $attempts
                );

                continue;
            }

            $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()
    {
        $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.');
    }
}