You are here

class CacheContextsManager in Drupal 10

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php \Drupal\Core\Cache\Context\CacheContextsManager
  2. 9 core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php \Drupal\Core\Cache\Context\CacheContextsManager

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

Hierarchy

Expanded class hierarchy of CacheContextsManager

See also

\Drupal\Core\Cache\Context\CacheContextInterface

\Drupal\Core\Cache\Context\CalculatedCacheContextInterface

\Drupal\Core\Cache\Context\CacheContextsPass

27 files declare their use of CacheContextsManager
AccessManagerTest.php in core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php
Contains \Drupal\Tests\Core\Access\AccessManagerTest.
ActiveWorkspaceCheckTest.php in core/modules/workspaces/tests/src/Unit/ActiveWorkspaceCheckTest.php
BreadcrumbManagerTest.php in core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php
CacheContextsManagerTest.php in core/tests/Drupal/Tests/Core/Cache/Context/CacheContextsManagerTest.php
Contains \Drupal\Tests\Core\Cache\Context\CacheContextsManagerTest.
CacheTest.php in core/tests/Drupal/Tests/Core/Cache/CacheTest.php

... See full list

1 string reference to 'CacheContextsManager'
core.services.yml in core/core.services.yml
core/core.services.yml
1 service uses CacheContextsManager
cache_contexts_manager in core/core.services.yml
Drupal\Core\Cache\Context\CacheContextsManager

File

core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php, line 23

Namespace

Drupal\Core\Cache\Context
View source
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) {
      [
        $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) {
      [
        $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) {
        [
          $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;
      }
      elseif ($this
        ->getService($context_id)
        ->getCacheableMetadata($parameter)
        ->getCacheMaxAge() === 0) {
        $optimized_content_tokens[] = $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) {
        [
          $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;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
CacheContextsManager::$container protected property The service container.
CacheContextsManager::$contexts protected property Available cache context IDs and corresponding labels.
CacheContextsManager::assertValidTokens public function Asserts the context tokens are valid.
CacheContextsManager::convertTokensToKeys public function Converts cache context tokens to cache keys.
CacheContextsManager::getAll public function Provides an array of available cache contexts.
CacheContextsManager::getLabels public function Provides an array of available cache context labels.
CacheContextsManager::getService protected function Retrieves a cache context service from the container.
CacheContextsManager::optimizeTokens public function Optimizes cache context tokens (the minimal representative subset).
CacheContextsManager::parseTokens public static function Parses cache context tokens into context IDs and optional parameters.
CacheContextsManager::validateTokens public function Validates an array of cache context tokens.
CacheContextsManager::__construct public function Constructs a CacheContextsManager object.