*/ 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(sprintf('Failed to parse the DSN "%s". Try running "composer require doctrine/dbal".', $connOrUrl)); } $this->conn = DriverManager::getConnection(['url' => $connOrUrl]); } } /** * {@inheritdoc} */ public function save(Key $key) { $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); } /** * {@inheritdoc} */ public function putOffExpiration(Key $key, $ttl) { 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); } /** * {@inheritdoc} */ public function delete(Key $key) { $this->conn->delete($this->table, [ $this->idCol => $this->getHashedKey($key), $this->tokenCol => $this->getUniqueToken($key), ]); } /** * {@inheritdoc} */ 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); 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): void { if ($schema->hasTable($this->table)) { 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\MySQLPlatform, $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform => 'UNIX_TIMESTAMP()', $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => 'strftime(\'%s\',\'now\')', $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform, $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform => '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, $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform => '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\PostgreSQL94Platform, $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform, $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform, $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform => true, default => false, }; } } __halt_compiler();----SIGNATURE:----avFLUeDsFhJp1fV3Q7aCtPWdweJLBZGMpZ2vfO+aFTCQuWv158eThCtwiQExUZwTeWGKSfGc9chqH+VaqZtnfA1YJrUkfEOoWyEmzGcJjDTFTJ/epk+jcsiWw9nbEPgOoYWXEYjJLXPTEdKk/0OlLMxCgDkhxac44ZJe1j4qTBi3T7z1NNrmWjM/DBUGi3aj2APvZyqfEj31vebNigPeotcUkh0FF916f5pGxRgAA8Y9oQ3cDKHeTkZhalZSNcQ2DGj2yACE9QbnEzKHaPxsnKJPAjGWK6Hd6xL1ftWoh2cnfwnUZRgWo0U5aAlKOxBDGtjYGPl1kH+aNDCsQcN22dxvqcBdP9yWD7XRN246OkusnjNpPBDHFlPXISC+DZmhP63P/y0CkI4RD2S/A7S5p8Z2uotx0ZldMF2ipfFvU1OPM20rMY2LHun6O+cQKNO5UwpkqNDpGWyF1iAueQt1iguMrhzHxbwz1m+a1ovTufLU2+kD49TAs88zD7QBM6M0HwKOARHYWOmBZzseMKOeDbvUDTMOynuDdzVdqNsP/xVMbC5qAw8kvJkpfXkXHMcNcPxZgNwqcTXNhFPlt1YS9kuSjkJzceMZSSLxmUqCSLLvgbZo2UIoBaba36ZpPIsBoQUNN0ula4KYAdtO32ycRj4mkliuEOY+qmp5IdK+VUQ=----ATTACHMENT:----NzQ3MTYwOTAwNDI5MjQzOCAzODQxODUwNjA3NzcxMTUwIDQ1ODI2Nzg5MzM1MjE5MTU=