Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. TransactionManagerBase.php

class TransactionManagerBase

The database transaction manager base class.

On many databases transactions cannot nest. Instead, we track nested calls to transactions and collapse them into a single client transaction.

Database drivers must implement their own class extending from this, and instantiate it via their Connection::driverTransactionManager() method.

Hierarchy

  • class \Drupal\Core\Database\Transaction\TransactionManagerBase implements \Drupal\Core\Database\Transaction\TransactionManagerInterface

Expanded class hierarchy of TransactionManagerBase

See also

\Drupal\Core\Database\Connection::driverTransactionManager()

3 files declare their use of TransactionManagerBase
TransactionManager.php in core/modules/pgsql/src/Driver/Database/pgsql/TransactionManager.php
TransactionManager.php in core/modules/sqlite/src/Driver/Database/sqlite/TransactionManager.php
TransactionManager.php in core/modules/mysql/src/Driver/Database/mysql/TransactionManager.php

File

core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php, line 24

Namespace

Drupal\Core\Database\Transaction
View source
abstract class TransactionManagerBase implements TransactionManagerInterface {
    
    /**
     * The ID of the root Transaction object.
     *
     * The unique identifier of the first 'root' transaction object created, when
     * the stack is empty.
     *
     * Normally, during the transaction stack lifecycle only one 'root'
     * Transaction object is processed. Any post transaction callbacks are only
     * processed during its destruction. However, there are cases when there
     * could be multiple 'root' transaction objects in the stack. For example: a
     * 'root' transaction object is opened, then a DDL statement is executed in a
     * database that does not support transactional DDL, and because of that,
     * another 'root' is opened before the original one is closed.
     *
     * Keeping track of the first 'root' created allows us to process the post
     * transaction callbacks only during its destruction and not during
     * destruction of another one.
     */
    private ?string $rootId = NULL;
    
    /**
     * The stack of Drupal transactions currently active.
     *
     * This property is keeping track of the Transaction objects started and
     * ended as a LIFO (Last In, First Out) stack.
     *
     * The database API allows to begin transactions, add an arbitrary number of
     * additional savepoints, and release any savepoint in the sequence. When
     * this happens, the database will implicitly release all the savepoints
     * created after the one released. Given Drupal implementation of the
     * Transaction objects, we cannot force reducing the scope of the
     * corresponding Transaction savepoint objects from the manager, because they
     * live in the scope of the calling code. This stack ensures that when an
     * outlived Transaction object gets out of scope, it will not try to release
     * on the database a savepoint that no longer exists.
     *
     * Differently, rollbacks are strictly being checked for LIFO order: if a
     * rollback is requested against a savepoint that is not the last created,
     * the manager will throw a TransactionOutOfOrderException.
     *
     * The array key is the transaction's unique id, its value a StackItem.
     *
     * @var array<string,StackItem>
     */
    private array $stack = [];
    
    /**
     * A list of voided stack items.
     *
     * In some cases the active transaction can be automatically committed by the
     * database server (for example, MySql when a DDL statement is executed
     * during a transaction). In such cases we need to void the remaining items
     * on the stack, and we track them here.
     *
     * The array key is the transaction's unique id, its value a StackItem.
     *
     * @var array<string,StackItem>
     */
    private array $voidedItems = [];
    
    /**
     * A list of post-transaction callbacks.
     *
     * @var callable[]
     */
    private array $postTransactionCallbacks = [];
    
    /**
     * The state of the underlying client connection transaction.
     *
     * Note that this is a proxy of the actual state on the database server,
     * best determined through calls to methods in this class. The actual
     * state on the database server could be different.
     */
    private ClientConnectionTransactionState $connectionTransactionState;
    
    /**
     * Constructor.
     *
     * @param \Drupal\Core\Database\Connection $connection
     *   The database connection.
     */
    public function __construct(Connection $connection) {
    }
    
    /**
     * Destructor.
     *
     * When destructing, $stack must have been already emptied.
     */
    public function __destruct() {
        assert($this->stack === [], "Transaction \$stack was not empty. Active stack: " . $this->dumpStackItemsAsString());
    }
    
    /**
     * Returns the current depth of the transaction stack.
     *
     * @return int
     *   The current depth of the transaction stack.
     *
     * @todo consider making this function protected.
     *
     * @internal
     */
    public function stackDepth() : int {
        return count($this->stack());
    }
    
    /**
     * Returns the content of the transaction stack.
     *
     * Drivers should not override this method unless they also override the
     * $stack property.
     *
     * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn
     * @return array<string,StackItem>
     *   The elements of the transaction stack.
     */
    protected function stack() : array {
        return $this->stack;
    }
    
    /**
     * Commits the entire transaction stack.
     *
     * @internal
     *   This method exists only to work around a bug caused by Drupal incorrectly
     *   relying on object destruction order to commit transactions. Xdebug 3.3.0
     *   changes the order of object destruction when the develop mode is enabled.
     */
    public function commitAll() : void {
        foreach (array_reverse($this->stack()) as $id => $item) {
            $this->unpile($item->name, $id);
        }
    }
    
    /**
     * Adds an item to the transaction stack.
     *
     * Drivers should not override this method unless they also override the
     * $stack property.
     *
     * @param string $id
     *   The id of the transaction.
     * @param \Drupal\Core\Database\Transaction\StackItem $item
     *   The stack item.
     */
    protected function addStackItem(string $id, StackItem $item) : void {
        $this->stack[$id] = $item;
    }
    
    /**
     * Removes an item from the transaction stack.
     *
     * Drivers should not override this method unless they also override the
     * $stack property.
     *
     * @param string $id
     *   The id of the transaction.
     */
    protected function removeStackItem(string $id) : void {
        unset($this->stack[$id]);
    }
    
    /**
     * Voids an item from the transaction stack.
     *
     * Drivers should not override this method unless they also override the
     * $stack property.
     *
     * @param string $id
     *   The id of the transaction.
     */
    protected function voidStackItem(string $id) : void {
        // The item should be removed from $stack and added to $voidedItems for
        // later processing.
        $this->voidedItems[$id] = $this->stack[$id];
        $this->removeStackItem($id);
    }
    
    /**
     * Produces a string representation of the stack items.
     *
     * A helper method for exception messages.
     *
     * Drivers should not override this method unless they also override the
     * $stack property.
     *
     * @return string
     *   The string representation of the stack items.
     */
    protected function dumpStackItemsAsString() : string {
        if ($this->stack() === []) {
            return '*** empty ***';
        }
        $temp = [];
        foreach ($this->stack() as $id => $item) {
            $temp[] = $id . '\\' . $item->name;
        }
        return implode(' > ', $temp);
    }
    
    /**
     * {@inheritdoc}
     */
    public function inTransaction() : bool {
        return (bool) $this->stackDepth() && $this->getConnectionTransactionState() === ClientConnectionTransactionState::Active;
    }
    
    /**
     * {@inheritdoc}
     */
    public function push(string $name = '') : Transaction {
        if (!$this->inTransaction()) {
            // If there is no transaction active, name the transaction
            // 'drupal_transaction'.
            $name = 'drupal_transaction';
        }
        elseif (!$name) {
            // Within transactions, savepoints are used. Each savepoint requires a
            // name. So if no name is present we need to create one.
            $name = 'savepoint_' . $this->stackDepth();
        }
        if ($this->has($name)) {
            throw new TransactionNameNonUniqueException("A transaction named {$name} is already in use. Active stack: " . $this->dumpStackItemsAsString());
        }
        // Define a unique ID for the transaction.
        $id = uniqid('', TRUE);
        // Do the client-level processing.
        if ($this->stackDepth() === 0) {
            $this->beginClientTransaction();
            $type = StackItemType::Root;
            $this->setConnectionTransactionState(ClientConnectionTransactionState::Active);
            // Only set ::rootId if there's not one set already, which may happen in
            // case of broken transactions.
            if ($this->rootId === NULL) {
                $this->rootId = $id;
            }
        }
        else {
            // If we're already in a Drupal transaction then we want to create a
            // database savepoint, rather than try to begin another database
            // transaction.
            $this->addClientSavepoint($name);
            $type = StackItemType::Savepoint;
        }
        // Add an item on the stack, increasing its depth.
        $this->addStackItem($id, new StackItem($name, $type));
        // Actually return a new Transaction object.
        return new Transaction($this->connection, $name, $id);
    }
    
    /**
     * {@inheritdoc}
     */
    public function unpile(string $name, string $id) : void {
        // If this is a 'root' transaction, and it is voided (that is, no longer in
        // the stack), then the transaction on the database is no longer active. An
        // action such as a rollback, or a DDL statement, was executed that
        // terminated the database transaction. So, we can process the post
        // transaction callbacks.
        if (!isset($this->stack()[$id]) && isset($this->voidedItems[$id]) && $this->rootId === $id) {
            $this->processPostTransactionCallbacks();
            $this->rootId = NULL;
            unset($this->voidedItems[$id]);
            return;
        }
        // If the $id does not correspond to the one in the stack for that $name,
        // we are facing an orphaned Transaction object (for example in case of a
        // DDL statement breaking an active transaction). That should be listed in
        // $voidedItems, so we can remove it from there.
        if (!isset($this->stack()[$id]) || $this->stack()[$id]->name !== $name) {
            unset($this->voidedItems[$id]);
            return;
        }
        // If we are not releasing the last savepoint but an earlier one, or
        // committing a root transaction while savepoints are active, all
        // subsequent savepoints will be released as well. The stack must be
        // diminished accordingly.
        while (($i = array_key_last($this->stack())) != $id) {
            $this->voidStackItem((string) $i);
        }
        if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Active) {
            if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) {
                // Release the client transaction savepoint in case the Drupal
                // transaction is not a root one.
                $this->releaseClientSavepoint($name);
            }
            elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) {
                // If this was the root Drupal transaction, we can commit the client
                // transaction.
                $this->processRootCommit();
                if ($this->rootId === $id) {
                    $this->processPostTransactionCallbacks();
                    $this->rootId = NULL;
                }
            }
            else {
                // The stack got corrupted.
                throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
            }
            // Remove the transaction from the stack.
            $this->removeStackItem($id);
            return;
        }
        // The stack got corrupted.
        throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
    }
    
    /**
     * {@inheritdoc}
     */
    public function rollback(string $name, string $id) : void {
        // Rolled back item should match the last one in stack.
        if ($id != array_key_last($this->stack()) || $name !== $this->stack()[$id]->name) {
            throw new TransactionOutOfOrderException("Error attempting rollback of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString());
        }
        if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Active) {
            if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) {
                // Rollback the client transaction to the savepoint when the Drupal
                // transaction is not a root one. Then, release the savepoint too. The
                // client connection remains active.
                $this->rollbackClientSavepoint($name);
                $this->releaseClientSavepoint($name);
                // The Transaction object remains open, and when it will get destructed
                // no commit should happen. Void the stack item.
                $this->voidStackItem($id);
            }
            elseif ($this->stackDepth() === 1 && $this->stack()[$id]->type === StackItemType::Root) {
                // If this was the root Drupal transaction, we can rollback the client
                // transaction. The transaction is closed.
                $this->processRootRollback();
                if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::RolledBack) {
                    // The Transaction object remains open, and when it will get destructed
                    // no commit should happen. Void the stack item.
                    $this->voidStackItem($id);
                }
            }
            else {
                // The stack got corrupted.
                throw new TransactionOutOfOrderException("Error attempting rollback of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString());
            }
            return;
        }
        // The stack got corrupted.
        throw new TransactionOutOfOrderException("Error attempting rollback of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString());
    }
    
    /**
     * {@inheritdoc}
     */
    public function addPostTransactionCallback(callable $callback) : void {
        if (!$this->inTransaction()) {
            throw new \LogicException('Root transaction end callbacks can only be added when there is an active transaction.');
        }
        $this->postTransactionCallbacks[] = $callback;
    }
    
    /**
     * {@inheritdoc}
     */
    public function has(string $name) : bool {
        foreach ($this->stack() as $item) {
            if ($item->name === $name) {
                return TRUE;
            }
        }
        return FALSE;
    }
    
    /**
     * Sets the state of the client connection transaction.
     *
     * Note that this is a proxy of the actual state on the database server,
     * best determined through calls to methods in this class. The actual
     * state on the database server could be different.
     *
     * Drivers should not override this method unless they also override the
     * $connectionTransactionState property.
     *
     * @param \Drupal\Core\Database\Transaction\ClientConnectionTransactionState $state
     *   The state of the client connection.
     */
    protected function setConnectionTransactionState(ClientConnectionTransactionState $state) : void {
        $this->connectionTransactionState = $state;
    }
    
    /**
     * Gets the state of the client connection transaction.
     *
     * Note that this is a proxy of the actual state on the database server,
     * best determined through calls to methods in this class. The actual
     * state on the database server could be different.
     *
     * Drivers should not override this method unless they also override the
     * $connectionTransactionState property.
     *
     * @return \Drupal\Core\Database\Transaction\ClientConnectionTransactionState
     *   The state of the client connection.
     */
    protected function getConnectionTransactionState() : ClientConnectionTransactionState {
        return $this->connectionTransactionState;
    }
    
    /**
     * Processes the root transaction rollback.
     */
    protected function processRootRollback() : void {
        $this->rollbackClientTransaction();
    }
    
    /**
     * Processes the root transaction commit.
     *
     * @throws \Drupal\Core\Database\TransactionCommitFailedException
     *   If the commit of the root transaction failed.
     */
    protected function processRootCommit() : void {
        $clientCommit = $this->commitClientTransaction();
        if (!$clientCommit) {
            throw new TransactionCommitFailedException();
        }
    }
    
    /**
     * Processes the post-transaction callbacks.
     */
    protected function processPostTransactionCallbacks() : void {
        if (!empty($this->postTransactionCallbacks)) {
            $callbacks = $this->postTransactionCallbacks;
            $this->postTransactionCallbacks = [];
            foreach ($callbacks as $callback) {
                call_user_func($callback, $this->getConnectionTransactionState() === ClientConnectionTransactionState::Committed || $this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided);
            }
        }
    }
    
    /**
     * Begins a transaction on the client connection.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure.
     */
    protected abstract function beginClientTransaction() : bool;
    
    /**
     * Adds a savepoint on the client transaction.
     *
     * This is a generic implementation. Drivers should override this method
     * to use a method specific for their client connection.
     *
     * @param string $name
     *   The name of the savepoint.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure.
     */
    protected function addClientSavepoint(string $name) : bool {
        $this->connection
            ->query('SAVEPOINT ' . $name);
        return TRUE;
    }
    
    /**
     * Rolls back to a savepoint on the client transaction.
     *
     * This is a generic implementation. Drivers should override this method
     * to use a method specific for their client connection.
     *
     * @param string $name
     *   The name of the savepoint.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure.
     */
    protected function rollbackClientSavepoint(string $name) : bool {
        $this->connection
            ->query('ROLLBACK TO SAVEPOINT ' . $name);
        return TRUE;
    }
    
    /**
     * Releases a savepoint on the client transaction.
     *
     * This is a generic implementation. Drivers should override this method
     * to use a method specific for their client connection.
     *
     * @param string $name
     *   The name of the savepoint.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure.
     */
    protected function releaseClientSavepoint(string $name) : bool {
        $this->connection
            ->query('RELEASE SAVEPOINT ' . $name);
        return TRUE;
    }
    
    /**
     * Rolls back a client transaction.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure.
     */
    protected abstract function rollbackClientTransaction() : bool;
    
    /**
     * Commits a client transaction.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure.
     */
    protected abstract function commitClientTransaction() : bool;
    
    /**
     * {@inheritdoc}
     */
    public function voidClientTransaction() : void {
        while ($i = array_key_last($this->stack())) {
            $this->voidStackItem((string) $i);
        }
        $this->setConnectionTransactionState(ClientConnectionTransactionState::Voided);
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
TransactionManagerBase::$connectionTransactionState private property The state of the underlying client connection transaction.
TransactionManagerBase::$postTransactionCallbacks private property A list of post-transaction callbacks.
TransactionManagerBase::$rootId private property The ID of the root Transaction object.
TransactionManagerBase::$stack private property The stack of Drupal transactions currently active.
TransactionManagerBase::$voidedItems private property A list of voided stack items.
TransactionManagerBase::addClientSavepoint protected function Adds a savepoint on the client transaction.
TransactionManagerBase::addPostTransactionCallback public function Adds a root transaction end callback. Overrides TransactionManagerInterface::addPostTransactionCallback
TransactionManagerBase::addStackItem protected function Adds an item to the transaction stack.
TransactionManagerBase::beginClientTransaction abstract protected function Begins a transaction on the client connection. 3
TransactionManagerBase::commitAll public function Commits the entire transaction stack.
TransactionManagerBase::commitClientTransaction abstract protected function Commits a client transaction. 3
TransactionManagerBase::dumpStackItemsAsString protected function Produces a string representation of the stack items.
TransactionManagerBase::getConnectionTransactionState protected function Gets the state of the client connection transaction.
TransactionManagerBase::has public function Checks if a named Drupal transaction is active. Overrides TransactionManagerInterface::has
TransactionManagerBase::inTransaction public function Determines if there is an active transaction open. Overrides TransactionManagerInterface::inTransaction
TransactionManagerBase::processPostTransactionCallbacks protected function Processes the post-transaction callbacks.
TransactionManagerBase::processRootCommit protected function Processes the root transaction commit. 1
TransactionManagerBase::processRootRollback protected function Processes the root transaction rollback.
TransactionManagerBase::push public function Pushes a new Drupal transaction on the stack. Overrides TransactionManagerInterface::push
TransactionManagerBase::releaseClientSavepoint protected function Releases a savepoint on the client transaction. 1
TransactionManagerBase::removeStackItem protected function Removes an item from the transaction stack.
TransactionManagerBase::rollback public function Rolls back a Drupal transaction. Overrides TransactionManagerInterface::rollback
TransactionManagerBase::rollbackClientSavepoint protected function Rolls back to a savepoint on the client transaction. 1
TransactionManagerBase::rollbackClientTransaction abstract protected function Rolls back a client transaction. 3
TransactionManagerBase::setConnectionTransactionState protected function Sets the state of the client connection transaction.
TransactionManagerBase::stack protected function Returns the content of the transaction stack.
TransactionManagerBase::stackDepth public function Returns the current depth of the transaction stack.
TransactionManagerBase::unpile public function Removes a Drupal transaction from the stack. Overrides TransactionManagerInterface::unpile
TransactionManagerBase::voidClientTransaction public function Voids the client connection. Overrides TransactionManagerInterface::voidClientTransaction
TransactionManagerBase::voidStackItem protected function Voids an item from the transaction stack.
TransactionManagerBase::__construct public function Constructor.
TransactionManagerBase::__destruct public function Destructor.
RSS feed
Powered by Drupal