Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Cache/DatabaseBackend.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Core\Cache; | |
4 | |
5 use Drupal\Component\Utility\Crypt; | |
6 use Drupal\Core\Database\Connection; | |
7 use Drupal\Core\Database\SchemaObjectExistsException; | |
8 | |
9 /** | |
10 * Defines a default cache implementation. | |
11 * | |
12 * This is Drupal's default cache implementation. It uses the database to store | |
13 * cached data. Each cache bin corresponds to a database table by the same name. | |
14 * | |
15 * @ingroup cache | |
16 */ | |
17 class DatabaseBackend implements CacheBackendInterface { | |
18 | |
19 /** | |
20 * The default maximum number of rows that this cache bin table can store. | |
21 * | |
22 * This maximum is introduced to ensure that the database is not filled with | |
23 * hundred of thousand of cache entries with gigabytes in size. | |
24 * | |
25 * Read about how to change it in the @link cache Cache API topic. @endlink | |
26 */ | |
27 const DEFAULT_MAX_ROWS = 5000; | |
28 | |
29 /** | |
30 * -1 means infinite allows numbers of rows for the cache backend. | |
31 */ | |
32 const MAXIMUM_NONE = -1; | |
33 | |
34 /** | |
35 * The maximum number of rows that this cache bin table is allowed to store. | |
36 * | |
37 * @see ::MAXIMUM_NONE | |
38 * | |
39 * @var int | |
40 */ | |
41 protected $maxRows; | |
42 | |
43 /** | |
44 * @var string | |
45 */ | |
46 protected $bin; | |
47 | |
48 | |
49 /** | |
50 * The database connection. | |
51 * | |
52 * @var \Drupal\Core\Database\Connection | |
53 */ | |
54 protected $connection; | |
55 | |
56 /** | |
57 * The cache tags checksum provider. | |
58 * | |
59 * @var \Drupal\Core\Cache\CacheTagsChecksumInterface | |
60 */ | |
61 protected $checksumProvider; | |
62 | |
63 /** | |
64 * Constructs a DatabaseBackend object. | |
65 * | |
66 * @param \Drupal\Core\Database\Connection $connection | |
67 * The database connection. | |
68 * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider | |
69 * The cache tags checksum provider. | |
70 * @param string $bin | |
71 * The cache bin for which the object is created. | |
72 * @param int $max_rows | |
73 * (optional) The maximum number of rows that are allowed in this cache bin | |
74 * table. | |
75 */ | |
76 public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin, $max_rows = NULL) { | |
77 // All cache tables should be prefixed with 'cache_'. | |
78 $bin = 'cache_' . $bin; | |
79 | |
80 $this->bin = $bin; | |
81 $this->connection = $connection; | |
82 $this->checksumProvider = $checksum_provider; | |
83 $this->maxRows = $max_rows === NULL ? static::DEFAULT_MAX_ROWS : $max_rows; | |
84 } | |
85 | |
86 /** | |
87 * {@inheritdoc} | |
88 */ | |
89 public function get($cid, $allow_invalid = FALSE) { | |
90 $cids = [$cid]; | |
91 $cache = $this->getMultiple($cids, $allow_invalid); | |
92 return reset($cache); | |
93 } | |
94 | |
95 /** | |
96 * {@inheritdoc} | |
97 */ | |
98 public function getMultiple(&$cids, $allow_invalid = FALSE) { | |
99 $cid_mapping = []; | |
100 foreach ($cids as $cid) { | |
101 $cid_mapping[$this->normalizeCid($cid)] = $cid; | |
102 } | |
103 // When serving cached pages, the overhead of using ::select() was found | |
104 // to add around 30% overhead to the request. Since $this->bin is a | |
105 // variable, this means the call to ::query() here uses a concatenated | |
106 // string. This is highly discouraged under any other circumstances, and | |
107 // is used here only due to the performance overhead we would incur | |
108 // otherwise. When serving an uncached page, the overhead of using | |
109 // ::select() is a much smaller proportion of the request. | |
110 $result = []; | |
111 try { | |
112 $result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN ( :cids[] ) ORDER BY cid', [':cids[]' => array_keys($cid_mapping)]); | |
113 } | |
114 catch (\Exception $e) { | |
115 // Nothing to do. | |
116 } | |
117 $cache = []; | |
118 foreach ($result as $item) { | |
119 // Map the cache ID back to the original. | |
120 $item->cid = $cid_mapping[$item->cid]; | |
121 $item = $this->prepareItem($item, $allow_invalid); | |
122 if ($item) { | |
123 $cache[$item->cid] = $item; | |
124 } | |
125 } | |
126 $cids = array_diff($cids, array_keys($cache)); | |
127 return $cache; | |
128 } | |
129 | |
130 /** | |
131 * Prepares a cached item. | |
132 * | |
133 * Checks that items are either permanent or did not expire, and unserializes | |
134 * data as appropriate. | |
135 * | |
136 * @param object $cache | |
137 * An item loaded from cache_get() or cache_get_multiple(). | |
138 * @param bool $allow_invalid | |
139 * If FALSE, the method returns FALSE if the cache item is not valid. | |
140 * | |
141 * @return mixed|false | |
142 * The item with data unserialized as appropriate and a property indicating | |
143 * whether the item is valid, or FALSE if there is no valid item to load. | |
144 */ | |
145 protected function prepareItem($cache, $allow_invalid) { | |
146 if (!isset($cache->data)) { | |
147 return FALSE; | |
148 } | |
149 | |
150 $cache->tags = $cache->tags ? explode(' ', $cache->tags) : []; | |
151 | |
152 // Check expire time. | |
153 $cache->valid = $cache->expire == Cache::PERMANENT || $cache->expire >= REQUEST_TIME; | |
154 | |
155 // Check if invalidateTags() has been called with any of the items's tags. | |
156 if (!$this->checksumProvider->isValid($cache->checksum, $cache->tags)) { | |
157 $cache->valid = FALSE; | |
158 } | |
159 | |
160 if (!$allow_invalid && !$cache->valid) { | |
161 return FALSE; | |
162 } | |
163 | |
164 // Unserialize and return the cached data. | |
165 if ($cache->serialized) { | |
166 $cache->data = unserialize($cache->data); | |
167 } | |
168 | |
169 return $cache; | |
170 } | |
171 | |
172 /** | |
173 * {@inheritdoc} | |
174 */ | |
175 public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { | |
176 $this->setMultiple([ | |
177 $cid => [ | |
178 'data' => $data, | |
179 'expire' => $expire, | |
180 'tags' => $tags, | |
181 ], | |
182 ]); | |
183 } | |
184 | |
185 /** | |
186 * {@inheritdoc} | |
187 */ | |
188 public function setMultiple(array $items) { | |
189 $try_again = FALSE; | |
190 try { | |
191 // The bin might not yet exist. | |
192 $this->doSetMultiple($items); | |
193 } | |
194 catch (\Exception $e) { | |
195 // If there was an exception, try to create the bins. | |
196 if (!$try_again = $this->ensureBinExists()) { | |
197 // If the exception happened for other reason than the missing bin | |
198 // table, propagate the exception. | |
199 throw $e; | |
200 } | |
201 } | |
202 // Now that the bin has been created, try again if necessary. | |
203 if ($try_again) { | |
204 $this->doSetMultiple($items); | |
205 } | |
206 } | |
207 | |
208 /** | |
209 * Stores multiple items in the persistent cache. | |
210 * | |
211 * @param array $items | |
212 * An array of cache items, keyed by cid. | |
213 * | |
214 * @see \Drupal\Core\Cache\CacheBackendInterface::setMultiple() | |
215 */ | |
216 protected function doSetMultiple(array $items) { | |
217 $values = []; | |
218 | |
219 foreach ($items as $cid => $item) { | |
220 $item += [ | |
221 'expire' => CacheBackendInterface::CACHE_PERMANENT, | |
222 'tags' => [], | |
223 ]; | |
224 | |
225 assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($item[\'tags\'])', 'Cache Tags must be strings.'); | |
226 $item['tags'] = array_unique($item['tags']); | |
227 // Sort the cache tags so that they are stored consistently in the DB. | |
228 sort($item['tags']); | |
229 | |
230 $fields = [ | |
231 'cid' => $this->normalizeCid($cid), | |
232 'expire' => $item['expire'], | |
233 'created' => round(microtime(TRUE), 3), | |
234 'tags' => implode(' ', $item['tags']), | |
235 'checksum' => $this->checksumProvider->getCurrentChecksum($item['tags']), | |
236 ]; | |
237 | |
238 if (!is_string($item['data'])) { | |
239 $fields['data'] = serialize($item['data']); | |
240 $fields['serialized'] = 1; | |
241 } | |
242 else { | |
243 $fields['data'] = $item['data']; | |
244 $fields['serialized'] = 0; | |
245 } | |
246 $values[] = $fields; | |
247 } | |
248 | |
249 // Use an upsert query which is atomic and optimized for multiple-row | |
250 // merges. | |
251 $query = $this->connection | |
252 ->upsert($this->bin) | |
253 ->key('cid') | |
254 ->fields(['cid', 'expire', 'created', 'tags', 'checksum', 'data', 'serialized']); | |
255 foreach ($values as $fields) { | |
256 // Only pass the values since the order of $fields matches the order of | |
257 // the insert fields. This is a performance optimization to avoid | |
258 // unnecessary loops within the method. | |
259 $query->values(array_values($fields)); | |
260 } | |
261 | |
262 $query->execute(); | |
263 } | |
264 | |
265 /** | |
266 * {@inheritdoc} | |
267 */ | |
268 public function delete($cid) { | |
269 $this->deleteMultiple([$cid]); | |
270 } | |
271 | |
272 /** | |
273 * {@inheritdoc} | |
274 */ | |
275 public function deleteMultiple(array $cids) { | |
276 $cids = array_values(array_map([$this, 'normalizeCid'], $cids)); | |
277 try { | |
278 // Delete in chunks when a large array is passed. | |
279 foreach (array_chunk($cids, 1000) as $cids_chunk) { | |
280 $this->connection->delete($this->bin) | |
281 ->condition('cid', $cids_chunk, 'IN') | |
282 ->execute(); | |
283 } | |
284 } | |
285 catch (\Exception $e) { | |
286 // Create the cache table, which will be empty. This fixes cases during | |
287 // core install where a cache table is cleared before it is set | |
288 // with {cache_render} and {cache_data}. | |
289 if (!$this->ensureBinExists()) { | |
290 $this->catchException($e); | |
291 } | |
292 } | |
293 } | |
294 | |
295 /** | |
296 * {@inheritdoc} | |
297 */ | |
298 public function deleteAll() { | |
299 try { | |
300 $this->connection->truncate($this->bin)->execute(); | |
301 } | |
302 catch (\Exception $e) { | |
303 // Create the cache table, which will be empty. This fixes cases during | |
304 // core install where a cache table is cleared before it is set | |
305 // with {cache_render} and {cache_data}. | |
306 if (!$this->ensureBinExists()) { | |
307 $this->catchException($e); | |
308 } | |
309 } | |
310 } | |
311 | |
312 /** | |
313 * {@inheritdoc} | |
314 */ | |
315 public function invalidate($cid) { | |
316 $this->invalidateMultiple([$cid]); | |
317 } | |
318 | |
319 /** | |
320 * {@inheritdoc} | |
321 */ | |
322 public function invalidateMultiple(array $cids) { | |
323 $cids = array_values(array_map([$this, 'normalizeCid'], $cids)); | |
324 try { | |
325 // Update in chunks when a large array is passed. | |
326 foreach (array_chunk($cids, 1000) as $cids_chunk) { | |
327 $this->connection->update($this->bin) | |
328 ->fields(['expire' => REQUEST_TIME - 1]) | |
329 ->condition('cid', $cids_chunk, 'IN') | |
330 ->execute(); | |
331 } | |
332 } | |
333 catch (\Exception $e) { | |
334 $this->catchException($e); | |
335 } | |
336 } | |
337 | |
338 /** | |
339 * {@inheritdoc} | |
340 */ | |
341 public function invalidateAll() { | |
342 try { | |
343 $this->connection->update($this->bin) | |
344 ->fields(['expire' => REQUEST_TIME - 1]) | |
345 ->execute(); | |
346 } | |
347 catch (\Exception $e) { | |
348 $this->catchException($e); | |
349 } | |
350 } | |
351 | |
352 /** | |
353 * {@inheritdoc} | |
354 */ | |
355 public function garbageCollection() { | |
356 try { | |
357 // Bounded size cache bin, using FIFO. | |
358 if ($this->maxRows !== static::MAXIMUM_NONE) { | |
359 $first_invalid_create_time = $this->connection->select($this->bin) | |
360 ->fields($this->bin, ['created']) | |
361 ->orderBy("{$this->bin}.created", 'DESC') | |
362 ->range($this->maxRows, $this->maxRows + 1) | |
363 ->execute() | |
364 ->fetchField(); | |
365 | |
366 if ($first_invalid_create_time) { | |
367 $this->connection->delete($this->bin) | |
368 ->condition('created', $first_invalid_create_time, '<=') | |
369 ->execute(); | |
370 } | |
371 } | |
372 | |
373 $this->connection->delete($this->bin) | |
374 ->condition('expire', Cache::PERMANENT, '<>') | |
375 ->condition('expire', REQUEST_TIME, '<') | |
376 ->execute(); | |
377 } | |
378 catch (\Exception $e) { | |
379 // If the table does not exist, it surely does not have garbage in it. | |
380 // If the table exists, the next garbage collection will clean up. | |
381 // There is nothing to do. | |
382 } | |
383 } | |
384 | |
385 /** | |
386 * {@inheritdoc} | |
387 */ | |
388 public function removeBin() { | |
389 try { | |
390 $this->connection->schema()->dropTable($this->bin); | |
391 } | |
392 catch (\Exception $e) { | |
393 $this->catchException($e); | |
394 } | |
395 } | |
396 | |
397 /** | |
398 * Check if the cache bin exists and create it if not. | |
399 */ | |
400 protected function ensureBinExists() { | |
401 try { | |
402 $database_schema = $this->connection->schema(); | |
403 if (!$database_schema->tableExists($this->bin)) { | |
404 $schema_definition = $this->schemaDefinition(); | |
405 $database_schema->createTable($this->bin, $schema_definition); | |
406 return TRUE; | |
407 } | |
408 } | |
409 // If another process has already created the cache table, attempting to | |
410 // recreate it will throw an exception. In this case just catch the | |
411 // exception and do nothing. | |
412 catch (SchemaObjectExistsException $e) { | |
413 return TRUE; | |
414 } | |
415 return FALSE; | |
416 } | |
417 | |
418 /** | |
419 * Act on an exception when cache might be stale. | |
420 * | |
421 * If the table does not yet exist, that's fine, but if the table exists and | |
422 * yet the query failed, then the cache is stale and the exception needs to | |
423 * propagate. | |
424 * | |
425 * @param $e | |
426 * The exception. | |
427 * @param string|null $table_name | |
428 * The table name. Defaults to $this->bin. | |
429 * | |
430 * @throws \Exception | |
431 */ | |
432 protected function catchException(\Exception $e, $table_name = NULL) { | |
433 if ($this->connection->schema()->tableExists($table_name ?: $this->bin)) { | |
434 throw $e; | |
435 } | |
436 } | |
437 | |
438 /** | |
439 * Normalizes a cache ID in order to comply with database limitations. | |
440 * | |
441 * @param string $cid | |
442 * The passed in cache ID. | |
443 * | |
444 * @return string | |
445 * An ASCII-encoded cache ID that is at most 255 characters long. | |
446 */ | |
447 protected function normalizeCid($cid) { | |
448 // Nothing to do if the ID is a US ASCII string of 255 characters or less. | |
449 $cid_is_ascii = mb_check_encoding($cid, 'ASCII'); | |
450 if (strlen($cid) <= 255 && $cid_is_ascii) { | |
451 return $cid; | |
452 } | |
453 // Return a string that uses as much as possible of the original cache ID | |
454 // with the hash appended. | |
455 $hash = Crypt::hashBase64($cid); | |
456 if (!$cid_is_ascii) { | |
457 return $hash; | |
458 } | |
459 return substr($cid, 0, 255 - strlen($hash)) . $hash; | |
460 } | |
461 | |
462 /** | |
463 * Defines the schema for the {cache_*} bin tables. | |
464 * | |
465 * @internal | |
466 */ | |
467 public function schemaDefinition() { | |
468 $schema = [ | |
469 'description' => 'Storage for the cache API.', | |
470 'fields' => [ | |
471 'cid' => [ | |
472 'description' => 'Primary Key: Unique cache ID.', | |
473 'type' => 'varchar_ascii', | |
474 'length' => 255, | |
475 'not null' => TRUE, | |
476 'default' => '', | |
477 'binary' => TRUE, | |
478 ], | |
479 'data' => [ | |
480 'description' => 'A collection of data to cache.', | |
481 'type' => 'blob', | |
482 'not null' => FALSE, | |
483 'size' => 'big', | |
484 ], | |
485 'expire' => [ | |
486 'description' => 'A Unix timestamp indicating when the cache entry should expire, or ' . Cache::PERMANENT . ' for never.', | |
487 'type' => 'int', | |
488 'not null' => TRUE, | |
489 'default' => 0, | |
490 ], | |
491 'created' => [ | |
492 'description' => 'A timestamp with millisecond precision indicating when the cache entry was created.', | |
493 'type' => 'numeric', | |
494 'precision' => 14, | |
495 'scale' => 3, | |
496 'not null' => TRUE, | |
497 'default' => 0, | |
498 ], | |
499 'serialized' => [ | |
500 'description' => 'A flag to indicate whether content is serialized (1) or not (0).', | |
501 'type' => 'int', | |
502 'size' => 'small', | |
503 'not null' => TRUE, | |
504 'default' => 0, | |
505 ], | |
506 'tags' => [ | |
507 'description' => 'Space-separated list of cache tags for this entry.', | |
508 'type' => 'text', | |
509 'size' => 'big', | |
510 'not null' => FALSE, | |
511 ], | |
512 'checksum' => [ | |
513 'description' => 'The tag invalidation checksum when this entry was saved.', | |
514 'type' => 'varchar_ascii', | |
515 'length' => 255, | |
516 'not null' => TRUE, | |
517 ], | |
518 ], | |
519 'indexes' => [ | |
520 'expire' => ['expire'], | |
521 'created' => ['created'], | |
522 ], | |
523 'primary key' => ['cid'], | |
524 ]; | |
525 return $schema; | |
526 } | |
527 | |
528 /** | |
529 * The maximum number of rows that this cache bin table is allowed to store. | |
530 * | |
531 * @return int | |
532 */ | |
533 public function getMaxRows() { | |
534 return $this->maxRows; | |
535 } | |
536 | |
537 } |