vendor/symfony/lock/Store/PdoStore.php line 75

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\Connection;
  12. use Doctrine\DBAL\Schema\Schema;
  13. use Symfony\Component\Lock\Exception\InvalidArgumentException;
  14. use Symfony\Component\Lock\Exception\InvalidTtlException;
  15. use Symfony\Component\Lock\Exception\LockConflictedException;
  16. use Symfony\Component\Lock\Key;
  17. use Symfony\Component\Lock\PersistingStoreInterface;
  18. /**
  19.  * PdoStore is a PersistingStoreInterface implementation using a PDO connection.
  20.  *
  21.  * Lock metadata are stored in a table. You can use createTable() to initialize
  22.  * a correctly defined table.
  23.  * CAUTION: This store relies on all client and server nodes to have
  24.  * synchronized clocks for lock expiry to occur at the correct time.
  25.  * To ensure locks don't expire prematurely; the TTLs should be set with enough
  26.  * extra time to account for any clock drift between nodes.
  27.  *
  28.  * @author Jérémy Derussé <jeremy@derusse.com>
  29.  */
  30. class PdoStore implements PersistingStoreInterface
  31. {
  32.     use DatabaseTableTrait;
  33.     use ExpiringStoreTrait;
  34.     private $conn;
  35.     private $dsn;
  36.     private $driver;
  37.     private $username '';
  38.     private $password '';
  39.     private $connectionOptions = [];
  40.     private $dbalStore;
  41.     /**
  42.      * You can either pass an existing database connection as PDO instance
  43.      * or a DSN string that will be used to lazy-connect to the database
  44.      * when the lock is actually used.
  45.      *
  46.      * List of available options:
  47.      *  * db_table: The name of the table [default: lock_keys]
  48.      *  * db_id_col: The column where to store the lock key [default: key_id]
  49.      *  * db_token_col: The column where to store the lock token [default: key_token]
  50.      *  * db_expiration_col: The column where to store the expiration [default: key_expiration]
  51.      *  * db_username: The username when lazy-connect [default: '']
  52.      *  * db_password: The password when lazy-connect [default: '']
  53.      *  * db_connection_options: An array of driver-specific connection options [default: []]
  54.      *
  55.      * @param \PDO|string $connOrDsn     A \PDO instance or DSN string or null
  56.      * @param array       $options       An associative array of options
  57.      * @param float       $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
  58.      * @param int         $initialTtl    The expiration delay of locks in seconds
  59.      *
  60.      * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
  61.      * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
  62.      * @throws InvalidArgumentException When the initial ttl is not valid
  63.      */
  64.     public function __construct($connOrDsn, array $options = [], float $gcProbability 0.01int $initialTtl 300)
  65.     {
  66.         if ($connOrDsn instanceof Connection || (\is_string($connOrDsn) && str_contains($connOrDsn'://'))) {
  67.             trigger_deprecation('symfony/lock''5.4''Usage of a DBAL Connection with "%s" is deprecated and will be removed in symfony 6.0. Use "%s" instead.'__CLASS__DoctrineDbalStore::class);
  68.             $this->dbalStore = new DoctrineDbalStore($connOrDsn$options$gcProbability$initialTtl);
  69.             return;
  70.         }
  71.         $this->init($options$gcProbability$initialTtl);
  72.         if ($connOrDsn instanceof \PDO) {
  73.             if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
  74.                 throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).'__METHOD__));
  75.             }
  76.             $this->conn $connOrDsn;
  77.         } elseif (\is_string($connOrDsn)) {
  78.             $this->dsn $connOrDsn;
  79.         } else {
  80.             throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.'__CLASS__get_debug_type($connOrDsn)));
  81.         }
  82.         $this->username $options['db_username'] ?? $this->username;
  83.         $this->password $options['db_password'] ?? $this->password;
  84.         $this->connectionOptions $options['db_connection_options'] ?? $this->connectionOptions;
  85.     }
  86.     /**
  87.      * {@inheritdoc}
  88.      */
  89.     public function save(Key $key)
  90.     {
  91.         if (isset($this->dbalStore)) {
  92.             $this->dbalStore->save($key);
  93.             return;
  94.         }
  95.         $key->reduceLifetime($this->initialTtl);
  96.         $sql "INSERT INTO $this->table ($this->idCol$this->tokenCol$this->expirationCol) VALUES (:id, :token, {$this->getCurrentTimestampStatement()} + $this->initialTtl)";
  97.         $conn $this->getConnection();
  98.         try {
  99.             $stmt $conn->prepare($sql);
  100.         } catch (\PDOException $e) {
  101.             if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql''sqlite''sqlsrv'], true)) {
  102.                 $this->createTable();
  103.             }
  104.             $stmt $conn->prepare($sql);
  105.         }
  106.         $stmt->bindValue(':id'$this->getHashedKey($key));
  107.         $stmt->bindValue(':token'$this->getUniqueToken($key));
  108.         try {
  109.             $stmt->execute();
  110.         } catch (\PDOException $e) {
  111.             // the lock is already acquired. It could be us. Let's try to put off.
  112.             $this->putOffExpiration($key$this->initialTtl);
  113.         }
  114.         $this->randomlyPrune();
  115.         $this->checkNotExpired($key);
  116.     }
  117.     /**
  118.      * {@inheritdoc}
  119.      */
  120.     public function putOffExpiration(Key $keyfloat $ttl)
  121.     {
  122.         if (isset($this->dbalStore)) {
  123.             $this->dbalStore->putOffExpiration($key$ttl);
  124.             return;
  125.         }
  126.         if ($ttl 1) {
  127.             throw new InvalidTtlException(sprintf('"%s()" expects a TTL greater or equals to 1 second. Got "%s".'__METHOD__$ttl));
  128.         }
  129.         $key->reduceLifetime($ttl);
  130.         $sql "UPDATE $this->table SET $this->expirationCol = {$this->getCurrentTimestampStatement()} + $ttl$this->tokenCol = :token1 WHERE $this->idCol = :id AND ($this->tokenCol = :token2 OR $this->expirationCol <= {$this->getCurrentTimestampStatement()})";
  131.         $stmt $this->getConnection()->prepare($sql);
  132.         $uniqueToken $this->getUniqueToken($key);
  133.         $stmt->bindValue(':id'$this->getHashedKey($key));
  134.         $stmt->bindValue(':token1'$uniqueToken);
  135.         $stmt->bindValue(':token2'$uniqueToken);
  136.         $result $stmt->execute();
  137.         // 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
  138.         if (!(\is_object($result) ? $result $stmt)->rowCount() && !$this->exists($key)) {
  139.             throw new LockConflictedException();
  140.         }
  141.         $this->checkNotExpired($key);
  142.     }
  143.     /**
  144.      * {@inheritdoc}
  145.      */
  146.     public function delete(Key $key)
  147.     {
  148.         if (isset($this->dbalStore)) {
  149.             $this->dbalStore->delete($key);
  150.             return;
  151.         }
  152.         $sql "DELETE FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token";
  153.         $stmt $this->getConnection()->prepare($sql);
  154.         $stmt->bindValue(':id'$this->getHashedKey($key));
  155.         $stmt->bindValue(':token'$this->getUniqueToken($key));
  156.         $stmt->execute();
  157.     }
  158.     /**
  159.      * {@inheritdoc}
  160.      */
  161.     public function exists(Key $key)
  162.     {
  163.         if (isset($this->dbalStore)) {
  164.             return $this->dbalStore->exists($key);
  165.         }
  166.         $sql "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token AND $this->expirationCol > {$this->getCurrentTimestampStatement()}";
  167.         $stmt $this->getConnection()->prepare($sql);
  168.         $stmt->bindValue(':id'$this->getHashedKey($key));
  169.         $stmt->bindValue(':token'$this->getUniqueToken($key));
  170.         $result $stmt->execute();
  171.         return (bool) (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn());
  172.     }
  173.     private function getConnection(): \PDO
  174.     {
  175.         if (null === $this->conn) {
  176.             $this->conn = new \PDO($this->dsn$this->username$this->password$this->connectionOptions);
  177.             $this->conn->setAttribute(\PDO::ATTR_ERRMODE\PDO::ERRMODE_EXCEPTION);
  178.         }
  179.         return $this->conn;
  180.     }
  181.     /**
  182.      * Creates the table to store lock keys which can be called once for setup.
  183.      *
  184.      * @throws \PDOException    When the table already exists
  185.      * @throws \DomainException When an unsupported PDO driver is used
  186.      */
  187.     public function createTable(): void
  188.     {
  189.         if (isset($this->dbalStore)) {
  190.             $this->dbalStore->createTable();
  191.             return;
  192.         }
  193.         // connect if we are not yet
  194.         $conn $this->getConnection();
  195.         $driver $this->getDriver();
  196.         switch ($driver) {
  197.             case 'mysql':
  198.                 $sql "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(44) NOT NULL, $this->expirationCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB";
  199.                 break;
  200.             case 'sqlite':
  201.                 $sql "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->tokenCol TEXT NOT NULL, $this->expirationCol INTEGER)";
  202.                 break;
  203.             case 'pgsql':
  204.                 $sql "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
  205.                 break;
  206.             case 'oci':
  207.                 $sql "CREATE TABLE $this->table ($this->idCol VARCHAR2(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR2(64) NOT NULL, $this->expirationCol INTEGER)";
  208.                 break;
  209.             case 'sqlsrv':
  210.                 $sql "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
  211.                 break;
  212.             default:
  213.                 throw new \DomainException(sprintf('Creating the lock table is currently not implemented for platform "%s".'$driver));
  214.         }
  215.         $conn->exec($sql);
  216.     }
  217.     /**
  218.      * Adds the Table to the Schema if it doesn't exist.
  219.      *
  220.      * @deprecated since symfony/lock 5.4 use DoctrineDbalStore instead
  221.      */
  222.     public function configureSchema(Schema $schema): void
  223.     {
  224.         if (isset($this->dbalStore)) {
  225.             $this->dbalStore->configureSchema($schema);
  226.             return;
  227.         }
  228.         throw new \BadMethodCallException(sprintf('"%s::%s()" is only supported when using a doctrine/dbal Connection.'__CLASS____METHOD__));
  229.     }
  230.     /**
  231.      * Cleans up the table by removing all expired locks.
  232.      */
  233.     private function prune(): void
  234.     {
  235.         $sql "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
  236.         $this->getConnection()->exec($sql);
  237.     }
  238.     private function getDriver(): string
  239.     {
  240.         if (null !== $this->driver) {
  241.             return $this->driver;
  242.         }
  243.         $conn $this->getConnection();
  244.         $this->driver $conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
  245.         return $this->driver;
  246.     }
  247.     /**
  248.      * Provides an SQL function to get the current timestamp regarding the current connection's driver.
  249.      */
  250.     private function getCurrentTimestampStatement(): string
  251.     {
  252.         switch ($this->getDriver()) {
  253.             case 'mysql':
  254.                 return 'UNIX_TIMESTAMP()';
  255.             case 'sqlite':
  256.                 return 'strftime(\'%s\',\'now\')';
  257.             case 'pgsql':
  258.                 return 'CAST(EXTRACT(epoch FROM NOW()) AS INT)';
  259.             case 'oci':
  260.                 return '(SYSDATE - TO_DATE(\'19700101\',\'yyyymmdd\'))*86400 - TO_NUMBER(SUBSTR(TZ_OFFSET(sessiontimezone), 1, 3))*3600';
  261.             case 'sqlsrv':
  262.                 return 'DATEDIFF(s, \'1970-01-01\', GETUTCDATE())';
  263.             default:
  264.                 return (string) time();
  265.         }
  266.     }
  267. }