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

Breadcrumb

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

class DoctrineDbalStore

DbalStore is a PersistingStoreInterface implementation using a Doctrine DBAL connection.

Lock metadata are stored in a table. You can use createTable() to initialize a correctly defined table.

CAUTION: This store relies on all client and server nodes to have synchronized clocks for lock expiry to occur at the correct time. To ensure locks don't expire prematurely; the TTLs should be set with enough extra time to account for any clock drift between nodes.

@author Jérémy Derussé <jeremy@derusse.com>

Hierarchy

  • class \Symfony\Component\Lock\Store\DoctrineDbalStore implements \Symfony\Component\Lock\PersistingStoreInterface uses \Symfony\Component\Lock\Store\DatabaseTableTrait, \Symfony\Component\Lock\Store\ExpiringStoreTrait

Expanded class hierarchy of DoctrineDbalStore

File

vendor/symfony/lock/Store/DoctrineDbalStore.php, line 42

Namespace

Symfony\Component\Lock\Store
View source
class DoctrineDbalStore implements PersistingStoreInterface {
    use DatabaseTableTrait;
    use ExpiringStoreTrait;
    private Connection $conn;
    
    /**
     * List of available options:
     *  * db_table: The name of the table [default: lock_keys]
     *  * db_id_col: The column where to store the lock key [default: key_id]
     *  * db_token_col: The column where to store the lock token [default: key_token]
     *  * db_expiration_col: The column where to store the expiration [default: key_expiration].
     *
     * @param Connection|string $connOrUrl     A DBAL Connection instance or Doctrine URL
     * @param array             $options       An associative array of options
     * @param float             $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
     * @param int               $initialTtl    The expiration delay of locks in seconds
     *
     * @throws InvalidArgumentException When namespace contains invalid characters
     * @throws InvalidArgumentException When the initial ttl is not valid
     */
    public function __construct(Connection|string $connOrUrl, array $options = [], float $gcProbability = 0.01, int $initialTtl = 300) {
        $this->init($options, $gcProbability, $initialTtl);
        if ($connOrUrl instanceof Connection) {
            $this->conn = $connOrUrl;
        }
        else {
            if (!class_exists(DriverManager::class)) {
                throw new InvalidArgumentException('Failed to parse the DSN. Try running "composer require doctrine/dbal".');
            }
            $params = (new DsnParser([
                'db2' => 'ibm_db2',
                'mssql' => 'pdo_sqlsrv',
                'mysql' => 'pdo_mysql',
                'mysql2' => 'pdo_mysql',
                'postgres' => 'pdo_pgsql',
                'postgresql' => 'pdo_pgsql',
                'pgsql' => 'pdo_pgsql',
                'sqlite' => 'pdo_sqlite',
                'sqlite3' => 'pdo_sqlite',
            ]))->parse($connOrUrl);
            $config = new Configuration();
            $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
            $this->conn = DriverManager::getConnection($params, $config);
        }
    }
    public function save(Key $key) : void {
        $key->reduceLifetime($this->initialTtl);
        $sql = "INSERT INTO {$this->table} ({$this->idCol}, {$this->tokenCol}, {$this->expirationCol}) VALUES (?, ?, {$this->getCurrentTimestampStatement()} + {$this->initialTtl})";
        try {
            $this->conn
                ->executeStatement($sql, [
                $this->getHashedKey($key),
                $this->getUniqueToken($key),
            ], [
                ParameterType::STRING,
                ParameterType::STRING,
            ]);
        } catch (TableNotFoundException) {
            if (!$this->conn
                ->isTransactionActive() || $this->platformSupportsTableCreationInTransaction()) {
                $this->createTable();
            }
            try {
                $this->conn
                    ->executeStatement($sql, [
                    $this->getHashedKey($key),
                    $this->getUniqueToken($key),
                ], [
                    ParameterType::STRING,
                    ParameterType::STRING,
                ]);
            } catch (DBALException) {
                $this->putOffExpiration($key, $this->initialTtl);
            }
        } catch (DBALException) {
            // the lock is already acquired. It could be us. Let's try to put off.
            $this->putOffExpiration($key, $this->initialTtl);
        }
        $this->randomlyPrune();
        $this->checkNotExpired($key);
    }
    public function putOffExpiration(Key $key, $ttl) : void {
        if ($ttl < 1) {
            throw new InvalidTtlException(\sprintf('"%s()" expects a TTL greater or equals to 1 second. Got "%s".', __METHOD__, $ttl));
        }
        $key->reduceLifetime($ttl);
        $sql = "UPDATE {$this->table} SET {$this->expirationCol} = {$this->getCurrentTimestampStatement()} + ?, {$this->tokenCol} = ? WHERE {$this->idCol} = ? AND ({$this->tokenCol} = ? OR {$this->expirationCol} <= {$this->getCurrentTimestampStatement()})";
        $uniqueToken = $this->getUniqueToken($key);
        $result = $this->conn
            ->executeQuery($sql, [
            $ttl,
            $uniqueToken,
            $this->getHashedKey($key),
            $uniqueToken,
        ], [
            ParameterType::INTEGER,
            ParameterType::STRING,
            ParameterType::STRING,
            ParameterType::STRING,
        ]);
        // If this method is called twice in the same second, the row wouldn't be updated. We have to call exists to know if we are the owner
        if (!$result->rowCount() && !$this->exists($key)) {
            throw new LockConflictedException();
        }
        $this->checkNotExpired($key);
    }
    public function delete(Key $key) : void {
        $this->conn
            ->delete($this->table, [
            $this->idCol => $this->getHashedKey($key),
            $this->tokenCol => $this->getUniqueToken($key),
        ]);
    }
    public function exists(Key $key) : bool {
        $sql = "SELECT 1 FROM {$this->table} WHERE {$this->idCol} = ? AND {$this->tokenCol} = ? AND {$this->expirationCol} > {$this->getCurrentTimestampStatement()}";
        $result = $this->conn
            ->fetchOne($sql, [
            $this->getHashedKey($key),
            $this->getUniqueToken($key),
        ], [
            ParameterType::STRING,
            ParameterType::STRING,
        ]);
        return (bool) $result;
    }
    
    /**
     * Creates the table to store lock keys which can be called once for setup.
     *
     * @throws DBALException When the table already exists
     */
    public function createTable() : void {
        $schema = new Schema();
        $this->configureSchema($schema, static fn() => true);
        foreach ($schema->toSql($this->conn
            ->getDatabasePlatform()) as $sql) {
            $this->conn
                ->executeStatement($sql);
        }
    }
    
    /**
     * Adds the Table to the Schema if it doesn't exist.
     */
    public function configureSchema(Schema $schema, \Closure $isSameDatabase) : void {
        if ($schema->hasTable($this->table)) {
            return;
        }
        if (!$isSameDatabase($this->conn
            ->executeStatement(...))) {
            return;
        }
        $table = $schema->createTable($this->table);
        $table->addColumn($this->idCol, 'string', [
            'length' => 64,
        ]);
        $table->addColumn($this->tokenCol, 'string', [
            'length' => 44,
        ]);
        $table->addColumn($this->expirationCol, 'integer', [
            'unsigned' => true,
        ]);
        $table->setPrimaryKey([
            $this->idCol,
        ]);
    }
    
    /**
     * Cleans up the table by removing all expired locks.
     */
    private function prune() : void {
        $sql = "DELETE FROM {$this->table} WHERE {$this->expirationCol} <= {$this->getCurrentTimestampStatement()}";
        $this->conn
            ->executeStatement($sql);
    }
    
    /**
     * Provides an SQL function to get the current timestamp regarding the current connection's driver.
     */
    private function getCurrentTimestampStatement() : string {
        $platform = $this->conn
            ->getDatabasePlatform();
        return match (true) {    $platform instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform => 'UNIX_TIMESTAMP()',
            $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => 'strftime(\'%s\',\'now\')',
            $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform => 'CAST(EXTRACT(epoch FROM NOW()) AS INT)',
            $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform => '(SYSDATE - TO_DATE(\'19700101\',\'yyyymmdd\'))*86400 - TO_NUMBER(SUBSTR(TZ_OFFSET(sessiontimezone), 1, 3))*3600',
            $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform => 'DATEDIFF(s, \'1970-01-01\', GETUTCDATE())',
            default => (string) time(),
        
        };
    }
    
    /**
     * Checks whether current platform supports table creation within transaction.
     */
    private function platformSupportsTableCreationInTransaction() : bool {
        $platform = $this->conn
            ->getDatabasePlatform();
        return match (true) {    $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform, $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform, $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform => true,
            default => false,
        
        };
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
DatabaseTableTrait::$expirationCol private property
DatabaseTableTrait::$gcProbability private property
DatabaseTableTrait::$idCol private property
DatabaseTableTrait::$initialTtl private property
DatabaseTableTrait::$table private property
DatabaseTableTrait::$tokenCol private property
DatabaseTableTrait::getHashedKey private function Returns a hashed version of the key.
DatabaseTableTrait::getUniqueToken private function
DatabaseTableTrait::init private function
DatabaseTableTrait::randomlyPrune private function Prune the table randomly, based on GC probability.
DoctrineDbalStore::$conn private property
DoctrineDbalStore::configureSchema public function Adds the Table to the Schema if it doesn&#039;t exist.
DoctrineDbalStore::createTable public function Creates the table to store lock keys which can be called once for setup.
DoctrineDbalStore::delete public function Removes a resource from the storage. Overrides PersistingStoreInterface::delete
DoctrineDbalStore::exists public function Returns whether or not the resource exists in the storage. Overrides PersistingStoreInterface::exists
DoctrineDbalStore::getCurrentTimestampStatement private function Provides an SQL function to get the current timestamp regarding the current connection&#039;s driver.
DoctrineDbalStore::platformSupportsTableCreationInTransaction private function Checks whether current platform supports table creation within transaction.
DoctrineDbalStore::prune private function Cleans up the table by removing all expired locks.
DoctrineDbalStore::putOffExpiration public function Extends the TTL of a resource. Overrides PersistingStoreInterface::putOffExpiration
DoctrineDbalStore::save public function Stores the resource if it&#039;s not locked by someone else. Overrides PersistingStoreInterface::save
DoctrineDbalStore::__construct public function List of available options:
ExpiringStoreTrait::checkNotExpired private function

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal