056f838c04a4e87e745bcba06c94dab55422017c
[yaffs-website] / web / core / lib / Drupal / Core / Cache / Context / CacheContextsManager.php
1 <?php
2
3 namespace Drupal\Core\Cache\Context;
4
5 use Drupal\Core\Cache\CacheableMetadata;
6 use Symfony\Component\DependencyInjection\ContainerInterface;
7
8 /**
9  * Converts cache context tokens into cache keys.
10  *
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).
15  *
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
18  *
19  * @see \Drupal\Core\Cache\Context\CacheContextInterface
20  * @see \Drupal\Core\Cache\Context\CalculatedCacheContextInterface
21  * @see \Drupal\Core\Cache\Context\CacheContextsPass
22  */
23 class CacheContextsManager {
24
25   /**
26    * The service container.
27    *
28    * @var \Symfony\Component\DependencyInjection\ContainerInterface
29    */
30   protected $container;
31
32   /**
33    * Available cache context IDs and corresponding labels.
34    *
35    * @var string[]
36    */
37   protected $contexts;
38
39   /**
40    * Constructs a CacheContextsManager object.
41    *
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.
46    */
47   public function __construct(ContainerInterface $container, array $contexts) {
48     $this->container = $container;
49     $this->contexts = $contexts;
50   }
51
52   /**
53    * Provides an array of available cache contexts.
54    *
55    * @return string[]
56    *   An array of available cache context IDs.
57    */
58   public function getAll() {
59     return $this->contexts;
60   }
61
62   /**
63    * Provides an array of available cache context labels.
64    *
65    * To be used in cache configuration forms.
66    *
67    * @param bool $include_calculated_cache_contexts
68    *   Whether to also return calculated cache contexts. Default to FALSE.
69    *
70    * @return array
71    *   An array of available cache contexts and corresponding labels.
72    */
73   public function getLabels($include_calculated_cache_contexts = FALSE) {
74     $with_labels = [];
75     foreach ($this->contexts as $context) {
76       $service = $this->getService($context);
77       if (!$include_calculated_cache_contexts && $service instanceof CalculatedCacheContextInterface) {
78         continue;
79       }
80       $with_labels[$context] = $service->getLabel();
81     }
82     return $with_labels;
83   }
84
85   /**
86    * Converts cache context tokens to cache keys.
87    *
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'.
94    *
95    * @param string[] $context_tokens
96    *   An array of cache context tokens.
97    *
98    * @return \Drupal\Core\Cache\Context\ContextCacheKeys
99    *   The ContextCacheKeys object containing the converted cache keys and
100    *   cacheability metadata.
101    */
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));
112     }
113
114     sort($optimized_tokens);
115     $keys = [];
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);
119     }
120
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);
124   }
125
126   /**
127    * Optimizes cache context tokens (the minimal representative subset).
128    *
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.
132    *
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.
136    *
137    * If a cache context is being optimized away, it is able to set cacheable
138    * metadata for itself which will be bubbled up.
139    *
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".
142    *
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
145    * context:
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']
152    *
153    * @param string[] $context_tokens
154    *   A set of cache context tokens.
155    *
156    * @return string[]
157    *   A representative subset of the given set of cache context tokens..
158    */
159   public function optimizeTokens(array $context_tokens) {
160     $optimized_content_tokens = [];
161     foreach ($context_tokens as $context_token) {
162
163       // Extract the parameter if available.
164       $parameter = NULL;
165       $context_id = $context_token;
166       if (strpos($context_token, ':') !== FALSE) {
167         list($context_id, $parameter) = explode(':', $context_token);
168       }
169
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;
176       }
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;
181       }
182       // The context token has a period or a colon. Iterate over all ancestor
183       // cache contexts. If one exists, omit the context token.
184       else {
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);
189         do {
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;
195           }
196
197         } while (!$ancestor_found && strpos($ancestor, '.') !== FALSE);
198         if (!$ancestor_found) {
199           $optimized_content_tokens[] = $context_token;
200         }
201       }
202     }
203     return $optimized_content_tokens;
204   }
205
206   /**
207    * Retrieves a cache context service from the container.
208    *
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.
212    *
213    * @return \Drupal\Core\Cache\Context\CacheContextInterface
214    *   The requested cache context service.
215    */
216   protected function getService($context_id) {
217     return $this->container->get('cache_context.' . $context_id);
218   }
219
220   /**
221    * Parses cache context tokens into context IDs and optional parameters.
222    *
223    * @param string[] $context_tokens
224    *   An array of cache context tokens.
225    *
226    * @return array
227    *   An array with the parsed results, with each result being an array
228    *   containing:
229    *   - The cache context ID.
230    *   - The associated parameter (for a calculated cache context), or NULL if
231    *     there is no parameter.
232    */
233   public static function parseTokens(array $context_tokens) {
234     $contexts_with_parameters = [];
235     foreach ($context_tokens as $context) {
236       $context_id = $context;
237       $parameter = NULL;
238       if (strpos($context, ':') !== FALSE) {
239         list($context_id, $parameter) = explode(':', $context, 2);
240       }
241       $contexts_with_parameters[] = [$context_id, $parameter];
242     }
243     return $contexts_with_parameters;
244   }
245
246   /**
247    * Validates an array of cache context tokens.
248    *
249    * Can be called before using cache contexts in operations, to check validity.
250    *
251    * @param string[] $context_tokens
252    *   An array of cache context tokens.
253    *
254    * @throws \LogicException
255    *
256    * @see \Drupal\Core\Cache\Context\CacheContextsManager::parseTokens()
257    */
258   public function validateTokens(array $context_tokens = []) {
259     if (empty($context_tokens)) {
260       return;
261     }
262
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);
266     }
267
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)));
271       }
272
273       if (isset($this->validContextTokens[$context_token])) {
274         continue;
275       }
276
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);
286       }
287       if (isset($this->validContextTokens[$context_id])) {
288         $this->validContextTokens[$context_token] = TRUE;
289       }
290       else {
291         throw new \LogicException(sprintf('"%s" is not a valid cache context ID.', $context_id));
292       }
293     }
294   }
295
296   /**
297    * Asserts the context tokens are valid
298    *
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.
303    *
304    * @param mixed $context_tokens
305    *   Variable to be examined - should be array of context_tokens.
306    *
307    * @return bool
308    *   TRUE if context_tokens is an array of valid tokens.
309    */
310   public function assertValidTokens($context_tokens) {
311     if (!is_array($context_tokens)) {
312       return FALSE;
313     }
314
315     try {
316       $this->validateTokens($context_tokens);
317     }
318     catch (\LogicException $e) {
319       return FALSE;
320     }
321
322     return TRUE;
323   }
324
325 }