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

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