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\StoreView 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'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'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's not locked by someone else. | Overrides PersistingStoreInterface::save |
DoctrineDbalStore::__construct | public | function | List of available options: | |
ExpiringStoreTrait::checkNotExpired | private | function |