vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php line 185

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\ArrayParameterType;
  12. use Doctrine\DBAL\Configuration;
  13. use Doctrine\DBAL\Connection;
  14. use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
  15. use Doctrine\DBAL\DriverManager;
  16. use Doctrine\DBAL\Exception as DBALException;
  17. use Doctrine\DBAL\Exception\TableNotFoundException;
  18. use Doctrine\DBAL\ParameterType;
  19. use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
  20. use Doctrine\DBAL\Schema\Schema;
  21. use Doctrine\DBAL\ServerVersionProvider;
  22. use Doctrine\DBAL\Tools\DsnParser;
  23. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  24. use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
  25. use Symfony\Component\Cache\Marshaller\MarshallerInterface;
  26. use Symfony\Component\Cache\PruneableInterface;
  27. class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
  28. {
  29.     protected $maxIdLength 255;
  30.     private $marshaller;
  31.     private $conn;
  32.     private $platformName;
  33.     private $serverVersion;
  34.     private $table 'cache_items';
  35.     private $idCol 'item_id';
  36.     private $dataCol 'item_data';
  37.     private $lifetimeCol 'item_lifetime';
  38.     private $timeCol 'item_time';
  39.     private $namespace;
  40.     /**
  41.      * You can either pass an existing database Doctrine DBAL Connection or
  42.      * a DSN string that will be used to connect to the database.
  43.      *
  44.      * The cache table is created automatically when possible.
  45.      * Otherwise, use the createTable() method.
  46.      *
  47.      * List of available options:
  48.      *  * db_table: The name of the table [default: cache_items]
  49.      *  * db_id_col: The column where to store the cache id [default: item_id]
  50.      *  * db_data_col: The column where to store the cache data [default: item_data]
  51.      *  * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
  52.      *  * db_time_col: The column where to store the timestamp [default: item_time]
  53.      *
  54.      * @param Connection|string $connOrDsn
  55.      *
  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 (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#'$namespace$match)) {
  61.             throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.'$match[0]));
  62.         }
  63.         if ($connOrDsn instanceof Connection) {
  64.             $this->conn $connOrDsn;
  65.         } elseif (\is_string($connOrDsn)) {
  66.             if (!class_exists(DriverManager::class)) {
  67.                 throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
  68.             }
  69.             if (class_exists(DsnParser::class)) {
  70.                 $params = (new DsnParser([
  71.                     'db2' => 'ibm_db2',
  72.                     'mssql' => 'pdo_sqlsrv',
  73.                     'mysql' => 'pdo_mysql',
  74.                     'mysql2' => 'pdo_mysql',
  75.                     'postgres' => 'pdo_pgsql',
  76.                     'postgresql' => 'pdo_pgsql',
  77.                     'pgsql' => 'pdo_pgsql',
  78.                     'sqlite' => 'pdo_sqlite',
  79.                     'sqlite3' => 'pdo_sqlite',
  80.                 ]))->parse($connOrDsn);
  81.             } else {
  82.                 $params = ['url' => $connOrDsn];
  83.             }
  84.             $config = new Configuration();
  85.             if (class_exists(DefaultSchemaManagerFactory::class)) {
  86.                 $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
  87.             }
  88.             $this->conn DriverManager::getConnection($params$config);
  89.         } else {
  90.             throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be "%s" or string, "%s" given.'__METHOD__Connection::class, get_debug_type($connOrDsn)));
  91.         }
  92.         $this->table $options['db_table'] ?? $this->table;
  93.         $this->idCol $options['db_id_col'] ?? $this->idCol;
  94.         $this->dataCol $options['db_data_col'] ?? $this->dataCol;
  95.         $this->lifetimeCol $options['db_lifetime_col'] ?? $this->lifetimeCol;
  96.         $this->timeCol $options['db_time_col'] ?? $this->timeCol;
  97.         $this->namespace $namespace;
  98.         $this->marshaller $marshaller ?? new DefaultMarshaller();
  99.         parent::__construct($namespace$defaultLifetime);
  100.     }
  101.     /**
  102.      * Creates the table to store cache items which can be called once for setup.
  103.      *
  104.      * Cache ID are saved in a column of maximum length 255. Cache data is
  105.      * saved in a BLOB.
  106.      *
  107.      * @throws DBALException When the table already exists
  108.      */
  109.     public function createTable()
  110.     {
  111.         $schema = new Schema();
  112.         $this->addTableToSchema($schema);
  113.         foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
  114.             $this->conn->executeStatement($sql);
  115.         }
  116.     }
  117.     /**
  118.      * {@inheritdoc}
  119.      */
  120.     public function configureSchema(Schema $schemaConnection $forConnection): void
  121.     {
  122.         // only update the schema for this connection
  123.         if ($forConnection !== $this->conn) {
  124.             return;
  125.         }
  126.         if ($schema->hasTable($this->table)) {
  127.             return;
  128.         }
  129.         $this->addTableToSchema($schema);
  130.     }
  131.     /**
  132.      * {@inheritdoc}
  133.      */
  134.     public function prune(): bool
  135.     {
  136.         $deleteSql "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?";
  137.         $params = [time()];
  138.         $paramTypes = [ParameterType::INTEGER];
  139.         if ('' !== $this->namespace) {
  140.             $deleteSql .= " AND $this->idCol LIKE ?";
  141.             $params[] = sprintf('%s%%'$this->namespace);
  142.             $paramTypes[] = ParameterType::STRING;
  143.         }
  144.         try {
  145.             $this->conn->executeStatement($deleteSql$params$paramTypes);
  146.         } catch (TableNotFoundException $e) {
  147.         }
  148.         return true;
  149.     }
  150.     /**
  151.      * {@inheritdoc}
  152.      */
  153.     protected function doFetch(array $ids): iterable
  154.     {
  155.         $now time();
  156.         $expired = [];
  157.         $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 (?)";
  158.         $result $this->conn->executeQuery($sql, [
  159.             $now,
  160.             $ids,
  161.         ], [
  162.             ParameterType::INTEGER,
  163.             class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING Connection::PARAM_STR_ARRAY,
  164.         ])->iterateNumeric();
  165.         foreach ($result as $row) {
  166.             if (null === $row[1]) {
  167.                 $expired[] = $row[0];
  168.             } else {
  169.                 yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
  170.             }
  171.         }
  172.         if ($expired) {
  173.             $sql "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)";
  174.             $this->conn->executeStatement($sql, [
  175.                 $now,
  176.                 $expired,
  177.             ], [
  178.                 ParameterType::INTEGER,
  179.                 class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING Connection::PARAM_STR_ARRAY,
  180.             ]);
  181.         }
  182.     }
  183.     /**
  184.      * {@inheritdoc}
  185.      */
  186.     protected function doHave(string $id): bool
  187.     {
  188.         $sql "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)";
  189.         $result $this->conn->executeQuery($sql, [
  190.             $id,
  191.             time(),
  192.         ], [
  193.             ParameterType::STRING,
  194.             ParameterType::INTEGER,
  195.         ]);
  196.         return (bool) $result->fetchOne();
  197.     }
  198.     /**
  199.      * {@inheritdoc}
  200.      */
  201.     protected function doClear(string $namespace): bool
  202.     {
  203.         if ('' === $namespace) {
  204.             if ('sqlite' === $this->getPlatformName()) {
  205.                 $sql "DELETE FROM $this->table";
  206.             } else {
  207.                 $sql "TRUNCATE TABLE $this->table";
  208.             }
  209.         } else {
  210.             $sql "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
  211.         }
  212.         try {
  213.             $this->conn->executeStatement($sql);
  214.         } catch (TableNotFoundException $e) {
  215.         }
  216.         return true;
  217.     }
  218.     /**
  219.      * {@inheritdoc}
  220.      */
  221.     protected function doDelete(array $ids): bool
  222.     {
  223.         $sql "DELETE FROM $this->table WHERE $this->idCol IN (?)";
  224.         try {
  225.             $this->conn->executeStatement($sql, [array_values($ids)], [class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING Connection::PARAM_STR_ARRAY]);
  226.         } catch (TableNotFoundException $e) {
  227.         }
  228.         return true;
  229.     }
  230.     /**
  231.      * {@inheritdoc}
  232.      */
  233.     protected function doSave(array $valuesint $lifetime)
  234.     {
  235.         if (!$values $this->marshaller->marshall($values$failed)) {
  236.             return $failed;
  237.         }
  238.         $platformName $this->getPlatformName();
  239.         $insertSql "INSERT INTO $this->table ($this->idCol$this->dataCol$this->lifetimeCol$this->timeCol) VALUES (?, ?, ?, ?)";
  240.         switch (true) {
  241.             case 'mysql' === $platformName:
  242.                 $sql $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
  243.                 break;
  244.             case 'oci' === $platformName:
  245.                 // DUAL is Oracle specific dummy table
  246.                 $sql "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
  247.                     "WHEN NOT MATCHED THEN INSERT ($this->idCol$this->dataCol$this->lifetimeCol$this->timeCol) VALUES (?, ?, ?, ?) ".
  248.                     "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
  249.                 break;
  250.             case 'sqlsrv' === $platformName && version_compare($this->getServerVersion(), '10''>='):
  251.                 // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
  252.                 // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
  253.                 $sql "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
  254.                     "WHEN NOT MATCHED THEN INSERT ($this->idCol$this->dataCol$this->lifetimeCol$this->timeCol) VALUES (?, ?, ?, ?) ".
  255.                     "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
  256.                 break;
  257.             case 'sqlite' === $platformName:
  258.                 $sql 'INSERT OR REPLACE'.substr($insertSql6);
  259.                 break;
  260.             case 'pgsql' === $platformName && version_compare($this->getServerVersion(), '9.5''>='):
  261.                 $sql $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol$this->lifetimeCol$this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
  262.                 break;
  263.             default:
  264.                 $platformName null;
  265.                 $sql "UPDATE $this->table SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ? WHERE $this->idCol = ?";
  266.                 break;
  267.         }
  268.         $now time();
  269.         $lifetime $lifetime ?: null;
  270.         try {
  271.             $stmt $this->conn->prepare($sql);
  272.         } catch (TableNotFoundException $e) {
  273.             if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql''sqlite''sqlsrv'], true)) {
  274.                 $this->createTable();
  275.             }
  276.             $stmt $this->conn->prepare($sql);
  277.         }
  278.         if ('sqlsrv' === $platformName || 'oci' === $platformName) {
  279.             $bind = static function ($id$data) use ($stmt) {
  280.                 $stmt->bindValue(1$id);
  281.                 $stmt->bindValue(2$id);
  282.                 $stmt->bindValue(3$dataParameterType::LARGE_OBJECT);
  283.                 $stmt->bindValue(6$dataParameterType::LARGE_OBJECT);
  284.             };
  285.             $stmt->bindValue(4$lifetimeParameterType::INTEGER);
  286.             $stmt->bindValue(5$nowParameterType::INTEGER);
  287.             $stmt->bindValue(7$lifetimeParameterType::INTEGER);
  288.             $stmt->bindValue(8$nowParameterType::INTEGER);
  289.         } elseif (null !== $platformName) {
  290.             $bind = static function ($id$data) use ($stmt) {
  291.                 $stmt->bindValue(1$id);
  292.                 $stmt->bindValue(2$dataParameterType::LARGE_OBJECT);
  293.             };
  294.             $stmt->bindValue(3$lifetimeParameterType::INTEGER);
  295.             $stmt->bindValue(4$nowParameterType::INTEGER);
  296.         } else {
  297.             $stmt->bindValue(2$lifetimeParameterType::INTEGER);
  298.             $stmt->bindValue(3$nowParameterType::INTEGER);
  299.             $insertStmt $this->conn->prepare($insertSql);
  300.             $insertStmt->bindValue(3$lifetimeParameterType::INTEGER);
  301.             $insertStmt->bindValue(4$nowParameterType::INTEGER);
  302.             $bind = static function ($id$data) use ($stmt$insertStmt) {
  303.                 $stmt->bindValue(1$dataParameterType::LARGE_OBJECT);
  304.                 $stmt->bindValue(4$id);
  305.                 $insertStmt->bindValue(1$id);
  306.                 $insertStmt->bindValue(2$dataParameterType::LARGE_OBJECT);
  307.             };
  308.         }
  309.         foreach ($values as $id => $data) {
  310.             $bind($id$data);
  311.             try {
  312.                 $rowCount $stmt->executeStatement();
  313.             } catch (TableNotFoundException $e) {
  314.                 if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql''sqlite''sqlsrv'], true)) {
  315.                     $this->createTable();
  316.                 }
  317.                 $rowCount $stmt->executeStatement();
  318.             }
  319.             if (null === $platformName && === $rowCount) {
  320.                 try {
  321.                     $insertStmt->executeStatement();
  322.                 } catch (DBALException $e) {
  323.                     // A concurrent write won, let it be
  324.                 }
  325.             }
  326.         }
  327.         return $failed;
  328.     }
  329.     /**
  330.      * @internal
  331.      */
  332.     protected function getId($key)
  333.     {
  334.         if ('pgsql' !== $this->getPlatformName()) {
  335.             return parent::getId($key);
  336.         }
  337.         if (str_contains($key"\0") || str_contains($key'%') || !preg_match('//u'$key)) {
  338.             $key rawurlencode($key);
  339.         }
  340.         return parent::getId($key);
  341.     }
  342.     private function getPlatformName(): string
  343.     {
  344.         if (isset($this->platformName)) {
  345.             return $this->platformName;
  346.         }
  347.         $platform $this->conn->getDatabasePlatform();
  348.         switch (true) {
  349.             case $platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform:
  350.             case $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform:
  351.                 return $this->platformName 'mysql';
  352.             case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform:
  353.                 return $this->platformName 'sqlite';
  354.             case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform:
  355.             case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform:
  356.                 return $this->platformName 'pgsql';
  357.             case $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform:
  358.                 return $this->platformName 'oci';
  359.             case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform:
  360.             case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform:
  361.                 return $this->platformName 'sqlsrv';
  362.             default:
  363.                 return $this->platformName \get_class($platform);
  364.         }
  365.     }
  366.     private function getServerVersion(): string
  367.     {
  368.         if (isset($this->serverVersion)) {
  369.             return $this->serverVersion;
  370.         }
  371.         if ($this->conn instanceof ServerVersionProvider || $this->conn instanceof ServerInfoAwareConnection) {
  372.             return $this->serverVersion $this->conn->getServerVersion();
  373.         }
  374.         // The condition should be removed once support for DBAL <3.3 is dropped
  375.         $conn method_exists($this->conn'getNativeConnection') ? $this->conn->getNativeConnection() : $this->conn->getWrappedConnection();
  376.         return $this->serverVersion $conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
  377.     }
  378.     private function addTableToSchema(Schema $schema): void
  379.     {
  380.         $types = [
  381.             'mysql' => 'binary',
  382.             'sqlite' => 'text',
  383.         ];
  384.         $table $schema->createTable($this->table);
  385.         $table->addColumn($this->idCol$types[$this->getPlatformName()] ?? 'string', ['length' => 255]);
  386.         $table->addColumn($this->dataCol'blob', ['length' => 16777215]);
  387.         $table->addColumn($this->lifetimeCol'integer', ['unsigned' => true'notnull' => false]);
  388.         $table->addColumn($this->timeCol'integer', ['unsigned' => true]);
  389.         $table->setPrimaryKey([$this->idCol]);
  390.     }
  391. }