PageRenderTime 25ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/core/lib/Drupal/Core/Cache/DatabaseBackend.php

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