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

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