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($connOrDsnstring $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 $callbackfloat $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 $schemaConnection $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 $valuesint $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($insertSql6);
  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.     /**
  484.      * @internal
  485.      */
  486.     protected function getId($key)
  487.     {
  488.         if ('pgsql' !== $this->driver ?? ($this->getConnection() ? $this->driver null)) {
  489.             return parent::getId($key);
  490.         }
  491.         if (str_contains($key"\0") || str_contains($key'%') || !preg_match('//u'$key)) {
  492.             $key rawurlencode($key);
  493.         }
  494.         return parent::getId($key);
  495.     }
  496.     private function getConnection(): \PDO
  497.     {
  498.         if (null === $this->conn) {
  499.             $this->conn = new \PDO($this->dsn$this->username$this->password$this->connectionOptions);
  500.             $this->conn->setAttribute(\PDO::ATTR_ERRMODE\PDO::ERRMODE_EXCEPTION);
  501.         }
  502.         if (null === $this->driver) {
  503.             $this->driver $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
  504.         }
  505.         return $this->conn;
  506.     }
  507.     private function getServerVersion(): string
  508.     {
  509.         if (null === $this->serverVersion) {
  510.             $this->serverVersion $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
  511.         }
  512.         return $this->serverVersion;
  513.     }
  514. }