summaryrefslogtreecommitdiff
path: root/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php')
-rw-r--r--vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php383
1 files changed, 383 insertions, 0 deletions
diff --git a/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php
new file mode 100644
index 0000000..ae2bea7
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php
@@ -0,0 +1,383 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Doctrine\DBAL\ArrayParameterType;
15use Doctrine\DBAL\Configuration;
16use Doctrine\DBAL\Connection;
17use Doctrine\DBAL\DriverManager;
18use Doctrine\DBAL\Exception as DBALException;
19use Doctrine\DBAL\Exception\TableNotFoundException;
20use Doctrine\DBAL\ParameterType;
21use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
22use Doctrine\DBAL\Schema\Schema;
23use Doctrine\DBAL\Tools\DsnParser;
24use Symfony\Component\Cache\Exception\InvalidArgumentException;
25use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
26use Symfony\Component\Cache\Marshaller\MarshallerInterface;
27use Symfony\Component\Cache\PruneableInterface;
28
29class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
30{
31 private const MAX_KEY_LENGTH = 255;
32
33 private MarshallerInterface $marshaller;
34 private Connection $conn;
35 private string $platformName;
36 private string $table = 'cache_items';
37 private string $idCol = 'item_id';
38 private string $dataCol = 'item_data';
39 private string $lifetimeCol = 'item_lifetime';
40 private string $timeCol = 'item_time';
41
42 /**
43 * You can either pass an existing database Doctrine DBAL Connection or
44 * a DSN string that will be used to connect to the database.
45 *
46 * The cache table is created automatically when possible.
47 * Otherwise, use the createTable() method.
48 *
49 * List of available options:
50 * * db_table: The name of the table [default: cache_items]
51 * * db_id_col: The column where to store the cache id [default: item_id]
52 * * db_data_col: The column where to store the cache data [default: item_data]
53 * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
54 * * db_time_col: The column where to store the timestamp [default: item_time]
55 *
56 * @throws InvalidArgumentException When namespace contains invalid characters
57 */
58 public function __construct(
59 Connection|string $connOrDsn,
60 private string $namespace = '',
61 int $defaultLifetime = 0,
62 array $options = [],
63 ?MarshallerInterface $marshaller = null,
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
69 if ($connOrDsn instanceof Connection) {
70 $this->conn = $connOrDsn;
71 } else {
72 if (!class_exists(DriverManager::class)) {
73 throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
74 }
75 $params = (new DsnParser([
76 'db2' => 'ibm_db2',
77 'mssql' => 'pdo_sqlsrv',
78 'mysql' => 'pdo_mysql',
79 'mysql2' => 'pdo_mysql',
80 'postgres' => 'pdo_pgsql',
81 'postgresql' => 'pdo_pgsql',
82 'pgsql' => 'pdo_pgsql',
83 'sqlite' => 'pdo_sqlite',
84 'sqlite3' => 'pdo_sqlite',
85 ]))->parse($connOrDsn);
86
87 $config = new Configuration();
88 $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
89
90 $this->conn = DriverManager::getConnection($params, $config);
91 }
92
93 $this->maxIdLength = self::MAX_KEY_LENGTH;
94 $this->table = $options['db_table'] ?? $this->table;
95 $this->idCol = $options['db_id_col'] ?? $this->idCol;
96 $this->dataCol = $options['db_data_col'] ?? $this->dataCol;
97 $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
98 $this->timeCol = $options['db_time_col'] ?? $this->timeCol;
99 $this->marshaller = $marshaller ?? new DefaultMarshaller();
100
101 parent::__construct($namespace, $defaultLifetime);
102 }
103
104 /**
105 * Creates the table to store cache items which can be called once for setup.
106 *
107 * Cache ID are saved in a column of maximum length 255. Cache data is
108 * saved in a BLOB.
109 *
110 * @throws DBALException When the table already exists
111 */
112 public function createTable(): void
113 {
114 $schema = new Schema();
115 $this->addTableToSchema($schema);
116
117 foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
118 $this->conn->executeStatement($sql);
119 }
120 }
121
122 public function configureSchema(Schema $schema, Connection $forConnection, \Closure $isSameDatabase): void
123 {
124 if ($schema->hasTable($this->table)) {
125 return;
126 }
127
128 if ($forConnection !== $this->conn && !$isSameDatabase($this->conn->executeStatement(...))) {
129 return;
130 }
131
132 $this->addTableToSchema($schema);
133 }
134
135 public function prune(): bool
136 {
137 $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?";
138 $params = [time()];
139 $paramTypes = [ParameterType::INTEGER];
140
141 if ('' !== $this->namespace) {
142 $deleteSql .= " AND $this->idCol LIKE ?";
143 $params[] = sprintf('%s%%', $this->namespace);
144 $paramTypes[] = ParameterType::STRING;
145 }
146
147 try {
148 $this->conn->executeStatement($deleteSql, $params, $paramTypes);
149 } catch (TableNotFoundException) {
150 }
151
152 return true;
153 }
154
155 protected function doFetch(array $ids): iterable
156 {
157 $now = time();
158 $expired = [];
159
160 $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 (?)";
161 $result = $this->conn->executeQuery($sql, [
162 $now,
163 $ids,
164 ], [
165 ParameterType::INTEGER,
166 ArrayParameterType::STRING,
167 ])->iterateNumeric();
168
169 foreach ($result as $row) {
170 if (null === $row[1]) {
171 $expired[] = $row[0];
172 } else {
173 yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
174 }
175 }
176
177 if ($expired) {
178 $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)";
179 $this->conn->executeStatement($sql, [
180 $now,
181 $expired,
182 ], [
183 ParameterType::INTEGER,
184 ArrayParameterType::STRING,
185 ]);
186 }
187 }
188
189 protected function doHave(string $id): bool
190 {
191 $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)";
192 $result = $this->conn->executeQuery($sql, [
193 $id,
194 time(),
195 ], [
196 ParameterType::STRING,
197 ParameterType::INTEGER,
198 ]);
199
200 return (bool) $result->fetchOne();
201 }
202
203 protected function doClear(string $namespace): bool
204 {
205 if ('' === $namespace) {
206 $sql = $this->conn->getDatabasePlatform()->getTruncateTableSQL($this->table);
207 } else {
208 $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
209 }
210
211 try {
212 $this->conn->executeStatement($sql);
213 } catch (TableNotFoundException) {
214 }
215
216 return true;
217 }
218
219 protected function doDelete(array $ids): bool
220 {
221 $sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)";
222 try {
223 $this->conn->executeStatement($sql, [array_values($ids)], [ArrayParameterType::STRING]);
224 } catch (TableNotFoundException) {
225 }
226
227 return true;
228 }
229
230 protected function doSave(array $values, int $lifetime): array|bool
231 {
232 if (!$values = $this->marshaller->marshall($values, $failed)) {
233 return $failed;
234 }
235
236 $platformName = $this->getPlatformName();
237 $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)";
238
239 switch ($platformName) {
240 case 'mysql':
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':
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':
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':
257 $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
258 break;
259 case 'pgsql':
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
268 $now = time();
269 $lifetime = $lifetime ?: null;
270 try {
271 $stmt = $this->conn->prepare($sql);
272 } catch (TableNotFoundException) {
273 if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
274 $this->createTable();
275 }
276 $stmt = $this->conn->prepare($sql);
277 }
278
279 if ('sqlsrv' === $platformName || 'oci' === $platformName) {
280 $bind = static function ($id, $data) use ($stmt) {
281 $stmt->bindValue(1, $id);
282 $stmt->bindValue(2, $id);
283 $stmt->bindValue(3, $data, ParameterType::LARGE_OBJECT);
284 $stmt->bindValue(6, $data, ParameterType::LARGE_OBJECT);
285 };
286 $stmt->bindValue(4, $lifetime, ParameterType::INTEGER);
287 $stmt->bindValue(5, $now, ParameterType::INTEGER);
288 $stmt->bindValue(7, $lifetime, ParameterType::INTEGER);
289 $stmt->bindValue(8, $now, ParameterType::INTEGER);
290 } elseif (null !== $platformName) {
291 $bind = static function ($id, $data) use ($stmt) {
292 $stmt->bindValue(1, $id);
293 $stmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
294 };
295 $stmt->bindValue(3, $lifetime, ParameterType::INTEGER);
296 $stmt->bindValue(4, $now, ParameterType::INTEGER);
297 } else {
298 $stmt->bindValue(2, $lifetime, ParameterType::INTEGER);
299 $stmt->bindValue(3, $now, ParameterType::INTEGER);
300
301 $insertStmt = $this->conn->prepare($insertSql);
302 $insertStmt->bindValue(3, $lifetime, ParameterType::INTEGER);
303 $insertStmt->bindValue(4, $now, ParameterType::INTEGER);
304
305 $bind = static function ($id, $data) use ($stmt, $insertStmt) {
306 $stmt->bindValue(1, $data, ParameterType::LARGE_OBJECT);
307 $stmt->bindValue(4, $id);
308 $insertStmt->bindValue(1, $id);
309 $insertStmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
310 };
311 }
312
313 foreach ($values as $id => $data) {
314 $bind($id, $data);
315 try {
316 $rowCount = $stmt->executeStatement();
317 } catch (TableNotFoundException) {
318 if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
319 $this->createTable();
320 }
321 $rowCount = $stmt->executeStatement();
322 }
323 if (null === $platformName && 0 === $rowCount) {
324 try {
325 $insertStmt->executeStatement();
326 } catch (DBALException) {
327 // A concurrent write won, let it be
328 }
329 }
330 }
331
332 return $failed;
333 }
334
335 /**
336 * @internal
337 */
338 protected function getId(mixed $key): string
339 {
340 if ('pgsql' !== $this->platformName ??= $this->getPlatformName()) {
341 return parent::getId($key);
342 }
343
344 if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
345 $key = rawurlencode($key);
346 }
347
348 return parent::getId($key);
349 }
350
351 private function getPlatformName(): string
352 {
353 if (isset($this->platformName)) {
354 return $this->platformName;
355 }
356
357 $platform = $this->conn->getDatabasePlatform();
358
359 return $this->platformName = match (true) {
360 $platform instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform => 'mysql',
361 $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => 'sqlite',
362 $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform => 'pgsql',
363 $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform => 'oci',
364 $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform => 'sqlsrv',
365 default => $platform::class,
366 };
367 }
368
369 private function addTableToSchema(Schema $schema): void
370 {
371 $types = [
372 'mysql' => 'binary',
373 'sqlite' => 'text',
374 ];
375
376 $table = $schema->createTable($this->table);
377 $table->addColumn($this->idCol, $types[$this->getPlatformName()] ?? 'string', ['length' => 255]);
378 $table->addColumn($this->dataCol, 'blob', ['length' => 16777215]);
379 $table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]);
380 $table->addColumn($this->timeCol, 'integer', ['unsigned' => true]);
381 $table->setPrimaryKey([$this->idCol]);
382 }
383}