3 namespace Drupal\Core\Cache\Context;
5 use Drupal\Core\Cache\CacheableMetadata;
6 use Symfony\Component\DependencyInjection\ContainerInterface;
9 * Converts cache context tokens into cache keys.
11 * Uses cache context services (services tagged with 'cache.context', and whose
12 * service ID has the 'cache_context.' prefix) to dynamically generate cache
13 * keys based on the request context, thus allowing developers to express the
14 * state by which should varied (the current URL, language, and so on).
16 * Note that this maps exactly to HTTP's Vary header semantics:
17 * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
19 * @see \Drupal\Core\Cache\Context\CacheContextInterface
20 * @see \Drupal\Core\Cache\Context\CalculatedCacheContextInterface
21 * @see \Drupal\Core\Cache\Context\CacheContextsPass
23 class CacheContextsManager {
26 * The service container.
28 * @var \Symfony\Component\DependencyInjection\ContainerInterface
33 * Available cache context IDs and corresponding labels.
40 * Constructs a CacheContextsManager object.
42 * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
43 * The current service container.
44 * @param string[] $contexts
45 * An array of the available cache context IDs.
47 public function __construct(ContainerInterface $container, array $contexts) {
48 $this->container = $container;
49 $this->contexts = $contexts;
53 * Provides an array of available cache contexts.
56 * An array of available cache context IDs.
58 public function getAll() {
59 return $this->contexts;
63 * Provides an array of available cache context labels.
65 * To be used in cache configuration forms.
67 * @param bool $include_calculated_cache_contexts
68 * Whether to also return calculated cache contexts. Default to FALSE.
71 * An array of available cache contexts and corresponding labels.
73 public function getLabels($include_calculated_cache_contexts = FALSE) {
75 foreach ($this->contexts as $context) {
76 $service = $this->getService($context);
77 if (!$include_calculated_cache_contexts && $service instanceof CalculatedCacheContextInterface) {
80 $with_labels[$context] = $service->getLabel();
86 * Converts cache context tokens to cache keys.
88 * A cache context token is either:
89 * - a cache context ID (if the service ID is 'cache_context.foo', then 'foo'
90 * is a cache context ID); for example, 'foo'.
91 * - a calculated cache context ID, followed by a colon, followed by
92 * the parameter for the calculated cache context; for example,
93 * 'bar:some_parameter'.
95 * @param string[] $context_tokens
96 * An array of cache context tokens.
98 * @return \Drupal\Core\Cache\Context\ContextCacheKeys
99 * The ContextCacheKeys object containing the converted cache keys and
100 * cacheability metadata.
102 public function convertTokensToKeys(array $context_tokens) {
103 assert('$this->assertValidTokens($context_tokens)');
104 $cacheable_metadata = new CacheableMetadata();
105 $optimized_tokens = $this->optimizeTokens($context_tokens);
106 // Iterate over cache contexts that have been optimized away and get their
107 // cacheability metadata.
108 foreach (static::parseTokens(array_diff($context_tokens, $optimized_tokens)) as $context_token) {
109 list($context_id, $parameter) = $context_token;
110 $context = $this->getService($context_id);
111 $cacheable_metadata = $cacheable_metadata->merge($context->getCacheableMetadata($parameter));
114 sort($optimized_tokens);
116 foreach (array_combine($optimized_tokens, static::parseTokens($optimized_tokens)) as $context_token => $context) {
117 list($context_id, $parameter) = $context;
118 $keys[] = '[' . $context_token . ']=' . $this->getService($context_id)->getContext($parameter);
121 // Create the returned object and merge in the cacheability metadata.
122 $context_cache_keys = new ContextCacheKeys($keys);
123 return $context_cache_keys->merge($cacheable_metadata);
127 * Optimizes cache context tokens (the minimal representative subset).
129 * A minimal representative subset means that any cache context token in the
130 * given set of cache context tokens that is a property of another cache
131 * context cache context token in the set, is removed.
133 * Hence a minimal representative subset is the most compact representation
134 * possible of a set of cache context tokens, that still captures the entire
135 * universe of variations.
137 * If a cache context is being optimized away, it is able to set cacheable
138 * metadata for itself which will be bubbled up.
140 * For example, when caching per user ('user'), also caching per role
141 * ('user.roles') is meaningless because "per role" is implied by "per user".
143 * In the following examples, remember that the period indicates hierarchy and
144 * the colon can be used to get a specific value of a calculated cache
146 * - ['a', 'a.b'] -> ['a']
147 * - ['a', 'a.b.c'] -> ['a']
148 * - ['a.b', 'a.b.c'] -> ['a.b']
149 * - ['a', 'a.b', 'a.b.c'] -> ['a']
150 * - ['x', 'x:foo'] -> ['x']
151 * - ['a', 'a.b.c:bar'] -> ['a']
153 * @param string[] $context_tokens
154 * A set of cache context tokens.
157 * A representative subset of the given set of cache context tokens..
159 public function optimizeTokens(array $context_tokens) {
160 $optimized_content_tokens = [];
161 foreach ($context_tokens as $context_token) {
163 // Extract the parameter if available.
165 $context_id = $context_token;
166 if (strpos($context_token, ':') !== FALSE) {
167 list($context_id, $parameter) = explode(':', $context_token);
170 // Context tokens without:
171 // - a period means they don't have a parent
172 // - a colon means they're not a specific value of a cache context
173 // hence no optimizations are possible.
174 if (strpos($context_token, '.') === FALSE && strpos($context_token, ':') === FALSE) {
175 $optimized_content_tokens[] = $context_token;
177 // Check cacheability. If the context defines a max-age of 0, then it
178 // can not be optimized away. Pass the parameter along if we have one.
179 elseif ($this->getService($context_id)->getCacheableMetadata($parameter)->getCacheMaxAge() === 0) {
180 $optimized_content_tokens[] = $context_token;
182 // The context token has a period or a colon. Iterate over all ancestor
183 // cache contexts. If one exists, omit the context token.
185 $ancestor_found = FALSE;
186 // Treat a colon like a period, that allows us to consider 'a' the
187 // ancestor of 'a:foo', without any additional code for the colon.
188 $ancestor = str_replace(':', '.', $context_token);
190 $ancestor = substr($ancestor, 0, strrpos($ancestor, '.'));
191 if (in_array($ancestor, $context_tokens)) {
192 // An ancestor cache context is in $context_tokens, hence this cache
193 // context is implied.
194 $ancestor_found = TRUE;
197 } while (!$ancestor_found && strpos($ancestor, '.') !== FALSE);
198 if (!$ancestor_found) {
199 $optimized_content_tokens[] = $context_token;
203 return $optimized_content_tokens;
207 * Retrieves a cache context service from the container.
209 * @param string $context_id
210 * The context ID, which together with the service ID prefix allows the
211 * corresponding cache context service to be retrieved.
213 * @return \Drupal\Core\Cache\Context\CacheContextInterface
214 * The requested cache context service.
216 protected function getService($context_id) {
217 return $this->container->get('cache_context.' . $context_id);
221 * Parses cache context tokens into context IDs and optional parameters.
223 * @param string[] $context_tokens
224 * An array of cache context tokens.
227 * An array with the parsed results, with each result being an array
229 * - The cache context ID.
230 * - The associated parameter (for a calculated cache context), or NULL if
231 * there is no parameter.
233 public static function parseTokens(array $context_tokens) {
234 $contexts_with_parameters = [];
235 foreach ($context_tokens as $context) {
236 $context_id = $context;
238 if (strpos($context, ':') !== FALSE) {
239 list($context_id, $parameter) = explode(':', $context, 2);
241 $contexts_with_parameters[] = [$context_id, $parameter];
243 return $contexts_with_parameters;
247 * Validates an array of cache context tokens.
249 * Can be called before using cache contexts in operations, to check validity.
251 * @param string[] $context_tokens
252 * An array of cache context tokens.
254 * @throws \LogicException
256 * @see \Drupal\Core\Cache\Context\CacheContextsManager::parseTokens()
258 public function validateTokens(array $context_tokens = []) {
259 if (empty($context_tokens)) {
263 // Initialize the set of valid context tokens with the container's contexts.
264 if (!isset($this->validContextTokens)) {
265 $this->validContextTokens = array_flip($this->contexts);
268 foreach ($context_tokens as $context_token) {
269 if (!is_string($context_token)) {
270 throw new \LogicException(sprintf('Cache contexts must be strings, %s given.', gettype($context_token)));
273 if (isset($this->validContextTokens[$context_token])) {
277 // If it's a valid context token, then the ID must be stored in the set
278 // of valid context tokens (since we initialized it with the list of cache
279 // context IDs using the container). In case of an invalid context token,
280 // throw an exception, otherwise cache it, including the parameter, to
281 // minimize the amount of work in future ::validateContexts() calls.
282 $context_id = $context_token;
283 $colon_pos = strpos($context_id, ':');
284 if ($colon_pos !== FALSE) {
285 $context_id = substr($context_id, 0, $colon_pos);
287 if (isset($this->validContextTokens[$context_id])) {
288 $this->validContextTokens[$context_token] = TRUE;
291 throw new \LogicException(sprintf('"%s" is not a valid cache context ID.', $context_id));
297 * Asserts the context tokens are valid
299 * Similar to ::validateTokens, this method returns boolean TRUE when the
300 * context tokens are valid, and FALSE when they are not instead of returning
301 * NULL when they are valid and throwing a \LogicException when they are not.
302 * This function should be used with the assert() statement.
304 * @param mixed $context_tokens
305 * Variable to be examined - should be array of context_tokens.
308 * TRUE if context_tokens is an array of valid tokens.
310 public function assertValidTokens($context_tokens) {
311 if (!is_array($context_tokens)) {
316 $this->validateTokens($context_tokens);
318 catch (\LogicException $e) {