DatabaseCacheTagsChecksum.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <?php
  2. namespace Drupal\Core\Cache;
  3. use Drupal\Core\Database\Connection;
  4. use Drupal\Core\Database\SchemaObjectExistsException;
  5. /**
  6. * Cache tags invalidations checksum implementation that uses the database.
  7. */
  8. class DatabaseCacheTagsChecksum implements CacheTagsChecksumInterface, CacheTagsInvalidatorInterface {
  9. /**
  10. * The database connection.
  11. *
  12. * @var \Drupal\Core\Database\Connection
  13. */
  14. protected $connection;
  15. /**
  16. * Contains already loaded cache invalidations from the database.
  17. *
  18. * @var array
  19. */
  20. protected $tagCache = [];
  21. /**
  22. * A list of tags that have already been invalidated in this request.
  23. *
  24. * Used to prevent the invalidation of the same cache tag multiple times.
  25. *
  26. * @var array
  27. */
  28. protected $invalidatedTags = [];
  29. /**
  30. * Constructs a DatabaseCacheTagsChecksum object.
  31. *
  32. * @param \Drupal\Core\Database\Connection $connection
  33. * The database connection.
  34. */
  35. public function __construct(Connection $connection) {
  36. $this->connection = $connection;
  37. }
  38. /**
  39. * {@inheritdoc}
  40. */
  41. public function invalidateTags(array $tags) {
  42. try {
  43. foreach ($tags as $tag) {
  44. // Only invalidate tags once per request unless they are written again.
  45. if (isset($this->invalidatedTags[$tag])) {
  46. continue;
  47. }
  48. $this->invalidatedTags[$tag] = TRUE;
  49. unset($this->tagCache[$tag]);
  50. $this->connection->merge('cachetags')
  51. ->insertFields(['invalidations' => 1])
  52. ->expression('invalidations', 'invalidations + 1')
  53. ->key('tag', $tag)
  54. ->execute();
  55. }
  56. }
  57. catch (\Exception $e) {
  58. // Create the cache table, which will be empty. This fixes cases during
  59. // core install where cache tags are invalidated before the table is
  60. // created.
  61. if (!$this->ensureTableExists()) {
  62. $this->catchException($e);
  63. }
  64. }
  65. }
  66. /**
  67. * {@inheritdoc}
  68. */
  69. public function getCurrentChecksum(array $tags) {
  70. // Remove tags that were already invalidated during this request from the
  71. // static caches so that another invalidation can occur later in the same
  72. // request. Without that, written cache items would not be invalidated
  73. // correctly.
  74. foreach ($tags as $tag) {
  75. unset($this->invalidatedTags[$tag]);
  76. }
  77. return $this->calculateChecksum($tags);
  78. }
  79. /**
  80. * {@inheritdoc}
  81. */
  82. public function isValid($checksum, array $tags) {
  83. return $checksum == $this->calculateChecksum($tags);
  84. }
  85. /**
  86. * Calculates the current checksum for a given set of tags.
  87. *
  88. * @param array $tags
  89. * The array of tags to calculate the checksum for.
  90. *
  91. * @return int
  92. * The calculated checksum.
  93. */
  94. protected function calculateChecksum(array $tags) {
  95. $checksum = 0;
  96. $query_tags = array_diff($tags, array_keys($this->tagCache));
  97. if ($query_tags) {
  98. $db_tags = [];
  99. try {
  100. $db_tags = $this->connection->query('SELECT tag, invalidations FROM {cachetags} WHERE tag IN ( :tags[] )', [':tags[]' => $query_tags])
  101. ->fetchAllKeyed();
  102. $this->tagCache += $db_tags;
  103. }
  104. catch (\Exception $e) {
  105. // If the table does not exist yet, create.
  106. if (!$this->ensureTableExists()) {
  107. $this->catchException($e);
  108. }
  109. }
  110. // Fill static cache with empty objects for tags not found in the database.
  111. $this->tagCache += array_fill_keys(array_diff($query_tags, array_keys($db_tags)), 0);
  112. }
  113. foreach ($tags as $tag) {
  114. $checksum += $this->tagCache[$tag];
  115. }
  116. return $checksum;
  117. }
  118. /**
  119. * {@inheritdoc}
  120. */
  121. public function reset() {
  122. $this->tagCache = [];
  123. $this->invalidatedTags = [];
  124. }
  125. /**
  126. * Check if the cache tags table exists and create it if not.
  127. */
  128. protected function ensureTableExists() {
  129. try {
  130. $database_schema = $this->connection->schema();
  131. // Create the cache tags table if it does not exist.
  132. if (!$database_schema->tableExists('cachetags')) {
  133. $schema_definition = $this->schemaDefinition();
  134. $database_schema->createTable('cachetags', $schema_definition);
  135. return TRUE;
  136. }
  137. }
  138. // If another process has already created the cachetags table, attempting to
  139. // recreate it will throw an exception. In this case just catch the
  140. // exception and do nothing.
  141. catch (SchemaObjectExistsException $e) {
  142. return TRUE;
  143. }
  144. return FALSE;
  145. }
  146. /**
  147. * Defines the schema for the {cachetags} table.
  148. *
  149. * @internal
  150. */
  151. public function schemaDefinition() {
  152. $schema = [
  153. 'description' => 'Cache table for tracking cache tag invalidations.',
  154. 'fields' => [
  155. 'tag' => [
  156. 'description' => 'Namespace-prefixed tag string.',
  157. 'type' => 'varchar_ascii',
  158. 'length' => 255,
  159. 'not null' => TRUE,
  160. 'default' => '',
  161. ],
  162. 'invalidations' => [
  163. 'description' => 'Number incremented when the tag is invalidated.',
  164. 'type' => 'int',
  165. 'not null' => TRUE,
  166. 'default' => 0,
  167. ],
  168. ],
  169. 'primary key' => ['tag'],
  170. ];
  171. return $schema;
  172. }
  173. /**
  174. * Act on an exception when cache might be stale.
  175. *
  176. * If the {cachetags} table does not yet exist, that's fine but if the table
  177. * exists and yet the query failed, then the cache is stale and the
  178. * exception needs to propagate.
  179. *
  180. * @param \Exception $e
  181. * The exception.
  182. *
  183. * @throws \Exception
  184. */
  185. protected function catchException(\Exception $e) {
  186. if ($this->connection->schema()->tableExists('cachetags')) {
  187. throw $e;
  188. }
  189. }
  190. }