PageRenderTime 30ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php

http://github.com/drupal/drupal
PHP | 325 lines | 128 code | 26 blank | 171 comment | 19 complexity | 7caa7c786129a0994f5fa7e43e91601f MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\Core\Cache\Context;
  3. use Drupal\Core\Cache\CacheableMetadata;
  4. use Symfony\Component\DependencyInjection\ContainerInterface;
  5. /**
  6. * Converts cache context tokens into cache keys.
  7. *
  8. * Uses cache context services (services tagged with 'cache.context', and whose
  9. * service ID has the 'cache_context.' prefix) to dynamically generate cache
  10. * keys based on the request context, thus allowing developers to express the
  11. * state by which should varied (the current URL, language, and so on).
  12. *
  13. * Note that this maps exactly to HTTP's Vary header semantics:
  14. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
  15. *
  16. * @see \Drupal\Core\Cache\Context\CacheContextInterface
  17. * @see \Drupal\Core\Cache\Context\CalculatedCacheContextInterface
  18. * @see \Drupal\Core\Cache\Context\CacheContextsPass
  19. */
  20. class CacheContextsManager {
  21. /**
  22. * The service container.
  23. *
  24. * @var \Symfony\Component\DependencyInjection\ContainerInterface
  25. */
  26. protected $container;
  27. /**
  28. * Available cache context IDs and corresponding labels.
  29. *
  30. * @var string[]
  31. */
  32. protected $contexts;
  33. /**
  34. * Constructs a CacheContextsManager object.
  35. *
  36. * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
  37. * The current service container.
  38. * @param string[] $contexts
  39. * An array of the available cache context IDs.
  40. */
  41. public function __construct(ContainerInterface $container, array $contexts) {
  42. $this->container = $container;
  43. $this->contexts = $contexts;
  44. }
  45. /**
  46. * Provides an array of available cache contexts.
  47. *
  48. * @return string[]
  49. * An array of available cache context IDs.
  50. */
  51. public function getAll() {
  52. return $this->contexts;
  53. }
  54. /**
  55. * Provides an array of available cache context labels.
  56. *
  57. * To be used in cache configuration forms.
  58. *
  59. * @param bool $include_calculated_cache_contexts
  60. * Whether to also return calculated cache contexts. Default to FALSE.
  61. *
  62. * @return array
  63. * An array of available cache contexts and corresponding labels.
  64. */
  65. public function getLabels($include_calculated_cache_contexts = FALSE) {
  66. $with_labels = [];
  67. foreach ($this->contexts as $context) {
  68. $service = $this->getService($context);
  69. if (!$include_calculated_cache_contexts && $service instanceof CalculatedCacheContextInterface) {
  70. continue;
  71. }
  72. $with_labels[$context] = $service->getLabel();
  73. }
  74. return $with_labels;
  75. }
  76. /**
  77. * Converts cache context tokens to cache keys.
  78. *
  79. * A cache context token is either:
  80. * - a cache context ID (if the service ID is 'cache_context.foo', then 'foo'
  81. * is a cache context ID); for example, 'foo'.
  82. * - a calculated cache context ID, followed by a colon, followed by
  83. * the parameter for the calculated cache context; for example,
  84. * 'bar:some_parameter'.
  85. *
  86. * @param string[] $context_tokens
  87. * An array of cache context tokens.
  88. *
  89. * @return \Drupal\Core\Cache\Context\ContextCacheKeys
  90. * The ContextCacheKeys object containing the converted cache keys and
  91. * cacheability metadata.
  92. */
  93. public function convertTokensToKeys(array $context_tokens) {
  94. assert($this->assertValidTokens($context_tokens));
  95. $cacheable_metadata = new CacheableMetadata();
  96. $optimized_tokens = $this->optimizeTokens($context_tokens);
  97. // Iterate over cache contexts that have been optimized away and get their
  98. // cacheability metadata.
  99. foreach (static::parseTokens(array_diff($context_tokens, $optimized_tokens)) as $context_token) {
  100. list($context_id, $parameter) = $context_token;
  101. $context = $this->getService($context_id);
  102. $cacheable_metadata = $cacheable_metadata->merge($context->getCacheableMetadata($parameter));
  103. }
  104. sort($optimized_tokens);
  105. $keys = [];
  106. foreach (array_combine($optimized_tokens, static::parseTokens($optimized_tokens)) as $context_token => $context) {
  107. list($context_id, $parameter) = $context;
  108. $keys[] = '[' . $context_token . ']=' . $this->getService($context_id)->getContext($parameter);
  109. }
  110. // Create the returned object and merge in the cacheability metadata.
  111. $context_cache_keys = new ContextCacheKeys($keys);
  112. return $context_cache_keys->merge($cacheable_metadata);
  113. }
  114. /**
  115. * Optimizes cache context tokens (the minimal representative subset).
  116. *
  117. * A minimal representative subset means that any cache context token in the
  118. * given set of cache context tokens that is a property of another cache
  119. * context cache context token in the set, is removed.
  120. *
  121. * Hence a minimal representative subset is the most compact representation
  122. * possible of a set of cache context tokens, that still captures the entire
  123. * universe of variations.
  124. *
  125. * If a cache context is being optimized away, it is able to set cacheable
  126. * metadata for itself which will be bubbled up.
  127. *
  128. * For example, when caching per user ('user'), also caching per role
  129. * ('user.roles') is meaningless because "per role" is implied by "per user".
  130. *
  131. * In the following examples, remember that the period indicates hierarchy and
  132. * the colon can be used to get a specific value of a calculated cache
  133. * context:
  134. * - ['a', 'a.b'] -> ['a']
  135. * - ['a', 'a.b.c'] -> ['a']
  136. * - ['a.b', 'a.b.c'] -> ['a.b']
  137. * - ['a', 'a.b', 'a.b.c'] -> ['a']
  138. * - ['x', 'x:foo'] -> ['x']
  139. * - ['a', 'a.b.c:bar'] -> ['a']
  140. *
  141. * @param string[] $context_tokens
  142. * A set of cache context tokens.
  143. *
  144. * @return string[]
  145. * A representative subset of the given set of cache context tokens..
  146. */
  147. public function optimizeTokens(array $context_tokens) {
  148. $optimized_content_tokens = [];
  149. foreach ($context_tokens as $context_token) {
  150. // Extract the parameter if available.
  151. $parameter = NULL;
  152. $context_id = $context_token;
  153. if (strpos($context_token, ':') !== FALSE) {
  154. list($context_id, $parameter) = explode(':', $context_token);
  155. }
  156. // Context tokens without:
  157. // - a period means they don't have a parent
  158. // - a colon means they're not a specific value of a cache context
  159. // hence no optimizations are possible.
  160. if (strpos($context_token, '.') === FALSE && strpos($context_token, ':') === FALSE) {
  161. $optimized_content_tokens[] = $context_token;
  162. }
  163. // Check cacheability. If the context defines a max-age of 0, then it
  164. // can not be optimized away. Pass the parameter along if we have one.
  165. elseif ($this->getService($context_id)->getCacheableMetadata($parameter)->getCacheMaxAge() === 0) {
  166. $optimized_content_tokens[] = $context_token;
  167. }
  168. // The context token has a period or a colon. Iterate over all ancestor
  169. // cache contexts. If one exists, omit the context token.
  170. else {
  171. $ancestor_found = FALSE;
  172. // Treat a colon like a period, that allows us to consider 'a' the
  173. // ancestor of 'a:foo', without any additional code for the colon.
  174. $ancestor = str_replace(':', '.', $context_token);
  175. do {
  176. $ancestor = substr($ancestor, 0, strrpos($ancestor, '.'));
  177. if (in_array($ancestor, $context_tokens)) {
  178. // An ancestor cache context is in $context_tokens, hence this cache
  179. // context is implied.
  180. $ancestor_found = TRUE;
  181. }
  182. } while (!$ancestor_found && strpos($ancestor, '.') !== FALSE);
  183. if (!$ancestor_found) {
  184. $optimized_content_tokens[] = $context_token;
  185. }
  186. }
  187. }
  188. return $optimized_content_tokens;
  189. }
  190. /**
  191. * Retrieves a cache context service from the container.
  192. *
  193. * @param string $context_id
  194. * The context ID, which together with the service ID prefix allows the
  195. * corresponding cache context service to be retrieved.
  196. *
  197. * @return \Drupal\Core\Cache\Context\CacheContextInterface
  198. * The requested cache context service.
  199. */
  200. protected function getService($context_id) {
  201. return $this->container->get('cache_context.' . $context_id);
  202. }
  203. /**
  204. * Parses cache context tokens into context IDs and optional parameters.
  205. *
  206. * @param string[] $context_tokens
  207. * An array of cache context tokens.
  208. *
  209. * @return array
  210. * An array with the parsed results, with each result being an array
  211. * containing:
  212. * - The cache context ID.
  213. * - The associated parameter (for a calculated cache context), or NULL if
  214. * there is no parameter.
  215. */
  216. public static function parseTokens(array $context_tokens) {
  217. $contexts_with_parameters = [];
  218. foreach ($context_tokens as $context) {
  219. $context_id = $context;
  220. $parameter = NULL;
  221. if (strpos($context, ':') !== FALSE) {
  222. list($context_id, $parameter) = explode(':', $context, 2);
  223. }
  224. $contexts_with_parameters[] = [$context_id, $parameter];
  225. }
  226. return $contexts_with_parameters;
  227. }
  228. /**
  229. * Validates an array of cache context tokens.
  230. *
  231. * Can be called before using cache contexts in operations, to check validity.
  232. *
  233. * @param string[] $context_tokens
  234. * An array of cache context tokens.
  235. *
  236. * @throws \LogicException
  237. *
  238. * @see \Drupal\Core\Cache\Context\CacheContextsManager::parseTokens()
  239. */
  240. public function validateTokens(array $context_tokens = []) {
  241. if (empty($context_tokens)) {
  242. return;
  243. }
  244. // Initialize the set of valid context tokens with the container's contexts.
  245. if (!isset($this->validContextTokens)) {
  246. $this->validContextTokens = array_flip($this->contexts);
  247. }
  248. foreach ($context_tokens as $context_token) {
  249. if (!is_string($context_token)) {
  250. throw new \LogicException(sprintf('Cache contexts must be strings, %s given.', gettype($context_token)));
  251. }
  252. if (isset($this->validContextTokens[$context_token])) {
  253. continue;
  254. }
  255. // If it's a valid context token, then the ID must be stored in the set
  256. // of valid context tokens (since we initialized it with the list of cache
  257. // context IDs using the container). In case of an invalid context token,
  258. // throw an exception, otherwise cache it, including the parameter, to
  259. // minimize the amount of work in future ::validateContexts() calls.
  260. $context_id = $context_token;
  261. $colon_pos = strpos($context_id, ':');
  262. if ($colon_pos !== FALSE) {
  263. $context_id = substr($context_id, 0, $colon_pos);
  264. }
  265. if (isset($this->validContextTokens[$context_id])) {
  266. $this->validContextTokens[$context_token] = TRUE;
  267. }
  268. else {
  269. throw new \LogicException(sprintf('"%s" is not a valid cache context ID.', $context_id));
  270. }
  271. }
  272. }
  273. /**
  274. * Asserts the context tokens are valid
  275. *
  276. * Similar to ::validateTokens, this method returns boolean TRUE when the
  277. * context tokens are valid, and FALSE when they are not instead of returning
  278. * NULL when they are valid and throwing a \LogicException when they are not.
  279. * This function should be used with the assert() statement.
  280. *
  281. * @param mixed $context_tokens
  282. * Variable to be examined - should be array of context_tokens.
  283. *
  284. * @return bool
  285. * TRUE if context_tokens is an array of valid tokens.
  286. */
  287. public function assertValidTokens($context_tokens) {
  288. if (!is_array($context_tokens)) {
  289. return FALSE;
  290. }
  291. try {
  292. $this->validateTokens($context_tokens);
  293. }
  294. catch (\LogicException $e) {
  295. return FALSE;
  296. }
  297. return TRUE;
  298. }
  299. }