123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- <?php
- namespace Drupal\Core\Cache\Context;
- use Drupal\Core\Cache\CacheableMetadata;
- use Symfony\Component\DependencyInjection\ContainerInterface;
- /**
- * Converts cache context tokens into cache keys.
- *
- * Uses cache context services (services tagged with 'cache.context', and whose
- * service ID has the 'cache_context.' prefix) to dynamically generate cache
- * keys based on the request context, thus allowing developers to express the
- * state by which should varied (the current URL, language, and so on).
- *
- * Note that this maps exactly to HTTP's Vary header semantics:
- * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
- *
- * @see \Drupal\Core\Cache\Context\CacheContextInterface
- * @see \Drupal\Core\Cache\Context\CalculatedCacheContextInterface
- * @see \Drupal\Core\Cache\Context\CacheContextsPass
- */
- class CacheContextsManager {
- /**
- * The service container.
- *
- * @var \Symfony\Component\DependencyInjection\ContainerInterface
- */
- protected $container;
- /**
- * Available cache context IDs and corresponding labels.
- *
- * @var string[]
- */
- protected $contexts;
- /**
- * Constructs a CacheContextsManager object.
- *
- * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
- * The current service container.
- * @param string[] $contexts
- * An array of the available cache context IDs.
- */
- public function __construct(ContainerInterface $container, array $contexts) {
- $this->container = $container;
- $this->contexts = $contexts;
- }
- /**
- * Provides an array of available cache contexts.
- *
- * @return string[]
- * An array of available cache context IDs.
- */
- public function getAll() {
- return $this->contexts;
- }
- /**
- * Provides an array of available cache context labels.
- *
- * To be used in cache configuration forms.
- *
- * @param bool $include_calculated_cache_contexts
- * Whether to also return calculated cache contexts. Default to FALSE.
- *
- * @return array
- * An array of available cache contexts and corresponding labels.
- */
- public function getLabels($include_calculated_cache_contexts = FALSE) {
- $with_labels = [];
- foreach ($this->contexts as $context) {
- $service = $this->getService($context);
- if (!$include_calculated_cache_contexts && $service instanceof CalculatedCacheContextInterface) {
- continue;
- }
- $with_labels[$context] = $service->getLabel();
- }
- return $with_labels;
- }
- /**
- * Converts cache context tokens to cache keys.
- *
- * A cache context token is either:
- * - a cache context ID (if the service ID is 'cache_context.foo', then 'foo'
- * is a cache context ID); for example, 'foo'.
- * - a calculated cache context ID, followed by a colon, followed by
- * the parameter for the calculated cache context; for example,
- * 'bar:some_parameter'.
- *
- * @param string[] $context_tokens
- * An array of cache context tokens.
- *
- * @return \Drupal\Core\Cache\Context\ContextCacheKeys
- * The ContextCacheKeys object containing the converted cache keys and
- * cacheability metadata.
- */
- public function convertTokensToKeys(array $context_tokens) {
- assert($this->assertValidTokens($context_tokens));
- $cacheable_metadata = new CacheableMetadata();
- $optimized_tokens = $this->optimizeTokens($context_tokens);
- // Iterate over cache contexts that have been optimized away and get their
- // cacheability metadata.
- foreach (static::parseTokens(array_diff($context_tokens, $optimized_tokens)) as $context_token) {
- list($context_id, $parameter) = $context_token;
- $context = $this->getService($context_id);
- $cacheable_metadata = $cacheable_metadata->merge($context->getCacheableMetadata($parameter));
- }
- sort($optimized_tokens);
- $keys = [];
- foreach (array_combine($optimized_tokens, static::parseTokens($optimized_tokens)) as $context_token => $context) {
- list($context_id, $parameter) = $context;
- $keys[] = '[' . $context_token . ']=' . $this->getService($context_id)->getContext($parameter);
- }
- // Create the returned object and merge in the cacheability metadata.
- $context_cache_keys = new ContextCacheKeys($keys);
- return $context_cache_keys->merge($cacheable_metadata);
- }
- /**
- * Optimizes cache context tokens (the minimal representative subset).
- *
- * A minimal representative subset means that any cache context token in the
- * given set of cache context tokens that is a property of another cache
- * context cache context token in the set, is removed.
- *
- * Hence a minimal representative subset is the most compact representation
- * possible of a set of cache context tokens, that still captures the entire
- * universe of variations.
- *
- * If a cache context is being optimized away, it is able to set cacheable
- * metadata for itself which will be bubbled up.
- *
- * For example, when caching per user ('user'), also caching per role
- * ('user.roles') is meaningless because "per role" is implied by "per user".
- *
- * In the following examples, remember that the period indicates hierarchy and
- * the colon can be used to get a specific value of a calculated cache
- * context:
- * - ['a', 'a.b'] -> ['a']
- * - ['a', 'a.b.c'] -> ['a']
- * - ['a.b', 'a.b.c'] -> ['a.b']
- * - ['a', 'a.b', 'a.b.c'] -> ['a']
- * - ['x', 'x:foo'] -> ['x']
- * - ['a', 'a.b.c:bar'] -> ['a']
- *
- * @param string[] $context_tokens
- * A set of cache context tokens.
- *
- * @return string[]
- * A representative subset of the given set of cache context tokens..
- */
- public function optimizeTokens(array $context_tokens) {
- $optimized_content_tokens = [];
- foreach ($context_tokens as $context_token) {
- // Extract the parameter if available.
- $parameter = NULL;
- $context_id = $context_token;
- if (strpos($context_token, ':') !== FALSE) {
- list($context_id, $parameter) = explode(':', $context_token);
- }
- // Context tokens without:
- // - a period means they don't have a parent
- // - a colon means they're not a specific value of a cache context
- // hence no optimizations are possible.
- if (strpos($context_token, '.') === FALSE && strpos($context_token, ':') === FALSE) {
- $optimized_content_tokens[] = $context_token;
- }
- // Check cacheability. If the context defines a max-age of 0, then it
- // can not be optimized away. Pass the parameter along if we have one.
- elseif ($this->getService($context_id)->getCacheableMetadata($parameter)->getCacheMaxAge() === 0) {
- $optimized_content_tokens[] = $context_token;
- }
- // The context token has a period or a colon. Iterate over all ancestor
- // cache contexts. If one exists, omit the context token.
- else {
- $ancestor_found = FALSE;
- // Treat a colon like a period, that allows us to consider 'a' the
- // ancestor of 'a:foo', without any additional code for the colon.
- $ancestor = str_replace(':', '.', $context_token);
- do {
- $ancestor = substr($ancestor, 0, strrpos($ancestor, '.'));
- if (in_array($ancestor, $context_tokens)) {
- // An ancestor cache context is in $context_tokens, hence this cache
- // context is implied.
- $ancestor_found = TRUE;
- }
- } while (!$ancestor_found && strpos($ancestor, '.') !== FALSE);
- if (!$ancestor_found) {
- $optimized_content_tokens[] = $context_token;
- }
- }
- }
- return $optimized_content_tokens;
- }
- /**
- * Retrieves a cache context service from the container.
- *
- * @param string $context_id
- * The context ID, which together with the service ID prefix allows the
- * corresponding cache context service to be retrieved.
- *
- * @return \Drupal\Core\Cache\Context\CacheContextInterface
- * The requested cache context service.
- */
- protected function getService($context_id) {
- return $this->container->get('cache_context.' . $context_id);
- }
- /**
- * Parses cache context tokens into context IDs and optional parameters.
- *
- * @param string[] $context_tokens
- * An array of cache context tokens.
- *
- * @return array
- * An array with the parsed results, with each result being an array
- * containing:
- * - The cache context ID.
- * - The associated parameter (for a calculated cache context), or NULL if
- * there is no parameter.
- */
- public static function parseTokens(array $context_tokens) {
- $contexts_with_parameters = [];
- foreach ($context_tokens as $context) {
- $context_id = $context;
- $parameter = NULL;
- if (strpos($context, ':') !== FALSE) {
- list($context_id, $parameter) = explode(':', $context, 2);
- }
- $contexts_with_parameters[] = [$context_id, $parameter];
- }
- return $contexts_with_parameters;
- }
- /**
- * Validates an array of cache context tokens.
- *
- * Can be called before using cache contexts in operations, to check validity.
- *
- * @param string[] $context_tokens
- * An array of cache context tokens.
- *
- * @throws \LogicException
- *
- * @see \Drupal\Core\Cache\Context\CacheContextsManager::parseTokens()
- */
- public function validateTokens(array $context_tokens = []) {
- if (empty($context_tokens)) {
- return;
- }
- // Initialize the set of valid context tokens with the container's contexts.
- if (!isset($this->validContextTokens)) {
- $this->validContextTokens = array_flip($this->contexts);
- }
- foreach ($context_tokens as $context_token) {
- if (!is_string($context_token)) {
- throw new \LogicException(sprintf('Cache contexts must be strings, %s given.', gettype($context_token)));
- }
- if (isset($this->validContextTokens[$context_token])) {
- continue;
- }
- // If it's a valid context token, then the ID must be stored in the set
- // of valid context tokens (since we initialized it with the list of cache
- // context IDs using the container). In case of an invalid context token,
- // throw an exception, otherwise cache it, including the parameter, to
- // minimize the amount of work in future ::validateContexts() calls.
- $context_id = $context_token;
- $colon_pos = strpos($context_id, ':');
- if ($colon_pos !== FALSE) {
- $context_id = substr($context_id, 0, $colon_pos);
- }
- if (isset($this->validContextTokens[$context_id])) {
- $this->validContextTokens[$context_token] = TRUE;
- }
- else {
- throw new \LogicException(sprintf('"%s" is not a valid cache context ID.', $context_id));
- }
- }
- }
- /**
- * Asserts the context tokens are valid
- *
- * Similar to ::validateTokens, this method returns boolean TRUE when the
- * context tokens are valid, and FALSE when they are not instead of returning
- * NULL when they are valid and throwing a \LogicException when they are not.
- * This function should be used with the assert() statement.
- *
- * @param mixed $context_tokens
- * Variable to be examined - should be array of context_tokens.
- *
- * @return bool
- * TRUE if context_tokens is an array of valid tokens.
- */
- public function assertValidTokens($context_tokens) {
- if (!is_array($context_tokens)) {
- return FALSE;
- }
- try {
- $this->validateTokens($context_tokens);
- }
- catch (\LogicException $e) {
- return FALSE;
- }
- return TRUE;
- }
- }
|