vendor/symfony/cache/Adapter/PdoAdapter.php line 68

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\Cache\Adapter;
  11. use Doctrine\DBAL\Connection;
  12. use Doctrine\DBAL\Schema\Schema;
  13. use Psr\Cache\CacheItemInterface;
  14. use Psr\Log\LoggerInterface;
  15. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  16. use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
  17. use Symfony\Component\Cache\Marshaller\MarshallerInterface;
  18. use Symfony\Component\Cache\PruneableInterface;
  19. class PdoAdapter extends AbstractAdapter implements PruneableInterface
  20. {
  21.     protected $maxIdLength = 255;
  22.     private $marshaller;
  23.     private $conn;
  24.     private $dsn;
  25.     private $driver;
  26.     private $serverVersion;
  27.     private $table = 'cache_items';
  28.     private $idCol = 'item_id';
  29.     private $dataCol = 'item_data';
  30.     private $lifetimeCol = 'item_lifetime';
  31.     private $timeCol = 'item_time';
  32.     private $username = '';
  33.     private $password = '';
  34.     private $connectionOptions = [];
  35.     private $namespace;
  36.     private $dbalAdapter;
  37.     /**
  38.      * You can either pass an existing database connection as PDO instance or
  39.      * a DSN string that will be used to lazy-connect to the database when the
  40.      * cache is actually used.
  41.      *
  42.      * List of available options:
  43.      *  * db_table: The name of the table [default: cache_items]
  44.      *  * db_id_col: The column where to store the cache id [default: item_id]
  45.      *  * db_data_col: The column where to store the cache data [default: item_data]
  46.      *  * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
  47.      *  * db_time_col: The column where to store the timestamp [default: item_time]
  48.      *  * db_username: The username when lazy-connect [default: '']
  49.      *  * db_password: The password when lazy-connect [default: '']
  50.      *  * db_connection_options: An array of driver-specific connection options [default: []]
  51.      *
  52.      * @param \PDO|string $connOrDsn
  53.      *
  54.      * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
  55.      * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
  56.      * @throws InvalidArgumentException When namespace contains invalid characters
  57.      */
  58.     public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null)
  59.     {
  60.         if ($connOrDsn instanceof Connection || (\is_string($connOrDsn) && str_contains($connOrDsn, '://'))) {
  61.             trigger_deprecation('symfony/cache', '5.4', 'Usage of a DBAL Connection with "%s" is deprecated and will be removed in symfony 6.0. Use "%s" instead.', __CLASS__, DoctrineDbalAdapter::class);
  62.             $this->dbalAdapter = new DoctrineDbalAdapter($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);
  63.             return;
  64.         }
  65.         if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
  66.             throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
  67.         }
  68.         if ($connOrDsn instanceof \PDO) {
  69.             if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
  70.                 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)).', __CLASS__));
  71.             }
  72.             $this->conn = $connOrDsn;
  73.         } elseif (\is_string($connOrDsn)) {
  74.             $this->dsn = $connOrDsn;
  75.         } else {
  76.             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)));
  77.         }
  78.         $this->table = $options['db_table'] ?? $this->table;
  79.         $this->idCol = $options['db_id_col'] ?? $this->idCol;
  80.         $this->dataCol = $options['db_data_col'] ?? $this->dataCol;
  81.         $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
  82.         $this->timeCol = $options['db_time_col'] ?? $this->timeCol;
  83.         $this->username = $options['db_username'] ?? $this->username;
  84.         $this->password = $options['db_password'] ?? $this->password;
  85.         $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
  86.         $this->namespace = $namespace;
  87.         $this->marshaller = $marshaller ?? new DefaultMarshaller();
  88.         parent::__construct($namespace, $defaultLifetime);
  89.     }
  90.     /**
  91.      * {@inheritDoc}
  92.      */
  93.     public function getItem($key)
  94.     {
  95.         if (isset($this->dbalAdapter)) {
  96.             return $this->dbalAdapter->getItem($key);
  97.         }
  98.         return parent::getItem($key);
  99.     }
  100.     /**
  101.      * {@inheritDoc}
  102.      */
  103.     public function getItems(array $keys = [])
  104.     {
  105.         if (isset($this->dbalAdapter)) {
  106.             return $this->dbalAdapter->getItems($keys);
  107.         }
  108.         return parent::getItems($keys);
  109.     }
  110.     /**
  111.      * {@inheritDoc}
  112.      */
  113.     public function hasItem($key)
  114.     {
  115.         if (isset($this->dbalAdapter)) {
  116.             return $this->dbalAdapter->hasItem($key);
  117.         }
  118.         return parent::hasItem($key);
  119.     }
  120.     /**
  121.      * {@inheritDoc}
  122.      */
  123.     public function deleteItem($key)
  124.     {
  125.         if (isset($this->dbalAdapter)) {
  126.             return $this->dbalAdapter->deleteItem($key);
  127.         }
  128.         return parent::deleteItem($key);
  129.     }
  130.     /**
  131.      * {@inheritDoc}
  132.      */
  133.     public function deleteItems(array $keys)
  134.     {
  135.         if (isset($this->dbalAdapter)) {
  136.             return $this->dbalAdapter->deleteItems($keys);
  137.         }
  138.         return parent::deleteItems($keys);
  139.     }
  140.     /**
  141.      * {@inheritDoc}
  142.      */
  143.     public function clear(string $prefix = '')
  144.     {
  145.         if (isset($this->dbalAdapter)) {
  146.             return $this->dbalAdapter->clear($prefix);
  147.         }
  148.         return parent::clear($prefix);
  149.     }
  150.     /**
  151.      * {@inheritDoc}
  152.      */
  153.     public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
  154.     {
  155.         if (isset($this->dbalAdapter)) {
  156.             return $this->dbalAdapter->get($key, $callback, $beta, $metadata);
  157.         }
  158.         return parent::get($key, $callback, $beta, $metadata);
  159.     }
  160.     /**
  161.      * {@inheritDoc}
  162.      */
  163.     public function delete(string $key): bool
  164.     {
  165.         if (isset($this->dbalAdapter)) {
  166.             return $this->dbalAdapter->delete($key);
  167.         }
  168.         return parent::delete($key);
  169.     }
  170.     /**
  171.      * {@inheritDoc}
  172.      */
  173.     public function save(CacheItemInterface $item)
  174.     {
  175.         if (isset($this->dbalAdapter)) {
  176.             return $this->dbalAdapter->save($item);
  177.         }
  178.         return parent::save($item);
  179.     }
  180.     /**
  181.      * {@inheritDoc}
  182.      */
  183.     public function saveDeferred(CacheItemInterface $item)
  184.     {
  185.         if (isset($this->dbalAdapter)) {
  186.             return $this->dbalAdapter->saveDeferred($item);
  187.         }
  188.         return parent::saveDeferred($item);
  189.     }
  190.     /**
  191.      * {@inheritDoc}
  192.      */
  193.     public function setLogger(LoggerInterface $logger): void
  194.     {
  195.         if (isset($this->dbalAdapter)) {
  196.             $this->dbalAdapter->setLogger($logger);
  197.             return;
  198.         }
  199.         parent::setLogger($logger);
  200.     }
  201.     /**
  202.      * {@inheritDoc}
  203.      */
  204.     public function commit()
  205.     {
  206.         if (isset($this->dbalAdapter)) {
  207.             return $this->dbalAdapter->commit();
  208.         }
  209.         return parent::commit();
  210.     }
  211.     /**
  212.      * {@inheritDoc}
  213.      */
  214.     public function reset()
  215.     {
  216.         if (isset($this->dbalAdapter)) {
  217.             $this->dbalAdapter->reset();
  218.             return;
  219.         }
  220.         parent::reset();
  221.     }
  222.     /**
  223.      * Creates the table to store cache items which can be called once for setup.
  224.      *
  225.      * Cache ID are saved in a column of maximum length 255. Cache data is
  226.      * saved in a BLOB.
  227.      *
  228.      * @throws \PDOException    When the table already exists
  229.      * @throws \DomainException When an unsupported PDO driver is used
  230.      */
  231.     public function createTable()
  232.     {
  233.         if (isset($this->dbalAdapter)) {
  234.             $this->dbalAdapter->createTable();
  235.             return;
  236.         }
  237.         // connect if we are not yet
  238.         $conn = $this->getConnection();
  239.         switch ($this->driver) {
  240.             case 'mysql':
  241.                 // We use varbinary for the ID column because it prevents unwanted conversions:
  242.                 // - character set conversions between server and client
  243.                 // - trailing space removal
  244.                 // - case-insensitivity
  245.                 // - language processing like Ã© == e
  246.                 $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB";
  247.                 break;
  248.             case 'sqlite':
  249.                 $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
  250.                 break;
  251.             case 'pgsql':
  252.                 $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
  253.                 break;
  254.             case 'oci':
  255.                 $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
  256.                 break;
  257.             case 'sqlsrv':
  258.                 $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
  259.                 break;
  260.             default:
  261.                 throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
  262.         }
  263.         $conn->exec($sql);
  264.     }
  265.     /**
  266.      * Adds the Table to the Schema if the adapter uses this Connection.
  267.      *
  268.      * @deprecated since symfony/cache 5.4 use DoctrineDbalAdapter instead
  269.      */
  270.     public function configureSchema(Schema $schema, Connection $forConnection): void
  271.     {
  272.         if (isset($this->dbalAdapter)) {
  273.             $this->dbalAdapter->configureSchema($schema, $forConnection);
  274.         }
  275.     }
  276.     /**
  277.      * {@inheritdoc}
  278.      */
  279.     public function prune()
  280.     {
  281.         if (isset($this->dbalAdapter)) {
  282.             return $this->dbalAdapter->prune();
  283.         }
  284.         $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time";
  285.         if ('' !== $this->namespace) {
  286.             $deleteSql .= " AND $this->idCol LIKE :namespace";
  287.         }
  288.         $connection = $this->getConnection();
  289.         try {
  290.             $delete = $connection->prepare($deleteSql);
  291.         } catch (\PDOException $e) {
  292.             return true;
  293.         }
  294.         $delete->bindValue(':time', time(), \PDO::PARAM_INT);
  295.         if ('' !== $this->namespace) {
  296.             $delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR);
  297.         }
  298.         try {
  299.             return $delete->execute();
  300.         } catch (\PDOException $e) {
  301.             return true;
  302.         }
  303.     }
  304.     /**
  305.      * {@inheritdoc}
  306.      */
  307.     protected function doFetch(array $ids)
  308.     {
  309.         $connection = $this->getConnection();
  310.         $now = time();
  311.         $expired = [];
  312.         $sql = str_pad('', (\count($ids) << 1) - 1, '?,');
  313.         $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
  314.         $stmt = $connection->prepare($sql);
  315.         $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
  316.         foreach ($ids as $id) {
  317.             $stmt->bindValue(++$i, $id);
  318.         }
  319.         $result = $stmt->execute();
  320.         if (\is_object($result)) {
  321.             $result = $result->iterateNumeric();
  322.         } else {
  323.             $stmt->setFetchMode(\PDO::FETCH_NUM);
  324.             $result = $stmt;
  325.         }
  326.         foreach ($result as $row) {
  327.             if (null === $row[1]) {
  328.                 $expired[] = $row[0];
  329.             } else {
  330.                 yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
  331.             }
  332.         }
  333.         if ($expired) {
  334.             $sql = str_pad('', (\count($expired) << 1) - 1, '?,');
  335.             $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)";
  336.             $stmt = $connection->prepare($sql);
  337.             $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
  338.             foreach ($expired as $id) {
  339.                 $stmt->bindValue(++$i, $id);
  340.             }
  341.             $stmt->execute();
  342.         }
  343.     }
  344.     /**
  345.      * {@inheritdoc}
  346.      */
  347.     protected function doHave(string $id)
  348.     {
  349.         $connection = $this->getConnection();
  350.         $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)";
  351.         $stmt = $connection->prepare($sql);
  352.         $stmt->bindValue(':id', $id);
  353.         $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
  354.         $stmt->execute();
  355.         return (bool) $stmt->fetchColumn();
  356.     }
  357.     /**
  358.      * {@inheritdoc}
  359.      */
  360.     protected function doClear(string $namespace)
  361.     {
  362.         $conn = $this->getConnection();
  363.         if ('' === $namespace) {
  364.             if ('sqlite' === $this->driver) {
  365.                 $sql = "DELETE FROM $this->table";
  366.             } else {
  367.                 $sql = "TRUNCATE TABLE $this->table";
  368.             }
  369.         } else {
  370.             $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
  371.         }
  372.         try {
  373.             $conn->exec($sql);
  374.         } catch (\PDOException $e) {
  375.         }
  376.         return true;
  377.     }
  378.     /**
  379.      * {@inheritdoc}
  380.      */
  381.     protected function doDelete(array $ids)
  382.     {
  383.         $sql = str_pad('', (\count($ids) << 1) - 1, '?,');
  384.         $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
  385.         try {
  386.             $stmt = $this->getConnection()->prepare($sql);
  387.             $stmt->execute(array_values($ids));
  388.         } catch (\PDOException $e) {
  389.         }
  390.         return true;
  391.     }
  392.     /**
  393.      * {@inheritdoc}
  394.      */
  395.     protected function doSave(array $values, int $lifetime)
  396.     {
  397.         if (!$values = $this->marshaller->marshall($values, $failed)) {
  398.             return $failed;
  399.         }
  400.         $conn = $this->getConnection();
  401.         $driver = $this->driver;
  402.         $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
  403.         switch (true) {
  404.             case 'mysql' === $driver:
  405.                 $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
  406.                 break;
  407.             case 'oci' === $driver:
  408.                 // DUAL is Oracle specific dummy table
  409.                 $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
  410.                     "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
  411.                     "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
  412.                 break;
  413.             case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='):
  414.                 // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
  415.                 // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
  416.                 $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
  417.                     "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
  418.                     "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
  419.                 break;
  420.             case 'sqlite' === $driver:
  421.                 $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
  422.                 break;
  423.             case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='):
  424.                 $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
  425.                 break;
  426.             default:
  427.                 $driver = null;
  428.                 $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
  429.                 break;
  430.         }
  431.         $now = time();
  432.         $lifetime = $lifetime ?: null;
  433.         try {
  434.             $stmt = $conn->prepare($sql);
  435.         } catch (\PDOException $e) {
  436.             if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
  437.                 $this->createTable();
  438.             }
  439.             $stmt = $conn->prepare($sql);
  440.         }
  441.         // $id and $data are defined later in the loop. Binding is done by reference, values are read on execution.
  442.         if ('sqlsrv' === $driver || 'oci' === $driver) {
  443.             $stmt->bindParam(1, $id);
  444.             $stmt->bindParam(2, $id);
  445.             $stmt->bindParam(3, $data, \PDO::PARAM_LOB);
  446.             $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT);
  447.             $stmt->bindValue(5, $now, \PDO::PARAM_INT);
  448.             $stmt->bindParam(6, $data, \PDO::PARAM_LOB);
  449.             $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT);
  450.             $stmt->bindValue(8, $now, \PDO::PARAM_INT);
  451.         } else {
  452.             $stmt->bindParam(':id', $id);
  453.             $stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
  454.             $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
  455.             $stmt->bindValue(':time', $now, \PDO::PARAM_INT);
  456.         }
  457.         if (null === $driver) {
  458.             $insertStmt = $conn->prepare($insertSql);
  459.             $insertStmt->bindParam(':id', $id);
  460.             $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
  461.             $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
  462.             $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
  463.         }
  464.         foreach ($values as $id => $data) {
  465.             try {
  466.                 $stmt->execute();
  467.             } catch (\PDOException $e) {
  468.                 if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
  469.                     $this->createTable();
  470.                 }
  471.                 $stmt->execute();
  472.             }
  473.             if (null === $driver && !$stmt->rowCount()) {
  474.                 try {
  475.                     $insertStmt->execute();
  476.                 } catch (\PDOException $e) {
  477.                     // A concurrent write won, let it be
  478.                 }
  479.             }
  480.         }
  481.         return $failed;
  482.     }
  483.     private function getConnection(): \PDO
  484.     {
  485.         if (null === $this->conn) {
  486.             $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
  487.             $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
  488.         }
  489.         if (null === $this->driver) {
  490.             $this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
  491.         }
  492.         return $this->conn;
  493.     }
  494.     private function getServerVersion(): string
  495.     {
  496.         if (null === $this->serverVersion) {
  497.             $this->serverVersion = $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
  498.         }
  499.         return $this->serverVersion;
  500.     }
  501. }