diff --git a/core/core.services.yml b/core/core.services.yml index 969b0e0..1de4ce9 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -4,6 +4,9 @@ services: arguments: ['@settings'] calls: - [setContainer, ['@service_container']] + cache_contexts: + class: Drupal\Core\Cache\CacheContexts + arguments: ['@service_container', '%cache_contexts%' ] cache.backend.database: class: Drupal\Core\Cache\DatabaseBackendFactory arguments: ['@database'] @@ -640,6 +643,11 @@ services: date: class: Drupal\Core\Datetime\Date arguments: ['@entity.manager', '@language_manager', '@string_translation', '@config.factory'] + http.request_cache_contexts: + class: Drupal\Core\Http\RequestCacheContexts + arguments: ['@request'] + tags: + - { name: cache.context} feed.bridge.reader: class: Drupal\Component\Bridge\ZfExtensionManagerSfContainer calls: diff --git a/core/includes/common.inc b/core/includes/common.inc index 0f47bb2..524e304 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -10,7 +10,6 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheableHelper; use Drupal\Core\Language\Language; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; @@ -4536,8 +4535,7 @@ function drupal_cache_tags_page_get(Response $response) { */ function drupal_render_cache_by_query($query, $function, $expire = Cache::PERMANENT, $granularity = NULL) { $cache_keys = array_merge(array($function), drupal_render_cid_parts($granularity)); - $query->preExecute(); - $cache_keys[] = hash('sha256', serialize(array((string) $query, $query->getArguments()))); + $cache_keys[] = Cache::keyFromQuery($query); return array( '#query' => $query, '#pre_render' => array($function . '_pre_render'), @@ -4615,8 +4613,8 @@ function drupal_render_cid_create($elements) { } elseif (isset($elements['#cache']['keys'])) { // Add cache context keys when constants are used in the 'keys' parameter. - $cacheable_helper = new CacheableHelper; - $keys = $cacheable_helper->addCacheContextsToKeys($elements['#cache']['keys']); + $cacheable_helper = \Drupal::service("cache_contexts"); + $keys = $cacheable_helper->addContextsToKeys($elements['#cache']['keys']); $granularity = isset($elements['#cache']['granularity']) ? $elements['#cache']['granularity'] : NULL; // Merge in additional cache ID parts based provided by drupal_render_cid_parts(). diff --git a/core/lib/Drupal/Core/Cache/Cache.php b/core/lib/Drupal/Core/Cache/Cache.php index c2f5fe6..25e15bb 100644 --- a/core/lib/Drupal/Core/Cache/Cache.php +++ b/core/lib/Drupal/Core/Cache/Cache.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Cache; +use Drupal\Core\Database\Query\SelectInterface; + /** * Helper methods for cache. */ @@ -70,4 +72,19 @@ public static function getBins() { return $bins; } + /** + * Generates a hash from a query object, to be used as part of the cache key + * + * @param $query + * A select query object. + * + * @return string + * A hash of the query arguments. + */ + public static function keyFromQuery(SelectInterface $query) { + $query->preExecute(); + $keys = array((string) $query, $query->getArguments()); + return hash('sha256', serialize($keys)); + } + } diff --git a/core/lib/Drupal/Core/Cache/CacheContexts.php b/core/lib/Drupal/Core/Cache/CacheContexts.php new file mode 100644 index 0000000..433bed9 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheContexts.php @@ -0,0 +1,123 @@ + array( + * "service" => "provider_service_id", + * "callback" => "getSomeContext", + * ) + * ) + * + */ + public function __construct(ContainerInterface $container, array $contexts) { + $this->container = $container; + $this->contexts = $contexts; + } + + /** + * Provides an array of available cache contexts. + * + * @return array + * An array of available cache contexts. + */ + public function getAll() { + return array_keys($this->contexts); + } + + /** + * Provides an array of available cache context labels, to be used used in a + * cache configuration form. + * + * @return array + * An array of available cache context labels. + */ + public function getLabels() { + $with_labels = array(); + foreach ($this->contexts as $name => $params) { + $with_labels[$name] = $params["label"]; + } + return $with_labels; + } + + /** + * Converts cache contexts to string representations of the context. + * + * @param $keys + * An array of cache keys that may or may not contain cache contexts. + * @return array + * A copy of the input, with cache contexts converted. + */ + public function addContextsToKeys($keys) { + $context_keys = array_intersect($keys, $this->getAll()); + $new_keys = $keys; + + // Iterate over the indices instead of the values so that the order of the + // cache keys are preserved. + foreach (array_keys($context_keys) as $index) { + $new_keys[$index] = $this->getContext($keys[$index]); + } + return $new_keys; + } + + /** + * Provides the string representaton of a cache context. + * + * @todo Document this properly once the input arguments are decided on, + * assuming the reuse of existing cache constants is temporary. + * + * @return string + * The string representaton of a cache context. + */ + protected function getContext($context) { + $context_info = $this->contexts[$context]; + return call_user_func($this->getCallable($context_info)); + } + + /** + * @param array $context + * @return callable + * A callable that returns the resolved cache context. + */ + protected function getCallable($context) { + return array( + $this->getService($context["service"]), + $context["callback"] + ); + } + + /** + * Retrieves a service from the container. + * + * @param string $service + * The ID of the service to retrieve. + * @return mixed + * The specified service. + */ + protected function getService($service) { + return $this->container->get($service); + } + +} diff --git a/core/lib/Drupal/Core/Cache/CacheContextsPass.php b/core/lib/Drupal/Core/Cache/CacheContextsPass.php new file mode 100644 index 0000000..d5f87f0 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/CacheContextsPass.php @@ -0,0 +1,34 @@ +findTaggedServiceIds('cache.context') as $service_name => $attrs) { + $service = $container->getDefinition($service_name); + $class = $service->getClass(); + foreach ($class::getCacheContexts() as $context => $params) { + $cache_contexts[$context] = $params + array("service" => $service_name); + } + } + $container->setParameter('cache_contexts', $cache_contexts); + } +} diff --git a/core/lib/Drupal/Core/Cache/CacheableHelper.php b/core/lib/Drupal/Core/Cache/CacheableHelper.php deleted file mode 100644 index 88cc9af..0000000 --- a/core/lib/Drupal/Core/Cache/CacheableHelper.php +++ /dev/null @@ -1,91 +0,0 @@ -preExecute(); - $keys = array((string) $query, $query->getArguments()); - return hash('sha256', serialize($keys)); - } - - /** - * Converts cache contexts to string representations of the context. - * - * @param $keys - * An array of cache keys that may or may not contain cache contexts. - * @return array - * A copy of the input, with cache contexts converted. - */ - function addCacheContextsToKeys($keys) { - $keys_with_contexts = array(); - foreach ($keys as $key) { - $keys_with_contexts[] = $this->getContext($key) ?: $key; - } - return $keys_with_contexts; - } - - /** - * Provides the string representaton of a cache context. - * - * @todo Document this properly once the input arguments are decided on, - * assuming the reuse of existing cache constants is temporary. - * - * @return string - * The string representaton of a cache context. - */ - protected function getContext($context) { - switch ($context) { - case DRUPAL_CACHE_PER_PAGE: - // @todo: Make this use the request properly. - return $this->currentPath(); - case DRUPAL_CACHE_PER_USER: - return "u." . $this->currentUser()->id(); - case DRUPAL_CACHE_PER_ROLE: - return 'r.' . implode(',', $this->currentUser()->getRoles()); - default: - return FALSE; - } - } - - /** - * @return \Drupal\Core\Session\AccountInterface - * The current user. - */ - protected function currentUser() { - return \Drupal::currentUser(); - } - - /** - * @return string - * The current path. - */ - protected function currentPath() { - global $base_root; - return $base_root . request_uri(); - } - -} diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 847960c..1de7b19 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -8,6 +8,7 @@ namespace Drupal\Core; use Drupal\Core\Cache\ListCacheBinsPass; +use Drupal\Core\Cache\CacheContextsPass; use Drupal\Core\DependencyInjection\ServiceProviderInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass; @@ -70,6 +71,7 @@ public function register(ContainerBuilder $container) { $container->addCompilerPass(new RegisterPathProcessorsPass()); $container->addCompilerPass(new RegisterRouteProcessorsPass()); $container->addCompilerPass(new ListCacheBinsPass()); + $container->addCompilerPass(new CacheContextsPass()); // Add the compiler pass for appending string translators. $container->addCompilerPass(new RegisterStringTranslatorsPass()); // Add the compiler pass that will process the tagged breadcrumb builder diff --git a/core/lib/Drupal/Core/Http/RequestCacheContexts.php b/core/lib/Drupal/Core/Http/RequestCacheContexts.php new file mode 100644 index 0000000..fada2c7 --- /dev/null +++ b/core/lib/Drupal/Core/Http/RequestCacheContexts.php @@ -0,0 +1,36 @@ +request = $request; + } + + static function getCacheContexts() { + return array( + "http.request_path" => array( + "callback" => "getCurrentPath", + "label" => "Request path", + ), + ); + } + + /** + * @return string + * The current path. + */ + public function getCurrentPath() { + // @todo: Make this use the request. + global $base_root; + return $base_root . request_uri(); + } + +} diff --git a/core/modules/block/lib/Drupal/block/BlockBase.php b/core/modules/block/lib/Drupal/block/BlockBase.php index 9d03d08..e17d295 100644 --- a/core/modules/block/lib/Drupal/block/BlockBase.php +++ b/core/modules/block/lib/Drupal/block/BlockBase.php @@ -62,6 +62,7 @@ protected function baseConfigurationDefaults() { 'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE, 'cache' => array( 'max_age' => 0, + 'cache_contexts' => array(), ), ); } @@ -130,12 +131,27 @@ public function buildConfigurationForm(array $form, array &$form_state) { $period = array_map('format_interval', array_combine($period, $period)); $period[0] = '<' . t('no caching') . '>'; $period[\Drupal\Core\Cache\Cache::PERMANENT] = t('Forever'); + $form['cache'] = array( + '#type' => 'fieldset', + '#title' => t('Cache'), + ); $form['cache']['max_age'] = array( '#type' => 'select', - '#title' => t('Cache: Maximum age'), + '#title' => t('Maximum age'), '#description' => t('The maximum time a block can be cached.'), '#default_value' => $this->configuration['cache']['max_age'], '#options' => $period, + '#fieldset' => 'cache', + ); + $cache_contexts = \Drupal::service("cache_contexts"); + $form['cache']['cache_contexts'] = array( + '#type' => 'select', + '#multiple' => true, + '#title' => t('Keys'), + '#description' => t('Additional keys to vary the cache by.'), + '#default_value' => $this->configuration['cache']['cache_contexts'], + '#options' => $cache_contexts->getLabels(), + '#fieldset' => 'cache', ); // Add plugin-specific settings for this block type. $form += $this->blockForm($form, $form_state); @@ -256,4 +272,10 @@ public function isCacheable() { return $this->configuration['cache']['max_age'] > 0; } + /** + */ + public function getCacheContexts() { + return $this->configuration['cache']['cache_contexts']; + } + } diff --git a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php index 6fe8abc..2554e7d 100644 --- a/core/modules/block/lib/Drupal/block/BlockViewBuilder.php +++ b/core/modules/block/lib/Drupal/block/BlockViewBuilder.php @@ -85,7 +85,11 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la $default_cache_keys = array('entity_view', 'block', $entity->id(), $entity->langcode); $max_age = $plugin->getCacheMaxAge(); $build[$entity_id]['#cache'] += array( - 'keys' => array_merge($default_cache_keys, $plugin->getCacheKeys()), + 'keys' => array_merge( + $default_cache_keys, + $plugin->getCacheKeys(), + $plugin->getCacheContexts() + ), 'bin' => $plugin->getCacheBin(), 'expire' => ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : REQUEST_TIME + $max_age, ); diff --git a/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php b/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php index d088b23..972d2a8 100644 --- a/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php +++ b/core/modules/forum/lib/Drupal/forum/Plugin/Block/ForumBlockBase.php @@ -9,7 +9,7 @@ use Drupal\block\BlockBase; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Cache\CacheableHelper; +use Drupal\Core\Cache\Cache; /** * Provides a base class for Forum blocks. @@ -84,8 +84,7 @@ public function blockSubmit($form, &$form_state) { * {@inheritdoc} */ public function getCacheKeys() { - $cacheable_helper = new CacheableHelper; - return array($cacheable_helper->cacheKeyFromQuery($this->buildForumQuery())); + return array(Cache::keyFromQuery($this->buildForumQuery())); } } diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php index dc5b8da..89a859d 100644 --- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php +++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemHelpBlock.php @@ -124,9 +124,9 @@ public function buildConfigurationForm(array $form, array &$form_state) { $form = parent::buildConfigurationForm($form, $form_state); // The help block is never cacheable, because it is path-specific. - $form['cache']['max_age']['#disabled'] = TRUE; + $form['cache']['#disabled'] = TRUE; + $form['cache']['#description'] = t('This block is never cacheable, it is not configurable.'); $form['cache']['max_age']['#value'] = 0; - $form['cache']['max_age']['#description'] = t('This block is never cacheable, it is not configurable.'); return $form; } diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php index b683373..666b515 100644 --- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php +++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMainBlock.php @@ -35,9 +35,9 @@ public function buildConfigurationForm(array $form, array &$form_state) { $form = parent::buildConfigurationForm($form, $form_state); // The main content block is never cacheable, because it may be dynamic. - $form['cache']['max_age']['#disabled'] = TRUE; + $form['cache']['#disabled'] = TRUE; + $form['cache']['#description'] = t('This block is never cacheable, it is not configurable.'); $form['cache']['max_age']['#value'] = 0; - $form['cache']['max_age']['#description'] = t('This block is never cacheable, it is not configurable.'); return $form; } diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php index 4b603bf..b0878f8 100644 --- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php +++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemPoweredByBlock.php @@ -36,9 +36,9 @@ public function buildConfigurationForm(array $form, array &$form_state) { // The 'Powered by Drupal' block is permanently cacheable, because its // contents can never change. - $form['cache']['max_age']['#disabled'] = TRUE; + $form['cache']['#disabled'] = TRUE; $form['cache']['max_age']['#value'] = Cache::PERMANENT; - $form['cache']['max_age']['#description'] = t('This block is always cached forever, it is not configurable.'); + $form['cache']['#description'] = t('This block is always cached forever, it is not configurable.'); return $form; } diff --git a/core/modules/user/lib/Drupal/user/CacheContexts.php b/core/modules/user/lib/Drupal/user/CacheContexts.php new file mode 100644 index 0000000..8ee8789 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/CacheContexts.php @@ -0,0 +1,38 @@ +user = $user; + } + + static function getCacheContexts() { + return array( + "user.current_user" => array( + "callback" => "getCurrentUser", + "label" => "Current user id", + ), + "user.current_user_roles" => array( + "callback" => "getCurrentUserRoles", + "label" => "Current user's roles", + ), + ); + } + + public function getCurrentUser() { + return "u." . $this->user()->id(); + } + + public function getCurrentUserRoles() { + return 'r.' . implode(',', $this->user->getRoles()); + } + +} diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml index 23d8706..e444cf9 100644 --- a/core/modules/user/user.services.yml +++ b/core/modules/user/user.services.yml @@ -15,6 +15,11 @@ services: class: Drupal\user\Access\LoginStatusCheck tags: - { name: access_check, applies_to: _user_is_logged_in } + user.cache.contexts: + class: Drupal\user\CacheContexts + arguments: ['@current_user'] + tags: + - { name: cache.context} user.data: class: Drupal\user\UserData arguments: ['@database'] diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php new file mode 100644 index 0000000..f1bc656 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Cache/CacheContextsTest.php @@ -0,0 +1,81 @@ + 'CacheContext test', + 'description' => 'Tests cache contexts.', + 'group' => 'Cache', + ); + } + + public function testContextPlaceholdersAreReplaced() { + $container = $this->getMockContainer(); + $container->expects($this->once()) + ->method("get") + ->with("some_service") + ->will($this->returnValue(new SomeClass)); + + $cache_contexts = new CacheContexts($container, $this->getContextsFixture()); + + $new_keys = $cache_contexts->addContextsToKeys( + array("non-cache-context", "somemodule.some_context") + ); + + $expected = array("non-cache-context", "foo"); + $this->assertEquals($expected, $new_keys); + } + + public function testAvailableContextStrings() { + $cache_contexts = new CacheContexts($this->getMockContainer(), $this->getContextsFixture()); + $contexts = $cache_contexts->getAll(); + $this->assertEquals(array("somemodule.some_context"), $contexts); + } + + public function testAvailableContextLabels() { + $cache_contexts = new CacheContexts($this->getMockContainer(), $this->getContextsFixture()); + $labels = $cache_contexts->getLabels(); + $expected = array("somemodule.some_context" => "Some label"); + $this->assertEquals($expected, $labels); + } + + protected function getContextsFixture() { + return array( + "somemodule.some_context" => array( + "service" => "some_service", + "callback" => "someMethod", + "label" => "Some label", + ) + ); + } + + protected function getMockContainer() { + return $this->getMockBuilder('Drupal\Core\DependencyInjection\Container') + ->disableOriginalConstructor() + ->getMock(); + } +}