diff options
Diffstat (limited to 'vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php')
-rw-r--r-- | vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php | 383 |
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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Doctrine\DBAL\ArrayParameterType; | ||
15 | use Doctrine\DBAL\Configuration; | ||
16 | use Doctrine\DBAL\Connection; | ||
17 | use Doctrine\DBAL\DriverManager; | ||
18 | use Doctrine\DBAL\Exception as DBALException; | ||
19 | use Doctrine\DBAL\Exception\TableNotFoundException; | ||
20 | use Doctrine\DBAL\ParameterType; | ||
21 | use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; | ||
22 | use Doctrine\DBAL\Schema\Schema; | ||
23 | use Doctrine\DBAL\Tools\DsnParser; | ||
24 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
25 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; | ||
26 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
27 | use Symfony\Component\Cache\PruneableInterface; | ||
28 | |||
29 | class 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 | } | ||