Commit 11f55499 authored by catch's avatar catch

Issue #2432837 by Wim Leers, Fabianx: Make cache contexts hierarchical (e.g....

Issue #2432837 by Wim Leers, Fabianx: Make cache contexts hierarchical (e.g. 'user' is more specific than 'user.roles')
parent 8dbdbd76
......@@ -6,26 +6,86 @@ parameters:
factory.keyvalue.expirable:
default: keyvalue.expirable.database
services:
cache_factory:
class: Drupal\Core\Cache\CacheFactory
arguments: ['@settings', '%cache_default_bin_backends%']
calls:
- [setContainer, ['@service_container']]
cache_contexts:
class: Drupal\Core\Cache\CacheContexts
arguments: ['@service_container', '%cache_contexts%' ]
# Simple cache contexts, directly derived from the request context.
cache_context.ip:
class: Drupal\Core\Cache\IpCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
cache_context.headers:
class: Drupal\Core\Cache\HeadersCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
cache_context.cookies:
class: Drupal\Core\Cache\CookiesCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
cache_context.request_format:
class: Drupal\Core\Cache\RequestFormatCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
cache_context.url:
class: Drupal\Core\Cache\UrlCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context}
cache_context.pager:
class: Drupal\Core\Cache\PagerCacheContext
- { name: cache.context }
cache_context.url.host:
class: Drupal\Core\Cache\HostCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
cache_context.url.query_args:
class: Drupal\Core\Cache\QueryArgsCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
cache_context.url.query_args.pagers:
class: Drupal\Core\Cache\PagersCacheContext
arguments: ['@request_stack']
tags:
- { name: cache.context }
# Complex cache contexts, that depend on the routing system.
cache_context.route:
class: Drupal\Core\Cache\RouteCacheContext
arguments: ['@current_route_match']
tags:
- { name: cache.context }
cache_context.route.name:
class: Drupal\Core\Cache\RouteNameCacheContext
arguments: ['@current_route_match']
tags:
- { name: cache.context }
cache_context.route.menu_active_trails:
class: Drupal\Core\Cache\MenuActiveTrailsCacheContext
calls:
- [setContainer, ['@service_container']]
tags:
- { name: cache.context }
# Complex cache contexts, that may be calculated from a combination of
# multiple aspects of the request context plus additional logic. Hence they
# are their own roots.
cache_context.user:
class: Drupal\Core\Cache\UserCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context}
cache_context.user.roles:
class: Drupal\Core\Cache\UserRolesCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context}
cache_context.language:
class: Drupal\Core\Cache\LanguageCacheContext
cache_context.user.is_super_user:
class: Drupal\Core\Cache\IsSuperUserCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context}
cache_context.languages:
class: Drupal\Core\Cache\LanguagesCacheContext
arguments: ['@language_manager']
tags:
- { name: cache.context}
......@@ -38,12 +98,15 @@ services:
class: Drupal\Core\Cache\TimeZoneCacheContext
tags:
- { name: cache.context}
cache_context.menu.active_trail:
class: Drupal\Core\Cache\MenuActiveTrailCacheContext
cache_factory:
class: Drupal\Core\Cache\CacheFactory
arguments: ['@settings', '%cache_default_bin_backends%']
calls:
- [setContainer, ['@service_container']]
tags:
- { name: cache.context}
cache_contexts:
class: Drupal\Core\Cache\CacheContexts
arguments: ['@service_container', '%cache_contexts%' ]
cache_tags.invalidator:
parent: container.trait
class: Drupal\Core\Cache\CacheTagsInvalidator
......@@ -779,7 +842,7 @@ services:
- { name: event_subscriber }
main_content_renderer.html:
class: Drupal\Core\Render\MainContent\HtmlRenderer
arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer']
arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@cache_contexts']
tags:
- { name: render.main_content_renderer, format: html }
main_content_renderer.ajax:
......
......@@ -207,7 +207,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
$contexts = \Drupal::service("cache_contexts")->getLabels();
// Blocks are always rendered in the "per language" and "per theme" cache
// contexts. No need to show those options to the end user.
unset($contexts['language']);
unset($contexts['languages']);
unset($contexts['theme']);
$form['cache']['contexts'] = array(
'#type' => 'checkboxes',
......
......@@ -105,6 +105,7 @@ public function getLabels($include_calculated_cache_contexts = FALSE) {
* @throws \InvalidArgumentException
*/
public function convertTokensToKeys(array $context_tokens) {
$context_tokens = $this->optimizeTokens($context_tokens);
sort($context_tokens);
$keys = [];
foreach (static::parseTokens($context_tokens) as $context) {
......@@ -117,6 +118,69 @@ public function convertTokensToKeys(array $context_tokens) {
return $keys;
}
/**
* 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.
*
* E.g. when caching per user ('user'), also caching per role ('user.roles')
* is meaningless because "per role" is implied by "per user".
*
* 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) {
// 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;
}
// 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.
*
......
......@@ -28,6 +28,20 @@ public function process(ContainerBuilder $container) {
}
$cache_contexts[] = substr($id, 14);
}
// Validate.
sort($cache_contexts);
foreach ($cache_contexts as $id) {
// Validate the hierarchy of non-root-level cache contexts.
if (strpos($id, '.') !== FALSE) {
$parent = substr($id, 0, strrpos($id, '.'));
if (!in_array($parent, $cache_contexts)) {
throw new \InvalidArgumentException(sprintf('The service "%s" has an invalid service ID: the period indicates the hierarchy of cache contexts, therefore "%s" is considered the parent cache context, but no cache context service with that name was found.', $id, $parent));
}
}
}
$container->setParameter('cache_contexts', $cache_contexts);
}
......
......@@ -28,12 +28,13 @@ public static function getLabel();
* A cache context service's name is used as a token (placeholder) cache key,
* and is then replaced with the string returned by this method.
*
* @param string $parameter
* The parameter.
* @param string|null $parameter
* The parameter, or NULL to indicate all possible parameter values.
*
* @return string
* The string representation of the cache context.
* The string representation of the cache context. When $parameter is NULL,
* a value representing all possible parameters must be generated.
*/
public function getContext($parameter);
public function getContext($parameter = NULL);
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CookiesCacheContext.
*/
namespace Drupal\Core\Cache;
/**
* Defines the CookiesCacheContext service, for "per cookie" caching.
*/
class CookiesCacheContext extends RequestStackCacheContextBase implements CalculatedCacheContextInterface {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('HTTP cookies');
}
/**
* {@inheritdoc}
*/
public function getContext($cookie = NULL) {
if ($cookie === NULL) {
return $this->requestStack->getCurrentRequest()->cookies->all();
}
else {
return $this->requestStack->getCurrentRequest()->cookies->get($cookie);
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\HeadersCacheContext.
*/
namespace Drupal\Core\Cache;
/**
* Defines the HeadersCacheContext service, for "per header" caching.
*/
class HeadersCacheContext extends RequestStackCacheContextBase implements CalculatedCacheContextInterface {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('HTTP headers');
}
/**
* {@inheritdoc}
*/
public function getContext($header = NULL) {
if ($header === NULL) {
return $this->requestStack->getCurrentRequest()->headers->all();
}
else {
return $this->requestStack->getCurrentRequest()->headers->get($header);
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\HostCacheContext.
*/
namespace Drupal\Core\Cache;
/**
* Defines the HostCacheContext service, for "per host" caching.
*
* A "host" is defined as the combination of URI scheme, domain name and port.
*
* @see Symfony\Component\HttpFoundation::getSchemeAndHttpHost()
*/
class HostCacheContext extends RequestStackCacheContextBase {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Host');
}
/**
* {@inheritdoc}
*/
public function getContext() {
return $this->requestStack->getCurrentRequest()->getSchemeAndHttpHost();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\IpCacheContext.
*/
namespace Drupal\Core\Cache;
/**
* Defines the IpCacheContext service, for "per IP address" caching.
*/
class IpCacheContext extends RequestStackCacheContextBase {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('IP address');
}
/**
* {@inheritdoc}
*/
public function getContext() {
return $this->requestStack->getCurrentRequest()->getClientIp();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\IsSuperUserCacheContext.
*/
namespace Drupal\Core\Cache;
/**
* Defines the IsSuperUserCacheContext service, for "super user or not" caching.
*/
class IsSuperUserCacheContext extends UserCacheContext {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Is super user');
}
/**
* {@inheritdoc}
*/
public function getContext() {
return ((int) $this->user->id()) === 1 ? '1' : '0';
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Core\Cache\LanguageCacheContext.
* Contains \Drupal\Core\Cache\LanguagesCacheContext.
*/
namespace Drupal\Core\Cache;
......@@ -10,9 +10,9 @@
use Drupal\Core\Language\LanguageManagerInterface;
/**
* Defines the LanguageCacheContext service, for "per language" caching.
* Defines the LanguagesCacheContext service, for "per language" caching.
*/
class LanguageCacheContext implements CacheContextInterface {
class LanguagesCacheContext implements CalculatedCacheContextInterface {
/**
* The language manager.
......@@ -22,7 +22,7 @@ class LanguageCacheContext implements CacheContextInterface {
protected $languageManager;
/**
* Constructs a new LanguageCacheContext service.
* Constructs a new LanguagesCacheContext service.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
......@@ -40,18 +40,37 @@ public static function getLabel() {
/**
* {@inheritdoc}
*
* $type can be NULL, or one of the language types supported by the language
* manager, typically:
* - LanguageInterface::TYPE_INTERFACE
* - LanguageInterface::TYPE_CONTENT
* - LanguageInterface::TYPE_URL
*
* @see \Drupal\Core\Language\LanguageManagerInterface::getLanguageTypes()
*
* @throws \RuntimeException
* In case an invalid language type is specified.
*/
public function getContext() {
$context_parts = array();
if ($this->languageManager->isMultilingual()) {
foreach ($this->languageManager->getLanguageTypes() as $type) {
$context_parts[] = $this->languageManager->getCurrentLanguage($type)->getId();
public function getContext($type = NULL) {
if ($type === NULL) {
$context_parts = array();
if ($this->languageManager->isMultilingual()) {
foreach ($this->languageManager->getLanguageTypes() as $type) {
$context_parts[] = $this->languageManager->getCurrentLanguage($type)->getId();
}
}
else {
$context_parts[] = $this->languageManager->getCurrentLanguage()->getId();
}
return implode(',', $context_parts);
}
else {
$context_parts[] = $this->languageManager->getCurrentLanguage()->getId();
if (!in_array($type, $this->languageManager->getLanguageTypes())) {
throw new \RuntimeException(sprintf('The language type "%s" is invalid.', $type));
}
return $this->languageManager->getCurrentLanguage($type)->getId();
}
return implode(':', $context_parts);
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Core\Cache\MenuActiveTrailCacheContext.
* Contains \Drupal\Core\Cache\MenuActiveTrailsCacheContext.
*/
namespace Drupal\Core\Cache;
......@@ -10,12 +10,12 @@
use Symfony\Component\DependencyInjection\ContainerAware;
/**
* Defines the MenuActiveTrailCacheContext service.
* Defines the MenuActiveTrailsCacheContext service.
*
* This class is container-aware to avoid initializing the 'menu.active_trail'
* service (and its dependencies) when it is not necessary.
*/
class MenuActiveTrailCacheContext extends ContainerAware implements CalculatedCacheContextInterface {
class MenuActiveTrailsCacheContext extends ContainerAware implements CalculatedCacheContextInterface {
/**
* {@inheritdoc}
......@@ -27,7 +27,7 @@ public static function getLabel() {
/**
* {@inheritdoc}
*/
public function getContext($menu_name) {
public function getContext($menu_name = NULL) {
$active_trail = $this->container->get('menu.active_trail')
->getActiveTrailIds($menu_name);
return 'menu_trail.' . $menu_name . '|' . implode('|', $active_trail);
......
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\Core\Cache\PagerCacheContext.
* Contains \Drupal\Core\Cache\PagersCacheContext.
*/
namespace Drupal\Core\Cache;
......@@ -10,7 +10,7 @@
/**
* Defines a cache context for "per page in a pager" caching.
*/
class PagerCacheContext implements CalculatedCacheContextInterface {
class PagersCacheContext extends RequestStackCacheContextBase implements CalculatedCacheContextInterface {
/**
* {@inheritdoc}
......@@ -21,8 +21,16 @@ public static function getLabel() {
/**
* {@inheritdoc}
*
* @see pager_find_page()
*/
public function getContext($pager_id) {
public function getContext($pager_id = NULL) {
// The value of the 'page' query argument contains the information that
// controls *all* pagers.
if ($pager_id === NULL) {
return 'pager' . $this->requestStack->getCurrentRequest()->query->get('page', '');
}
return 'pager.' . $pager_id . '.' . pager_find_page($pager_id);
}
......
<?php
/**
* @file
* Contains \Drupal\Core\Cache\QueryArgsCacheContext.
*/
namespace Drupal\Core\Cache;
/**
* Defines the QueryArgsCacheContext service, for "per query args" caching.
*
* A "host" is defined as the combination of URI scheme, domain name and port.
*
* @see Symfony\Component\HttpFoundation::getSchemeAndHttpHost()
*/
class QueryArgsCacheContext extends RequestStackCacheContextBase implements CalculatedCacheContextInterface {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Query arguments');
}
/**
* {@inheritdoc}
*/
public function getContext($query_arg = NULL) {
if ($query_arg === NULL) {
return $this->requestStack->getCurrentRequest()->getQueryString();
}
else {
return $this->requestStack->getCurrentRequest()->query->get($query_arg);
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\RequestFormatCacheContext.
*/
namespace Drupal\Core\Cache;
/**
* Defines the RequestFormatCacheContext service, for "per format" caching.
*/
class RequestFormatCacheContext extends RequestStackCacheContextBase {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Request format');
}
/**
* {@inheritdoc}
*/
public function getContext() {
return $this->requestStack->getCurrentRequest()->getRequestFormat();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\RequestStackCacheContextBase.
*/
namespace Drupal\Core\Cache;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Defines a base class for cache contexts depending only on the request stack.
*/
abstract class RequestStackCacheContextBase implements CacheContextInterface {
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new RequestStackCacheContextBase class.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(RequestStack $request_stack) {
$this->requestStack = $request_stack;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Cache\RouteCacheContext.
*/
namespace Drupal\Core\Cache;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Defines the RouteCacheContext service, for "per route" caching.
*/
class RouteCacheContext {
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new RouteCacheContext class.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*/
public function __construct(RouteMatchInterface $route_match) {
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Route');
}
/**