vendor/symfony/lock/Store/DoctrineDbalStore.php line 116

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Lock\Store;
  11. use Doctrine\DBAL\Configuration;
  12. use Doctrine\DBAL\Connection;
  13. use Doctrine\DBAL\DriverManager;
  14. use Doctrine\DBAL\Exception as DBALException;
  15. use Doctrine\DBAL\Exception\TableNotFoundException;
  16. use Doctrine\DBAL\ParameterType;
  17. use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
  18. use Doctrine\DBAL\Schema\Schema;
  19. use Doctrine\DBAL\Tools\DsnParser;
  20. use Symfony\Component\Lock\Exception\InvalidArgumentException;
  21. use Symfony\Component\Lock\Exception\InvalidTtlException;
  22. use Symfony\Component\Lock\Exception\LockConflictedException;
  23. use Symfony\Component\Lock\Key;
  24. use Symfony\Component\Lock\PersistingStoreInterface;
  25. /**
  26.  * DbalStore is a PersistingStoreInterface implementation using a Doctrine DBAL connection.
  27.  *
  28.  * Lock metadata are stored in a table. You can use createTable() to initialize
  29.  * a correctly defined table.
  30.  * CAUTION: This store relies on all client and server nodes to have
  31.  * synchronized clocks for lock expiry to occur at the correct time.
  32.  * To ensure locks don't expire prematurely; the TTLs should be set with enough
  33.  * extra time to account for any clock drift between nodes.
  34.  *
  35.  * @author Jérémy Derussé <jeremy@derusse.com>
  36.  */
  37. class DoctrineDbalStore implements PersistingStoreInterface
  38. {
  39.     use DatabaseTableTrait;
  40.     use ExpiringStoreTrait;
  41.     private $conn;
  42.     /**
  43.      * List of available options:
  44.      *  * db_table: The name of the table [default: lock_keys]
  45.      *  * db_id_col: The column where to store the lock key [default: key_id]
  46.      *  * db_token_col: The column where to store the lock token [default: key_token]
  47.      *  * db_expiration_col: The column where to store the expiration [default: key_expiration].
  48.      *
  49.      * @param Connection|string $connOrUrl     A DBAL Connection instance or Doctrine URL
  50.      * @param array             $options       An associative array of options
  51.      * @param float             $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
  52.      * @param int               $initialTtl    The expiration delay of locks in seconds
  53.      *
  54.      * @throws InvalidArgumentException When namespace contains invalid characters
  55.      * @throws InvalidArgumentException When the initial ttl is not valid
  56.      */
  57.     public function __construct($connOrUrl, array $options = [], float $gcProbability 0.01int $initialTtl 300)
  58.     {
  59.         $this->init($options$gcProbability$initialTtl);
  60.         if ($connOrUrl instanceof Connection) {
  61.             $this->conn $connOrUrl;
  62.         } elseif (\is_string($connOrUrl)) {
  63.             if (!class_exists(DriverManager::class)) {
  64.                 throw new InvalidArgumentException('Failed to parse the DSN. Try running "composer require doctrine/dbal".');
  65.             }
  66.             if (class_exists(DsnParser::class)) {
  67.                 $params = (new DsnParser([
  68.                     'db2' => 'ibm_db2',
  69.                     'mssql' => 'pdo_sqlsrv',
  70.                     'mysql' => 'pdo_mysql',
  71.                     'mysql2' => 'pdo_mysql',
  72.                     'postgres' => 'pdo_pgsql',
  73.                     'postgresql' => 'pdo_pgsql',
  74.                     'pgsql' => 'pdo_pgsql',
  75.                     'sqlite' => 'pdo_sqlite',
  76.                     'sqlite3' => 'pdo_sqlite',
  77.                 ]))->parse($connOrUrl);
  78.             } else {
  79.                 $params = ['url' => $connOrUrl];
  80.             }
  81.             $config = new Configuration();
  82.             if (class_exists(DefaultSchemaManagerFactory::class)) {
  83.                 $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
  84.             }
  85.             $this->conn DriverManager::getConnection($params$config);
  86.         } else {
  87.             throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be "%s" or string, "%s" given.'Connection::class, __METHOD__get_debug_type($connOrUrl)));
  88.         }
  89.     }
  90.     /**
  91.      * {@inheritdoc}
  92.      */
  93.     public function save(Key $key)
  94.     {
  95.         $key->reduceLifetime($this->initialTtl);
  96.         $sql "INSERT INTO $this->table ($this->idCol$this->tokenCol$this->expirationCol) VALUES (?, ?, {$this->getCurrentTimestampStatement()} + $this->initialTtl)";
  97.         try {
  98.             $this->conn->executeStatement($sql, [
  99.                 $this->getHashedKey($key),
  100.                 $this->getUniqueToken($key),
  101.             ], [
  102.                 ParameterType::STRING,
  103.                 ParameterType::STRING,
  104.             ]);
  105.         } catch (TableNotFoundException $e) {
  106.             if (!$this->conn->isTransactionActive() || $this->platformSupportsTableCreationInTransaction()) {
  107.                 $this->createTable();
  108.             }
  109.             try {
  110.                 $this->conn->executeStatement($sql, [
  111.                     $this->getHashedKey($key),
  112.                     $this->getUniqueToken($key),
  113.                 ], [
  114.                     ParameterType::STRING,
  115.                     ParameterType::STRING,
  116.                 ]);
  117.             } catch (DBALException $e) {
  118.                 $this->putOffExpiration($key$this->initialTtl);
  119.             }
  120.         } catch (DBALException $e) {
  121.             // the lock is already acquired. It could be us. Let's try to put off.
  122.             $this->putOffExpiration($key$this->initialTtl);
  123.         }
  124.         $this->randomlyPrune();
  125.         $this->checkNotExpired($key);
  126.     }
  127.     /**
  128.      * {@inheritdoc}
  129.      */
  130.     public function putOffExpiration(Key $key$ttl)
  131.     {
  132.         if ($ttl 1) {
  133.             throw new InvalidTtlException(sprintf('"%s()" expects a TTL greater or equals to 1 second. Got "%s".'__METHOD__$ttl));
  134.         }
  135.         $key->reduceLifetime($ttl);
  136.         $sql "UPDATE $this->table SET $this->expirationCol = {$this->getCurrentTimestampStatement()} + ?, $this->tokenCol = ? WHERE $this->idCol = ? AND ($this->tokenCol = ? OR $this->expirationCol <= {$this->getCurrentTimestampStatement()})";
  137.         $uniqueToken $this->getUniqueToken($key);
  138.         $result $this->conn->executeQuery($sql, [
  139.             $ttl,
  140.             $uniqueToken,
  141.             $this->getHashedKey($key),
  142.             $uniqueToken,
  143.         ], [
  144.             ParameterType::INTEGER,
  145.             ParameterType::STRING,
  146.             ParameterType::STRING,
  147.             ParameterType::STRING,
  148.         ]);
  149.         // 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
  150.         if (!$result->rowCount() && !$this->exists($key)) {
  151.             throw new LockConflictedException();
  152.         }
  153.         $this->checkNotExpired($key);
  154.     }
  155.     /**
  156.      * {@inheritdoc}
  157.      */
  158.     public function delete(Key $key)
  159.     {
  160.         $this->conn->delete($this->table, [
  161.             $this->idCol => $this->getHashedKey($key),
  162.             $this->tokenCol => $this->getUniqueToken($key),
  163.         ]);
  164.     }
  165.     /**
  166.      * {@inheritdoc}
  167.      */
  168.     public function exists(Key $key)
  169.     {
  170.         $sql "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND $this->tokenCol = ? AND $this->expirationCol > {$this->getCurrentTimestampStatement()}";
  171.         $result $this->conn->fetchOne($sql, [
  172.             $this->getHashedKey($key),
  173.             $this->getUniqueToken($key),
  174.         ], [
  175.             ParameterType::STRING,
  176.             ParameterType::STRING,
  177.         ]);
  178.         return (bool) $result;
  179.     }
  180.     /**
  181.      * Creates the table to store lock keys which can be called once for setup.
  182.      *
  183.      * @throws DBALException When the table already exists
  184.      */
  185.     public function createTable(): void
  186.     {
  187.         $schema = new Schema();
  188.         $this->configureSchema($schema);
  189.         foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
  190.             $this->conn->executeStatement($sql);
  191.         }
  192.     }
  193.     /**
  194.      * Adds the Table to the Schema if it doesn't exist.
  195.      */
  196.     public function configureSchema(Schema $schema): void
  197.     {
  198.         if ($schema->hasTable($this->table)) {
  199.             return;
  200.         }
  201.         $table $schema->createTable($this->table);
  202.         $table->addColumn($this->idCol'string', ['length' => 64]);
  203.         $table->addColumn($this->tokenCol'string', ['length' => 44]);
  204.         $table->addColumn($this->expirationCol'integer', ['unsigned' => true]);
  205.         $table->setPrimaryKey([$this->idCol]);
  206.     }
  207.     /**
  208.      * Cleans up the table by removing all expired locks.
  209.      */
  210.     private function prune(): void
  211.     {
  212.         $sql "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
  213.         $this->conn->executeStatement($sql);
  214.     }
  215.     /**
  216.      * Provides an SQL function to get the current timestamp regarding the current connection's driver.
  217.      */
  218.     private function getCurrentTimestampStatement(): string
  219.     {
  220.         $platform $this->conn->getDatabasePlatform();
  221.         switch (true) {
  222.             case $platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform:
  223.             case $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform:
  224.                 return 'UNIX_TIMESTAMP()';
  225.             case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform:
  226.                 return 'strftime(\'%s\',\'now\')';
  227.             case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform:
  228.             case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform:
  229.                 return 'CAST(EXTRACT(epoch FROM NOW()) AS INT)';
  230.             case $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform:
  231.                 return '(SYSDATE - TO_DATE(\'19700101\',\'yyyymmdd\'))*86400 - TO_NUMBER(SUBSTR(TZ_OFFSET(sessiontimezone), 1, 3))*3600';
  232.             case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform:
  233.             case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform:
  234.                 return 'DATEDIFF(s, \'1970-01-01\', GETUTCDATE())';
  235.             default:
  236.                 return (string) time();
  237.         }
  238.     }
  239.     /**
  240.      * Checks whether current platform supports table creation within transaction.
  241.      */
  242.     private function platformSupportsTableCreationInTransaction(): bool
  243.     {
  244.         $platform $this->conn->getDatabasePlatform();
  245.         switch (true) {
  246.             case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform:
  247.             case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform:
  248.             case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform:
  249.             case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform:
  250.             case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform:
  251.                 return true;
  252.             default:
  253.                 return false;
  254.         }
  255.     }
  256. }