Issue #2158003 by Wim Leers, msonnabaum, effulgentsia, moshe weitzman: Remove Block Cache API in favor of blocks returning #cache with cache tags.

8.0.x
catch 2014-03-22 22:00:38 +00:00
parent 99c739a05b
commit a6ffb28261
74 changed files with 1695 additions and 563 deletions

View File

@ -4,6 +4,24 @@ services:
arguments: ['@settings']
calls:
- [setContainer, ['@service_container']]
cache_contexts:
class: Drupal\Core\Cache\CacheContexts
arguments: ['@service_container', '%cache_contexts%' ]
cache_context.url:
class: Drupal\Core\Cache\UrlCacheContext
arguments: ['@request']
tags:
- { name: cache.context}
cache_context.language:
class: Drupal\Core\Cache\LanguageCacheContext
arguments: ['@language_manager']
tags:
- { name: cache.context}
cache_context.theme:
class: Drupal\Core\Cache\ThemeCacheContext
arguments: ['@request', '@theme.negotiator']
tags:
- { name: cache.context}
cache.backend.database:
class: Drupal\Core\Cache\DatabaseBackendFactory
arguments: ['@database']

View File

@ -900,6 +900,13 @@ function drupal_serve_page_from_cache(stdClass $cache, Response $response, Reque
// max-age > 0, allowing the page to be cached by external proxies, when a
// session cookie is present unless the Vary header has been replaced.
$max_age = !$request->cookies->has(session_name()) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0;
// RFC 2616, section 14.21 says: 'To mark a response as "never expires," an
// origin server sends an Expires date approximately one year from the time
// the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more
// than one year in the future.'
if ($max_age > 31536000 || $max_age === \Drupal\Core\Cache\Cache::PERMANENT) {
$max_age = 31536000;
}
$response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
// Entity tag should change if the output changes.

View File

@ -131,78 +131,6 @@ const JS_DEFAULT = 0;
*/
const JS_THEME = 100;
/**
* @defgroup block_caching Block Caching
* @{
* Constants that define each block's caching state.
*
* Modules specify how their blocks can be cached in their hook_block_info()
* implementations. Caching can be turned off (DRUPAL_NO_CACHE), managed by the
* module declaring the block (DRUPAL_CACHE_CUSTOM), or managed by the core
* Block module. If the Block module is managing the cache, you can specify that
* the block is the same for every page and user (DRUPAL_CACHE_GLOBAL), or that
* it can change depending on the page (DRUPAL_CACHE_PER_PAGE) or by user
* (DRUPAL_CACHE_PER_ROLE or DRUPAL_CACHE_PER_USER). Page and user settings can
* be combined with a bitwise-binary or operator; for example,
* DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE means that the block can change
* depending on the user role or page it is on.
*
* The block cache is cleared when the 'content' cache tag is invalidated,
* following the same pattern as the page cache (node, comment, user, taxonomy
* added or updated...).
*
* Note that user 1 is excluded from block caching.
*/
/**
* The block should not get cached.
*
* This setting should be used:
* - For simple blocks (notably those that do not perform any db query), where
* querying the db cache would be more expensive than directly generating the
* content.
* - For blocks that change too frequently.
*/
const DRUPAL_NO_CACHE = -1;
/**
* The block is handling its own caching in its hook_block_view().
*
* This setting is useful when time based expiration is needed or a site uses a
* node access which invalidates standard block cache.
*/
const DRUPAL_CACHE_CUSTOM = -2;
/**
* The block or element can change depending on the user's roles.
*
* This is the default setting for blocks, used when the block does not specify
* anything.
*/
const DRUPAL_CACHE_PER_ROLE = 0x0001;
/**
* The block or element can change depending on the user.
*
* This setting can be resource-consuming for sites with large number of users,
* and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient.
*/
const DRUPAL_CACHE_PER_USER = 0x0002;
/**
* The block or element can change depending on the page being viewed.
*/
const DRUPAL_CACHE_PER_PAGE = 0x0004;
/**
* The block or element is the same for every user and page that it is visible.
*/
const DRUPAL_CACHE_GLOBAL = 0x0008;
/**
* @} End of "defgroup block_caching".
*/
/**
* The delimiter used to split plural strings.
*
@ -3718,16 +3646,12 @@ function drupal_render_page($page) {
* associative array with one or several of the following keys:
* - 'keys': An array of one or more keys that identify the element. If
* 'keys' is set, the cache ID is created automatically from these keys.
* See drupal_render_cid_create().
* - 'granularity' (optional): Define the cache granularity using binary
* combinations of the cache granularity constants, e.g.
* DRUPAL_CACHE_PER_USER to cache for each user separately or
* DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE to cache separately for
* each page and role. If not specified the element is cached globally for
* each theme and language.
* Cache keys may either be static (just strings) or tokens (placeholders
* that are converted to static keys by the @cache_contexts service,
* depending on the request). See drupal_render_cid_create().
* - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is
* required. If 'cid' is set, 'keys' and 'granularity' are ignored. Use
* only if you have special requirements.
* required. If 'cid' is set, 'keys' is ignored. Use only if you have
* special requirements.
* - 'expire': Set to one of the cache lifetime constants.
* - 'bin': Specify a cache bin to cache the element in. Default is 'cache'.
* - If this element has #type defined and the default attributes for this
@ -4510,101 +4434,11 @@ function drupal_cache_tags_page_get(Response $response) {
return array();
}
/**
* Prepares an element for caching based on a query.
*
* This smart caching strategy saves Drupal from querying and rendering to HTML
* when the underlying query is unchanged.
*
* Expensive queries should use the query builder to create the query and then
* call this function. Executing the query and formatting results should happen
* in a #pre_render callback.
*
* @param $query
* A select query object as returned by db_select().
* @param $function
* The name of the function doing this caching. A _pre_render suffix will be
* added to this string and is also part of the cache key in
* drupal_render_cache_set() and drupal_render_cache_get().
* @param $expire
* The cache expire time, passed eventually to \Drupal::cache()->set().
* @param $granularity
* One or more granularity constants passed to drupal_render_cid_parts().
*
* @return
* A renderable array with the following keys and values:
* - #query: The passed-in $query.
* - #pre_render: $function with a _pre_render suffix.
* - #cache: An associative array prepared for drupal_render_cache_set().
*/
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())));
return array(
'#query' => $query,
'#pre_render' => array($function . '_pre_render'),
'#cache' => array(
'keys' => $cache_keys,
'expire' => $expire,
),
);
}
/**
* Returns cache ID parts for building a cache ID.
*
* @param $granularity
* One or more cache granularity constants. For example, to cache separately
* for each user, use DRUPAL_CACHE_PER_USER. To cache separately for each
* page and role, use the expression:
* @code
* DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE
* @endcode
*
* @return
* An array of cache ID parts, always containing the active theme. If the
* locale module is enabled it also contains the active language. If
* $granularity was passed in, more parts are added.
*/
function drupal_render_cid_parts($granularity = NULL) {
global $theme, $base_root;
$cid_parts[] = $theme;
// If we have only one language enabled we do not need it as cid part.
$language_manager = \Drupal::languageManager();
if ($language_manager->isMultilingual()) {
foreach ($language_manager->getLanguageTypes() as $type) {
$cid_parts[] = $language_manager->getCurrentLanguage($type)->id;
}
}
if (!empty($granularity)) {
// 'PER_ROLE' and 'PER_USER' are mutually exclusive. 'PER_USER' can be a
// resource drag for sites with many users, so when a module is being
// equivocal, we favor the less expensive 'PER_ROLE' pattern.
if ($granularity & DRUPAL_CACHE_PER_ROLE) {
$cid_parts[] = 'r.' . implode(',', \Drupal::currentUser()->getRoles());
}
elseif ($granularity & DRUPAL_CACHE_PER_USER) {
$cid_parts[] = 'u.' . \Drupal::currentUser()->id();
}
if ($granularity & DRUPAL_CACHE_PER_PAGE) {
$cid_parts[] = $base_root . request_uri();
}
}
return $cid_parts;
}
/**
* Creates the cache ID for a renderable element.
*
* This creates the cache ID string, either by returning the #cache['cid']
* property if present or by building the cache ID out of the #cache['keys']
* and, optionally, the #cache['granularity'] properties.
* property if present or by building the cache ID out of the #cache['keys'].
*
* @param $elements
* A renderable array.
@ -4617,10 +4451,12 @@ function drupal_render_cid_create($elements) {
return $elements['#cache']['cid'];
}
elseif (isset($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().
$cid_parts = array_merge($elements['#cache']['keys'], drupal_render_cid_parts($granularity));
return implode(':', $cid_parts);
// Cache keys may either be static (just strings) or tokens (placeholders
// that are converted to static keys by the @cache_contexts service,
// depending on the request).
$cache_contexts = \Drupal::service("cache_contexts");
$keys = $cache_contexts->convertTokensToKeys($elements['#cache']['keys']);
return implode(':', $keys);
}
return FALSE;
}

View File

@ -7,6 +7,8 @@
namespace Drupal\Core\Cache;
use Drupal\Core\Database\Query\SelectInterface;
/**
* Helper methods for cache.
*
@ -72,4 +74,26 @@ class Cache {
return $bins;
}
/**
* Generates a hash from a query object, to be used as part of the cache key.
*
* This smart caching strategy saves Drupal from querying and rendering to
* HTML when the underlying query is unchanged.
*
* Expensive queries should use the query builder to create the query and then
* call this function. Executing the query and formatting results should
* happen in a #pre_render callback.
*
* @param \Drupal\Core\Database\Query\SelectInterface $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));
}
}

View File

@ -21,7 +21,7 @@ interface CacheBackendInterface {
/**
* Indicates that the item should never be removed unless explicitly deleted.
*/
const CACHE_PERMANENT = 0;
const CACHE_PERMANENT = -1;
/**
* Returns data from the persistent cache.

View File

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheContextInterface.
*/
namespace Drupal\Core\Cache;
/**
* Provides an interface for defining a cache context service.
*/
interface CacheContextInterface {
/**
* Returns the label of the cache context.
*
* @return string
* The label of the cache context.
*/
public static function getLabel();
/**
* Returns the string representation of the cache context.
*
* 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.
*
* @return string
* The string representation of the cache context.
*/
public function getContext();
}

View File

@ -0,0 +1,127 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheContexts.
*/
namespace Drupal\Core\Cache;
use Drupal\Core\Database\Query\SelectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the CacheContexts service.
*
* Converts string placeholders into their final string values, to be used as a
* cache key.
*/
class CacheContexts {
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Available cache contexts and corresponding labels.
*
* @var array
*/
protected $contexts;
/**
* Constructs a CacheContexts object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The current service container.
* @param array $contexts
* An array of key-value pairs, where the keys are service names (which also
* serve as the corresponding cache context token) and the values are the
* cache context labels.
*/
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 $this->contexts;
}
/**
* Provides an array of available cache context labels.
*
* To be used in cache configuration forms.
*
* @return array
* An array of available cache contexts and corresponding labels.
*/
public function getLabels() {
$with_labels = array();
foreach ($this->contexts as $context) {
$with_labels[$context] = $this->getService($context)->getLabel();
}
return $with_labels;
}
/**
* Converts cache context tokens to string representations of the context.
*
* Cache keys may either be static (just strings) or tokens (placeholders
* that are converted to static keys by the @cache_contexts service, depending
* depending on the request). This is the default cache contexts service.
*
* @param array $keys
* An array of cache keys that may or may not contain cache context tokens.
*
* @return array
* A copy of the input, with cache context tokens converted.
*/
public function convertTokensToKeys(array $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.
*
* @param string $context
* A cache context token of an available cache context service.
*
* @return string
* The string representaton of a cache context.
*/
protected function getContext($context) {
return $this->getService($context)->getContext();
}
/**
* 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);
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\CacheContextsPass.
*/
namespace Drupal\Core\Cache;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds cache_contexts parameter to the container.
*/
class CacheContextsPass implements CompilerPassInterface {
/**
* Implements CompilerPassInterface::process().
*
* Collects the cache contexts into the cache_contexts parameter.
*/
public function process(ContainerBuilder $container) {
$cache_contexts = array_keys($container->findTaggedServiceIds('cache.context'));
$container->setParameter('cache_contexts', $cache_contexts);
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\LanguageCacheContext.
*/
namespace Drupal\Core\Cache;
use Drupal\Core\Language\LanguageManagerInterface;
/**
* Defines the LanguageCacheContext service, for "per language" caching.
*/
class LanguageCacheContext implements CacheContextInterface {
/**
* The language manager.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
*/
protected $languageManager;
/**
* Constructs a new LanguageCacheContext service.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(LanguageManagerInterface $language_manager) {
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Language');
}
/**
* {@inheritdoc}
*/
public function getContext() {
$context_parts = array();
if ($this->language_manager->isMultilingual()) {
foreach ($this->language_manager->getLanguageTypes() as $type) {
$context_parts[] = $this->language_manager->getCurrentLanguage($type)->id;
}
}
return implode(':', $context_parts);
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\LanguageCacheContext.
*/
namespace Drupal\Core\Cache;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Theme\ThemeNegotiatorInterface;
/**
* Defines the ThemeCacheContext service, for "per theme" caching.
*/
class ThemeCacheContext implements CacheContextInterface {
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The theme negotiator.
*
* @var \Drupal\Core\Theme\ThemeNegotiator
*/
protected $themeNegotiator;
/**
* Constructs a new ThemeCacheContext service.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param \Drupal\Core\Theme\ThemeNegotiatorInterface $theme_negotiator
* The theme negotiator.
*/
public function __construct(Request $request, ThemeNegotiatorInterface $theme_negotiator) {
$this->request = $request;
$this->themeNegotiator = $theme_negotiator;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('Theme');
}
/**
* {@inheritdoc}
*/
public function getContext() {
return $this->themeNegotiator->determineActiveTheme($this->request) ?: 'stark';
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* @file
* Contains \Drupal\Core\Cache\UrlCacheContext.
*/
namespace Drupal\Core\Cache;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines the UrlCacheContext service, for "per page" caching.
*/
class UrlCacheContext implements CacheContextInterface {
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* Constructs a new UrlCacheContext service.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
*/
public function __construct(Request $request) {
$this->request = $request;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('URL');
}
/**
* {@inheritdoc}
*/
public function getContext() {
return $this->request->getUri();
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\Core;
use Drupal\Core\Cache\CacheContextsPass;
use Drupal\Core\Cache\ListCacheBinsPass;
use Drupal\Core\Config\ConfigFactoryOverridePass;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
@ -72,6 +73,7 @@ class CoreServiceProvider implements ServiceProviderInterface {
$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

View File

@ -158,8 +158,14 @@ class EntityViewBuilder extends EntityControllerBase implements EntityController
// type configuration.
if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) {
$return['#cache'] += array(
'keys' => array('entity_view', $this->entityTypeId, $entity->id(), $view_mode),
'granularity' => DRUPAL_CACHE_PER_ROLE,
'keys' => array(
'entity_view',
$this->entityTypeId,
$entity->id(),
$view_mode,
'cache_context.theme',
'cache_context.user.roles',
),
'bin' => $this->cacheBin,
);

View File

@ -169,52 +169,16 @@ function block_page_build(&$page) {
function block_get_blocks_by_region($region) {
$build = array();
if ($list = block_list($region)) {
$build = _block_get_renderable_region($list);
}
return $build;
}
/**
* Gets an array of blocks suitable for drupal_render().
*
* @param $list
* A list of blocks such as that returned by block_list().
*
* @return
* A renderable array.
*/
function _block_get_renderable_region($list = array()) {
$build = array();
// Block caching is not compatible with node_access modules. We also
// preserve the submission of forms in blocks, by fetching from cache
// only if the request method is 'GET' (or 'HEAD'). User 1 being out of
// the regular 'roles define permissions' schema, it brings too many
// chances of having unwanted output get in the cache and later be served
// to other users. We therefore exclude user 1 from block caching.
$not_cacheable = \Drupal::currentUser()->id() == 1 ||
count(\Drupal::moduleHandler()->getImplementations('node_grants')) ||
!\Drupal::request()->isMethodSafe();
foreach ($list as $key => $block) {
$settings = $block->get('settings');
if ($not_cacheable || in_array($settings['cache'], array(DRUPAL_NO_CACHE, DRUPAL_CACHE_CUSTOM))) {
// Non-cached blocks get built immediately.
foreach ($list as $key => $block) {
if ($block->access()) {
$build[$key] = entity_view($block, 'block');
}
}
else {
$build[$key] = array(
'#block' => $block,
'#weight' => $block->get('weight'),
'#pre_render' => array('_block_get_renderable_block'),
'#cache' => array(
'keys' => array($key, $settings['module']),
'granularity' => $settings['cache'],
'bin' => 'block',
'tags' => array('content' => TRUE),
),
);
// If none of the blocks in this region are visible, then don't set anything
// else in the render array, because that would cause the region to show up.
if (!empty($build)) {
// block_list() already returned the blocks in sorted order.
$build['#sorted'] = TRUE;
}
}
return $build;
@ -342,9 +306,7 @@ function block_list($region) {
$blocks[$region] = array();
}
uasort($blocks[$region], function($first, $second) {
return $first->weight === $second->weight ? 0 : ($first->weight < $second->weight ? -1 : 1);
});
uasort($blocks[$region], 'Drupal\block\Entity\Block::sort');
return $blocks[$region];
}
@ -364,26 +326,6 @@ function block_load($entity_id) {
return entity_load('block', $entity_id);
}
/**
* Builds the content and label for a block.
*
* For cacheable blocks, this is called during #pre_render.
*
* @param $element
* A renderable array.
*
* @return
* A renderable array.
*/
function _block_get_renderable_block($element) {
$block = $element['#block'];
// Don't bother to build blocks that aren't accessible.
if ($element['#access'] = $block->access()) {
$element += entity_view($block, 'block');
}
return $element;
}
/**
* Implements hook_rebuild().
*/
@ -456,6 +398,11 @@ function template_preprocess_block(&$variables) {
$variables['derivative_plugin_id'] = $variables['elements']['#derivative_plugin_id'];
$variables['label'] = !empty($variables['configuration']['label_display']) ? $variables['configuration']['label'] : '';
$variables['content'] = $variables['elements']['content'];
// A block's label is configuration: it is static. Allow dynamic labels to be
// set in the render array.
if (isset($variables['elements']['content']['#title']) && !empty($variables['configuration']['label_display'])) {
$variables['label'] = $variables['elements']['content']['#title'];
}
$variables['attributes']['class'][] = 'block';
$variables['attributes']['class'][] = drupal_html_class('block-' . $variables['configuration']['module']);

View File

@ -73,8 +73,18 @@ block.block.*:
type: string
label: 'Display title'
cache:
type: integer
label: 'Cache'
type: mapping
label: 'Cache settings'
mapping:
max_age:
type: integer
label: 'Maximum age'
contexts:
type: sequence
label: 'Vary by context'
sequence:
- type: string
label: 'Context'
status:
type: boolean
label: 'Status'

View File

@ -93,6 +93,12 @@ class CustomBlockBlock extends BlockBase implements ContainerFactoryPluginInterf
'status' => TRUE,
'info' => '',
'view_mode' => 'full',
// Modify the default max age for custom block blocks: modifications made
// to them will automatically invalidate corresponding cache tags, thus
// allowing us to cache custom block blocks forever.
'cache' => array(
'max_age' => \Drupal\Core\Cache\Cache::PERMANENT,
),
);
}

View File

@ -10,7 +10,10 @@ namespace Drupal\block;
use Drupal\Core\Plugin\PluginBase;
use Drupal\block\BlockInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Language\Language;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableInterface;
use Drupal\Core\Session\AccountInterface;
/**
@ -28,12 +31,7 @@ abstract class BlockBase extends PluginBase implements BlockPluginInterface {
public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configuration += $this->defaultConfiguration() + array(
'label' => '',
'module' => $plugin_definition['module'],
'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
'cache' => DRUPAL_NO_CACHE,
);
$this->setConfiguration($configuration);
}
/**
@ -47,7 +45,29 @@ abstract class BlockBase extends PluginBase implements BlockPluginInterface {
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = $configuration;
$this->configuration = NestedArray::mergeDeep(
$this->baseConfigurationDefaults(),
$this->defaultConfiguration(),
$configuration
);
}
/**
* Returns generic default configuration for block plugins.
*
* @return array
* An associative array with the default configuration.
*/
protected function baseConfigurationDefaults() {
return array(
'label' => '',
'module' => $this->pluginDefinition['module'],
'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
'cache' => array(
'max_age' => 0,
'contexts' => array(),
),
);
}
/**
@ -108,10 +128,50 @@ abstract class BlockBase extends PluginBase implements BlockPluginInterface {
'#default_value' => ($this->configuration['label_display'] === BlockInterface::BLOCK_LABEL_VISIBLE),
'#return_value' => BlockInterface::BLOCK_LABEL_VISIBLE,
);
// Identical options to the ones for page caching.
// @see \Drupal\system\Form\PerformanceForm::buildForm()
$period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
$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' => 'value',
'#value' => $this->configuration['cache'],
'#type' => 'details',
'#title' => t('Cache settings'),
);
$form['cache']['max_age'] = array(
'#type' => 'select',
'#title' => t('Maximum age'),
'#description' => t('The maximum time this block may be cached.'),
'#default_value' => $this->configuration['cache']['max_age'],
'#options' => $period,
);
$contexts = \Drupal::service("cache_contexts")->getLabels();
// Blocks are always rendered in a "per theme" cache context. No need to
// show that option to the end user.
unset($contexts['cache_context.theme']);
$form['cache']['contexts'] = array(
'#type' => 'checkboxes',
'#title' => t('Vary by context'),
'#description' => t('The contexts this cached block must be varied by.'),
'#default_value' => $this->configuration['cache']['contexts'],
'#options' => $contexts,
'#states' => array(
'disabled' => array(
':input[name="settings[cache][max_age]"]' => array('value' => (string) 0),
),
),
);
if (count($this->getRequiredCacheContexts()) > 0) {
// Remove the required cache contexts from the list of contexts a user can
// choose to modify by: they must always be applied.
$context_labels = array();
foreach ($this->getRequiredCacheContexts() as $context) {
$context_labels[] = $form['cache']['contexts']['#options'][$context];
unset($form['cache']['contexts']['#options'][$context]);
}
$required_context_list = implode(', ', $context_labels);
$form['cache']['contexts']['#description'] .= ' ' . t('This block is <em>always</em> varied by the following contexts: %required-context-list.', array('%required-context-list' => $required_context_list));
}
// Add plugin-specific settings for this block type.
$form += $this->blockForm($form, $form_state);
@ -134,6 +194,9 @@ abstract class BlockBase extends PluginBase implements BlockPluginInterface {
* @see \Drupal\block\BlockBase::blockValidate()
*/
public function validateConfigurationForm(array &$form, array &$form_state) {
// Transform the #type = checkboxes value to a numerically indexed array.
$form_state['values']['cache']['contexts'] = array_values(array_filter($form_state['values']['cache']['contexts']));
$this->blockValidate($form, $form_state);
}
@ -156,6 +219,7 @@ abstract class BlockBase extends PluginBase implements BlockPluginInterface {
$this->configuration['label'] = $form_state['values']['label'];
$this->configuration['label_display'] = $form_state['values']['label_display'];
$this->configuration['module'] = $form_state['values']['module'];
$this->configuration['cache'] = $form_state['values']['cache'];
$this->blockSubmit($form, $form_state);
}
}
@ -189,4 +253,59 @@ abstract class BlockBase extends PluginBase implements BlockPluginInterface {
return $transliterated;
}
/**
* Returns the cache contexts required for this block.
*
* @return array
* The required cache contexts IDs.
*/
protected function getRequiredCacheContexts() {
return array();
}
/**
* {@inheritdoc}
*/
public function getCacheKeys() {
// Return the required cache contexts, merged with the user-configured cache
// contexts, if any.
return array_merge($this->getRequiredCacheContexts(), $this->configuration['cache']['contexts']);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
// If a block plugin's output changes, then it must be able to invalidate a
// cache tag that affects all instances of this block: across themes and
// across regions.
$block_plugin_cache_tag = str_replace(':', '__', $this->getPluginID());
return array('block_plugin' => array($block_plugin_cache_tag));
}
/**
* {@inheritdoc}
*/
public function getCacheBin() {
return 'block';
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return (int)$this->configuration['cache']['max_age'];
}
/**
* {@inheritdoc}
*/
public function isCacheable() {
// Similar to the page cache, a block is cacheable if it has a max age.
// Blocks that should never be cached can override this method to simply
// return FALSE.
$max_age = $this->getCacheMaxAge();
return $max_age === Cache::PERMANENT || $max_age > 0;
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\block;
use Drupal\Core\Cache\CacheableInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
@ -20,7 +21,7 @@ use Drupal\Core\Session\AccountInterface;
* brif references to the important components that are not coupled to the
* interface.
*/
interface BlockPluginInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface {
interface BlockPluginInterface extends ConfigurablePluginInterface, PluginFormInterface, PluginInspectionInterface, CacheableInterface {
/**
* Indicates whether the block should be shown.

View File

@ -7,7 +7,9 @@
namespace Drupal\block;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\String;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
@ -43,61 +45,126 @@ class BlockViewBuilder extends EntityViewBuilder {
$plugin_id = $plugin->getPluginId();
$base_id = $plugin->getBasePluginId();
$derivative_id = $plugin->getDerivativeId();
$configuration = $plugin->getConfiguration();
if ($content = $plugin->build()) {
$configuration = $plugin->getConfiguration();
// Create the render array for the block as a whole.
// @see template_preprocess_block().
$build[$key] = array(
'#theme' => 'block',
'#attributes' => array(),
'#contextual_links' => array(
'block' => array(
'route_parameters' => array('block' => $entity_id),
),
// Create the render array for the block as a whole.
// @see template_preprocess_block().
$build[$entity_id] = array(
'#theme' => 'block',
'#attributes' => array(),
// All blocks get a "Configure block" contextual link.
'#contextual_links' => array(
'block' => array(
'route_parameters' => array('block' => $entity->id()),
),
'#configuration' => $configuration,
'#plugin_id' => $plugin_id,
'#base_plugin_id' => $base_id,
'#derivative_plugin_id' => $derivative_id,
);
$build[$key]['#configuration']['label'] = String::checkPlain($configuration['label']);
),
'#weight' => $entity->get('weight'),
'#configuration' => $configuration,
'#plugin_id' => $plugin_id,
'#base_plugin_id' => $base_id,
'#derivative_plugin_id' => $derivative_id,
// @todo Remove after fixing http://drupal.org/node/1989568.
'#block' => $entity,
);
$build[$entity_id]['#configuration']['label'] = check_plain($configuration['label']);
// Place the $content returned by the block plugin into a 'content'
// child element, as a way to allow the plugin to have complete control
// of its properties and rendering (e.g., its own #theme) without
// conflicting with the properties used above, or alternate ones used
// by alternate block rendering approaches in contrib (e.g., Panels).
// However, the use of a child element is an implementation detail of
// this particular block rendering approach. Semantically, the content
// returned by the plugin "is the" block, and in particular,
// #attributes and #contextual_links is information about the *entire*
// block. Therefore, we must move these properties from $content and
// merge them into the top-level element.
foreach (array('#attributes', '#contextual_links') as $property) {
if (isset($content[$property])) {
$build[$key][$property] += $content[$property];
unset($content[$property]);
}
}
$build[$key]['content'] = $content;
// Set cache tags; these always need to be set, whether the block is
// cacheable or not, so that the page cache is correctly informed.
$default_cache_tags = array(
'content' => TRUE,
'block_view' => TRUE,
'block' => array($entity->id()),
);
$build[$entity_id]['#cache']['tags'] = NestedArray::mergeDeep($default_cache_tags, $plugin->getCacheTags());
if ($plugin->isCacheable()) {
$build[$entity_id]['#pre_render'][] = array($this, 'buildBlock');
// Generic cache keys, with the block plugin's custom keys appended
// (usually cache context keys like 'cache_context.user.roles').
$default_cache_keys = array(
'entity_view',
'block',
$entity->id(),
$entity->langcode,
// Blocks are always rendered in a "per theme" cache context.
'cache_context.theme',
);
$max_age = $plugin->getCacheMaxAge();
$build[$entity_id]['#cache'] += array(
'keys' => array_merge($default_cache_keys, $plugin->getCacheKeys()),
'bin' => $plugin->getCacheBin(),
'expire' => ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : REQUEST_TIME + $max_age,
);
}
else {
$build[$key] = array();
$build[$entity_id] = $this->buildBlock($build[$entity_id]);
}
$this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$key], $plugin);
// @todo Remove after fixing http://drupal.org/node/1989568.
$build[$key]['#block'] = $entity;
// Don't run in ::buildBlock() to ensure cache keys can be altered. If an
// alter hook wants to modify the block contents, it can append another
// #pre_render hook.
$this->moduleHandler()->alter(array('block_view', "block_view_$base_id"), $build[$entity_id], $plugin);
}
return $build;
}
/**
* #pre_render callback for building a block.
*
* Renders the content using the provided block plugin, and then:
* - if there is no content, aborts rendering, and makes sure the block won't
* be rendered.
* - if there is content, moves the contextual links from the block content to
* the block itself.
*/
public function buildBlock($build) {
$content = $build['#block']->getPlugin()->build();
if (!empty($content)) {
// Place the $content returned by the block plugin into a 'content' child
// element, as a way to allow the plugin to have complete control of its
// properties and rendering (e.g., its own #theme) without conflicting
// with the properties used above, or alternate ones used by alternate
// block rendering approaches in contrib (e.g., Panels). However, the use
// of a child element is an implementation detail of this particular block
// rendering approach. Semantically, the content returned by the plugin
// "is the" block, and in particular, #attributes and #contextual_links is
// information about the *entire* block. Therefore, we must move these
// properties from $content and merge them into the top-level element.
foreach (array('#attributes', '#contextual_links') as $property) {
if (isset($content[$property])) {
$build[$property] += $content[$property];
unset($content[$property]);
}
}
$build['content'] = $content;
}
else {
// Abort rendering: render as the empty string and ensure this block is
// render cached, so we can avoid the work of having to repeatedly
// determine whether the block is empty. E.g. modifying or adding entities
// could cause the block to no longer be empty.
$build = array(
'#markup' => '',
'#cache' => $build['#cache'],
);
}
return $build;
}
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) { }
public function resetCache(array $entities = NULL) {
if (isset($entities)) {
Cache::invalidateTags(array('block' => array_keys($entities)));
}
else {
Cache::invalidateTags(array('block_view' => TRUE));
}
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\block\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\block\BlockPluginBag;
use Drupal\block\BlockInterface;
@ -146,6 +147,32 @@ class Block extends ConfigEntityBase implements BlockInterface, EntityWithPlugin
return $properties;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
parent::postSave($storage_controller, $update);
if ($update) {
Cache::invalidateTags(array('block' => $this->id()));
}
// When placing a new block, invalidate all cache entries for this theme,
// since any page that uses this theme might be affected.
else {
// @todo Replace with theme cache tag: https://drupal.org/node/2185617
Cache::invalidateTags(array('content' => TRUE));
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
parent::postDelete($storage_controller, $entities);
Cache::invalidateTags(array('block' => array_keys($entities)));
}
/**
* Sorts active blocks by weight; sorts inactive blocks by name.
*/

View File

@ -46,7 +46,6 @@ class Block extends DisplayPluginBase {
$options['block_description'] = array('default' => '', 'translatable' => TRUE);
$options['block_category'] = array('default' => 'Lists (Views)', 'translatable' => TRUE);
$options['block_caching'] = array('default' => DRUPAL_NO_CACHE);
$options['block_hide_empty'] = array('default' => FALSE);
$options['allow'] = array(
@ -131,13 +130,6 @@ class Block extends DisplayPluginBase {
'value' => empty($filtered_allow) ? t('None') : t('Items per page'),
);
$types = $this->blockCachingModes();
$options['block_caching'] = array(
'category' => 'other',
'title' => t('Block caching'),
'value' => $types[$this->getCacheType()],
);
$options['block_hide_empty'] = array(
'category' => 'other',
'title' => t('Hide block if the view output is empty'),
@ -145,33 +137,6 @@ class Block extends DisplayPluginBase {
);
}
/**
* Provide a list of core's block caching modes.
*/
protected function blockCachingModes() {
return array(
DRUPAL_NO_CACHE => t('Do not cache'),
DRUPAL_CACHE_GLOBAL => t('Cache once for everything (global)'),
DRUPAL_CACHE_PER_PAGE => t('Per page'),
DRUPAL_CACHE_PER_ROLE => t('Per role'),
DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE => t('Per role per page'),
DRUPAL_CACHE_PER_USER => t('Per user'),
DRUPAL_CACHE_PER_USER | DRUPAL_CACHE_PER_PAGE => t('Per user per page'),
);
}
/**
* Provide a single method to figure caching type, keeping a sensible default
* for when it's unset.
*/
public function getCacheType() {
$cache_type = $this->getOption('block_caching');
if (empty($cache_type)) {
$cache_type = DRUPAL_NO_CACHE;
}
return $cache_type;
}
/**
* Provide the default form for setting options.
*/
@ -196,16 +161,6 @@ class Block extends DisplayPluginBase {
'#default_value' => $this->getOption('block_category'),
);
break;
case 'block_caching':
$form['#title'] .= t('Block caching type');
$form['block_caching'] = array(
'#type' => 'radios',
'#description' => t("This sets the default status for Drupal's built-in block caching method; this requires that caching be turned on in block administration, and be careful because you have little control over when this cache is flushed."),
'#options' => $this->blockCachingModes(),
'#default_value' => $this->getCacheType(),
);
break;
case 'block_hide_empty':
$form['#title'] .= t('Block empty settings');
@ -251,7 +206,6 @@ class Block extends DisplayPluginBase {
switch ($form_state['section']) {
case 'block_description':
case 'block_category':
case 'block_caching':
case 'allow':
case 'block_hide_empty':
$this->setOption($form_state['section'], $form_state['values'][$form_state['section']]);

View File

@ -61,10 +61,13 @@ class BlockCacheTest extends WebTestBase {
}
/**
* Test DRUPAL_CACHE_PER_ROLE.
* Test "cache_context.user.roles" cache context.
*/
function testCachePerRole() {
$this->setCacheMode(DRUPAL_CACHE_PER_ROLE);
$this->setBlockCacheConfig(array(
'max_age' => 600,
'contexts' => array('cache_context.user.roles'),
));
// Enable our test block. Set some content for it to display.
$current_content = $this->randomName();
@ -108,10 +111,13 @@ class BlockCacheTest extends WebTestBase {
}
/**
* Test DRUPAL_CACHE_GLOBAL.
* Test a cacheable block without any cache context.
*/
function testCacheGlobal() {
$this->setCacheMode(DRUPAL_CACHE_GLOBAL);
$this->setBlockCacheConfig(array(
'max_age' => 600,
));
$current_content = $this->randomName();
\Drupal::state()->set('block_test.content', $current_content);
@ -124,18 +130,21 @@ class BlockCacheTest extends WebTestBase {
$this->drupalLogout();
$this->drupalGet('user');
$this->assertText($old_content, 'Block content served from global cache.');
$this->assertText($old_content, 'Block content served from cache.');
}
/**
* Test DRUPAL_NO_CACHE.
* Test non-cacheable block.
*/
function testNoCache() {
$this->setCacheMode(DRUPAL_NO_CACHE);
$this->setBlockCacheConfig(array(
'max_age' => 0,
));
$current_content = $this->randomName();
\Drupal::state()->set('block_test.content', $current_content);
// If DRUPAL_NO_CACHE has no effect, the next request would be cached.
// If max_age = 0 has no effect, the next request would be cached.
$this->drupalGet('');
$this->assertText($current_content, 'Block content displays.');
@ -143,14 +152,18 @@ class BlockCacheTest extends WebTestBase {
$current_content = $this->randomName();
\Drupal::state()->set('block_test.content', $current_content);
$this->drupalGet('');
$this->assertText($current_content, 'DRUPAL_NO_CACHE prevents blocks from being cached.');
$this->assertText($current_content, 'Maximum age of zero prevents blocks from being cached.');
}
/**
* Test DRUPAL_CACHE_PER_USER.
* Test "cache_context.user" cache context.
*/
function testCachePerUser() {
$this->setCacheMode(DRUPAL_CACHE_PER_USER);
$this->setBlockCacheConfig(array(
'max_age' => 600,
'contexts' => array('cache_context.user'),
));
$current_content = $this->randomName();
\Drupal::state()->set('block_test.content', $current_content);
$this->drupalLogin($this->normal_user);
@ -175,10 +188,14 @@ class BlockCacheTest extends WebTestBase {
}
/**
* Test DRUPAL_CACHE_PER_PAGE.
* Test "cache_context.url" cache context.
*/
function testCachePerPage() {
$this->setCacheMode(DRUPAL_CACHE_PER_PAGE);
$this->setBlockCacheConfig(array(
'max_age' => 600,
'contexts' => array('cache_context.url'),
));
$current_content = $this->randomName();
\Drupal::state()->set('block_test.content', $current_content);
@ -196,10 +213,11 @@ class BlockCacheTest extends WebTestBase {
}
/**
* Private helper method to set the test block's cache mode.
* Private helper method to set the test block's cache configuration.
*/
private function setCacheMode($cache_mode) {
$this->block->getPlugin()->setConfigurationValue('cache', $cache_mode);
private function setBlockCacheConfig($cache_config) {
$block = $this->block->getPlugin();
$block->setConfigurationValue('cache', $cache_config);
$this->block->save();
}

View File

@ -45,10 +45,13 @@ class BlockInterfaceTest extends DrupalUnitTestBase {
);
$expected_configuration = array(
'label' => 'Custom Display Message',
'display_message' => 'no message set',
'module' => 'block_test',
'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
'cache' => DRUPAL_NO_CACHE,
'cache' => array(
'max_age' => 0,
'contexts' => array(),
),
'display_message' => 'no message set',
);
// Initial configuration of the block at construction time.
$display_block = $manager->createInstance('test_block_instantiation', $configuration);
@ -60,6 +63,12 @@ class BlockInterfaceTest extends DrupalUnitTestBase {
$this->assertIdentical($display_block->getConfiguration(), $expected_configuration, 'The block configuration was updated correctly.');
$definition = $display_block->getPluginDefinition();
$period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
$period = array_map('format_interval', array_combine($period, $period));
$period[0] = '<' . t('no caching') . '>';
$period[\Drupal\Core\Cache\Cache::PERMANENT] = t('Forever');
$contexts = \Drupal::service("cache_contexts")->getLabels();
unset($contexts['cache_context.theme']);
$expected_form = array(
'module' => array(
'#type' => 'value',
@ -84,8 +93,27 @@ class BlockInterfaceTest extends DrupalUnitTestBase {
'#return_value' => 'visible',
),
'cache' => array(
'#type' => 'value',
'#value' => DRUPAL_NO_CACHE,
'#type' => 'details',
'#title' => t('Cache settings'),
'max_age' => array(
'#type' => 'select',
'#title' => t('Maximum age'),
'#description' => t('The maximum time this block may be cached.'),
'#default_value' => 0,
'#options' => $period,
),
'contexts' => array(
'#type' => 'checkboxes',
'#title' => t('Vary by context'),
'#description' => t('The contexts this cached block must be varied by.'),
'#default_value' => array(),
'#options' => $contexts,
'#states' => array(
'disabled' => array(
':input[name="settings[cache][max_age]"]' => array('value' => (string) 0),
),
),
),
),
'display_message' => array(
'#type' => 'textfield',

View File

@ -48,21 +48,24 @@ class BlockRenderOrderTest extends WebTestBase {
'stark_powered' => array(
'weight' => '-3',
'id' => 'stark_powered',
'label' => 'Test block A',
),
'stark_by' => array(
'weight' => '3',
'id' => 'stark_by',
'label' => 'Test block C',
),
'stark_drupal' => array(
'weight' => '3',
'id' => 'stark_drupal',
'label' => 'Test block B',
),
);
// Place the test blocks.
foreach ($test_blocks as $test_block) {
$this->drupalPlaceBlock('system_powered_by_block', array(
'label' => 'Test Block',
'label' => $test_block['label'],
'region' => $region,
'weight' => $test_block['weight'],
'id' => $test_block['id'],
@ -81,6 +84,6 @@ class BlockRenderOrderTest extends WebTestBase {
}
}
$this->assertTrue($position['stark_powered'] < $position['stark_by'], 'Blocks with different weight are rendered in the correct order.');
$this->assertTrue($position['stark_drupal'] < $position['stark_by'], 'Blocks with identical weight are rendered in reverse alphabetical order.');
$this->assertTrue($position['stark_drupal'] < $position['stark_by'], 'Blocks with identical weight are rendered in alphabetical order.');
}
}

View File

@ -56,7 +56,6 @@ class BlockStorageUnitTest extends DrupalUnitTestBase {
// Run each test method in the same installation.
$this->createTests();
$this->loadTests();
$this->renderTests();
$this->deleteTests();
}
@ -103,7 +102,10 @@ class BlockStorageUnitTest extends DrupalUnitTestBase {
'label' => '',
'module' => 'block_test',
'label_display' => BlockInterface::BLOCK_LABEL_VISIBLE,
'cache' => DRUPAL_NO_CACHE,
'cache' => array(
'max_age' => 0,
'contexts' => array(),
),
),
'visibility' => NULL,
);
@ -113,7 +115,7 @@ class BlockStorageUnitTest extends DrupalUnitTestBase {
}
/**
* Tests the rendering of blocks.
* Tests the loading of blocks.
*/
protected function loadTests() {
$entity = $this->controller->load('test_block');
@ -127,57 +129,6 @@ class BlockStorageUnitTest extends DrupalUnitTestBase {
$this->assertTrue($entity->uuid());
}
/**
* Tests the rendering of blocks.
*/
protected function renderTests() {
// Test the rendering of a block.
$entity = entity_load('block', 'test_block');
$output = entity_view($entity, 'block');
$expected = array();
$expected[] = '<div class="block block-block-test" id="block-test-block">';
$expected[] = ' ';
$expected[] = ' ';
$expected[] = '';
$expected[] = ' <div class="content">';
$expected[] = ' ';
$expected[] = ' </div>';
$expected[] = '</div>';
$expected[] = '';
$expected_output = implode("\n", $expected);
$this->assertEqual(drupal_render($output), $expected_output);
// Reset the HTML IDs so that the next render is not affected.
drupal_static_reset('drupal_html_id');
// Test the rendering of a block with a given title.
$entity = $this->controller->create(array(
'id' => 'test_block2',
'theme' => 'stark',
'plugin' => 'test_html',
'settings' => array(
'label' => 'Powered by Bananas',
),
));
$entity->save();
$output = entity_view($entity, 'block');
$expected = array();
$expected[] = '<div class="block block-block-test" id="block-test-block2">';
$expected[] = ' ';
$expected[] = ' <h2>Powered by Bananas</h2>';
$expected[] = ' ';
$expected[] = '';
$expected[] = ' <div class="content">';
$expected[] = ' ';
$expected[] = ' </div>';
$expected[] = '</div>';
$expected[] = '';
$expected_output = implode("\n", $expected);
$this->assertEqual(drupal_render($output), $expected_output);
// Clean up this entity.
$entity->delete();
}
/**
* Tests the deleting of blocks.
*/

View File

@ -7,6 +7,7 @@
namespace Drupal\block\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\simpletest\WebTestBase;
/**
@ -249,35 +250,106 @@ class BlockTest extends BlockTestBase {
}
/**
* Test _block_rehash().
* Test that cache tags are properly set and bubbled up to the page cache.
*
* Verify that invalidation of these cache tags works:
* - "block:<block ID>"
* - "block_plugin:<block plugin ID>"
*/
function testBlockRehash() {
\Drupal::moduleHandler()->install(array('block_test'));
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('block_test'), 'Test block module enabled.');
public function testBlockCacheTags() {
// The page cache only works for anonymous users.
$this->drupalLogout();
// Clear the block cache to load the block_test module's block definitions.
$this->container->get('plugin.manager.block')->clearCachedDefinitions();
// Enable page caching.
$config = \Drupal::config('system.performance');
$config->set('cache.page.use_internal', 1);
$config->set('cache.page.max_age', 300);
$config->save();
// Add a test block.
$block = array();
$block['id'] = 'test_cache';
$block['theme'] = \Drupal::config('system.theme')->get('default');
$block['region'] = 'header';
$block = $this->drupalPlaceBlock('test_cache', array('region' => 'header'));
// Place the "Powered by Drupal" block.
$block = $this->drupalPlaceBlock('system_powered_by_block', array('id' => 'powered', 'cache' => array('max_age' => 315360000)));
// Our test block's caching should default to DRUPAL_CACHE_PER_ROLE.
$settings = $block->get('settings');
$this->assertEqual($settings['cache'], DRUPAL_CACHE_PER_ROLE, 'Test block cache mode defaults to DRUPAL_CACHE_PER_ROLE.');
// Prime the page cache.
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
// Disable caching for this block.
$block->getPlugin()->setConfigurationValue('cache', DRUPAL_NO_CACHE);
// Verify a cache hit, but also the presence of the correct cache tags in
// both the page and block caches.
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
$cid_parts = array(url('<front>', array('absolute' => TRUE)), 'html');
$cid = sha1(implode(':', $cid_parts));
$cache_entry = \Drupal::cache('page')->get($cid);
$expected_cache_tags = array(
'content:1',
'block_view:1',
'block:powered',
'block_plugin:system_powered_by_block',
);
$this->assertIdentical($cache_entry->tags, $expected_cache_tags);
$cache_entry = \Drupal::cache('block')->get('entity_view:block:powered:en:stark');
$this->assertIdentical($cache_entry->tags, $expected_cache_tags);
// The "Powered by Drupal" block is modified; verify a cache miss.
$block->set('region', 'content');
$block->save();
// Flushing all caches should call _block_rehash().
$this->resetAll();
// Verify that block is updated with the new caching mode.
$block = entity_load('block', $block->id());
$settings = $block->get('settings');
$this->assertEqual($settings['cache'], DRUPAL_NO_CACHE, "Test block's database entry updated to DRUPAL_NO_CACHE.");
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
// Now we should have a cache hit again.
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
// Place the "Powered by Drupal" block another time; verify a cache miss.
$block_2 = $this->drupalPlaceBlock('system_powered_by_block', array('id' => 'powered-2', 'cache' => array('max_age' => 315360000)));
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
$cid_parts = array(url('<front>', array('absolute' => TRUE)), 'html');
$cid = sha1(implode(':', $cid_parts));
$cache_entry = \Drupal::cache('page')->get($cid);
$expected_cache_tags = array(
'content:1',
'block_view:1',
'block:powered-2',
'block:powered',
'block_plugin:system_powered_by_block',
);
$this->assertEqual($cache_entry->tags, $expected_cache_tags);
$expected_cache_tags = array(
'content:1',
'block_view:1',
'block:powered',
'block_plugin:system_powered_by_block',
);
$cache_entry = \Drupal::cache('block')->get('entity_view:block:powered:en:stark');
$this->assertIdentical($cache_entry->tags, $expected_cache_tags);
$expected_cache_tags = array(
'content:1',
'block_view:1',
'block:powered-2',
'block_plugin:system_powered_by_block',
);
$cache_entry = \Drupal::cache('block')->get('entity_view:block:powered-2:en:stark');
$this->assertIdentical($cache_entry->tags, $expected_cache_tags);
// The plugin providing the "Powered by Drupal" block is modified; verify a
// cache miss.
Cache::invalidateTags(array('block_plugin:system_powered_by_block'));
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
// Now we should have a cache hit again.
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
// Delete the "Powered by Drupal" blocks; verify a cache miss.
entity_delete_multiple('block', array('powered', 'powered-2'));
$this->drupalGet('<front>');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
}
}

View File

@ -0,0 +1,346 @@
<?php
/**
* @file
* Contains \Drupal\block\Tests\BlockViewBuilderTest.
*/
namespace Drupal\block\Tests;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\UrlCacheContext;
use Drupal\simpletest\DrupalUnitTestBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the block view builder.
*/
class BlockViewBuilderTest extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('block', 'block_test', 'system');
/**
* The block being tested.
*
* @var \Drupal\block\Entity\BlockInterface
*/
protected $block;
/**
* The block storage controller.
*
* @var \Drupal\Core\Config\Entity\ConfigStorageControllerInterface
*/
protected $controller;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Block rendering',
'description' => 'Tests the block view builder.',
'group' => 'Block',
);
}
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->controller = $this->container
->get('entity.manager')
->getStorageController('block');
\Drupal::state()->set('block_test.content', 'Llamas &gt; unicorns!');
// Create a block with only required values.
$this->block = $this->controller->create(array(
'id' => 'test_block',
'theme' => 'stark',
'plugin' => 'test_cache',
));
$this->block->save();
$this->container->get('cache.block')->deleteAll();
}
/**
* Tests the rendering of blocks.
*/
public function testBasicRendering() {
\Drupal::state()->set('block_test.content', '');
$entity = $this->controller->create(array(
'id' => 'test_block1',
'theme' => 'stark',
'plugin' => 'test_html',
));
$entity->save();
// Test the rendering of a block.
$entity = entity_load('block', 'test_block1');
$output = entity_view($entity, 'block');
$expected = array();
$expected[] = '<div class="block block-block-test" id="block-test-block1">';
$expected[] = ' ';
$expected[] = ' ';
$expected[] = '';
$expected[] = ' <div class="content">';
$expected[] = ' ';
$expected[] = ' </div>';
$expected[] = '</div>';
$expected[] = '';
$expected_output = implode("\n", $expected);
$this->assertEqual(drupal_render($output), $expected_output);
// Reset the HTML IDs so that the next render is not affected.
drupal_static_reset('drupal_html_id');
// Test the rendering of a block with a given title.
$entity = $this->controller->create(array(
'id' => 'test_block2',
'theme' => 'stark',
'plugin' => 'test_html',
'settings' => array(
'label' => 'Powered by Bananas',
),
));
$entity->save();
$output = entity_view($entity, 'block');
$expected = array();
$expected[] = '<div class="block block-block-test" id="block-test-block2">';
$expected[] = ' ';
$expected[] = ' <h2>Powered by Bananas</h2>';
$expected[] = ' ';
$expected[] = '';
$expected[] = ' <div class="content">';
$expected[] = ' ';
$expected[] = ' </div>';
$expected[] = '</div>';
$expected[] = '';
$expected_output = implode("\n", $expected);
$this->assertEqual(drupal_render($output), $expected_output);
}
/**
* Tests block render cache handling.
*/
public function testBlockViewBuilderCache() {
// Verify cache handling for a non-empty block.
$this->verifyRenderCacheHandling();
// Create an empty block.
$this->block = $this->controller->create(array(
'id' => 'test_block',
'theme' => 'stark',
'plugin' => 'test_cache',
));
$this->block->save();
\Drupal::state()->set('block_test.content', NULL);
// Verify cache handling for an empty block.
$this->verifyRenderCacheHandling();
}
/**
* Verifies render cache handling of the block being tested.
*
* @see ::testBlockViewBuilderCache()
*/
protected function verifyRenderCacheHandling() {
// Force a request via GET so we can get drupal_render() cache working.
$request_method = \Drupal::request()->server->get('REQUEST_METHOD');
$this->container->get('request')->setMethod('GET');
// Test that entities with caching disabled do not generate a cache entry.
$build = $this->getBlockRenderArray();
$this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags'), 'The render array element of uncacheable blocks is not cached, but does have cache tags set.');
// Enable block caching.
$this->setBlockCacheConfig(array(
'max_age' => 600,
));
// Test that a cache entry is created.
$build = $this->getBlockRenderArray();
$cid = drupal_render_cid_create($build);
drupal_render($build);
$this->assertTrue($this->container->get('cache.block')->get($cid), 'The block render element has been cached.');
// Re-save the block and check that the cache entry has been deleted.
$this->block->save();
$this->assertFalse($this->container->get('cache.block')->get($cid), 'The block render cache entry has been cleared when the block was saved.');
// Rebuild the render array (creating a new cache entry in the process) and
// delete the block to check the cache entry is deleted.
unset($build['#printed']);
drupal_render($build);
$this->assertTrue($this->container->get('cache.block')->get($cid), 'The block render element has been cached.');
$this->block->delete();
$this->assertFalse($this->container->get('cache.block')->get($cid), 'The block render cache entry has been cleared when the block was deleted.');
// Restore the previous request method.
$this->container->get('request')->setMethod($request_method);
}
/**
* Tests block view altering.
*/
public function testBlockViewBuilderAlter() {
// Establish baseline.
$build = $this->getBlockRenderArray();
$this->assertIdentical(drupal_render($build), 'Llamas &gt; unicorns!');
// Enable the block view alter hook that adds a suffix, for basic testing.
\Drupal::state()->set('block_test_view_alter_suffix', TRUE);
// Basic: non-empty block.
$build = $this->getBlockRenderArray();
$this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '<br>Goodbye!', 'A block with content is altered.');
$this->assertIdentical(drupal_render($build), 'Llamas &gt; unicorns!<br>Goodbye!');
// Basic: empty block.
\Drupal::state()->set('block_test.content', NULL);
$build = $this->getBlockRenderArray();
$this->assertTrue(isset($build['#suffix']) && $build['#suffix'] === '<br>Goodbye!', 'A block without content is altered.');
$this->assertIdentical(drupal_render($build), '<br>Goodbye!');
// Disable the block view alter hook that adds a suffix, for basic testing.
\Drupal::state()->set('block_test_view_alter_suffix', FALSE);
// Force a request via GET so we can get drupal_render() cache working.
$request_method = \Drupal::request()->server->get('REQUEST_METHOD');
$this->container->get('request')->setMethod('GET');
$default_keys = array('entity_view', 'block', 'test_block', 'en', 'cache_context.theme');
$default_tags = array('content' => TRUE, 'block_view' => TRUE, 'block' => array('test_block'), 'block_plugin' => array('test_cache'));
// Advanced: cached block, but an alter hook adds an additional cache key.
$this->setBlockCacheConfig(array(
'max_age' => 600,
));
$alter_add_key = $this->randomName();
\Drupal::state()->set('block_test_view_alter_cache_key', $alter_add_key);
$expected_keys = array_merge($default_keys, array($alter_add_key));
$build = $this->getBlockRenderArray();
$this->assertIdentical($expected_keys, $build['#cache']['keys'], 'An altered cacheable block has the expected cache keys.');
$cid = drupal_render_cid_create(array('#cache' => array('keys' => $expected_keys)));
$this->assertIdentical(drupal_render($build), '');
$cache_entry = $this->container->get('cache.block')->get($cid);
$this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
$expected_flattened_tags = array('content:1', 'block_view:1', 'block:test_block', 'block_plugin:test_cache');
$this->assertIdentical($cache_entry->tags, array_combine($expected_flattened_tags, $expected_flattened_tags)); //, 'The block render element has been cached with the expected cache tags.');
$this->container->get('cache.block')->delete($cid);
// Advanced: cached block, but an alter hook adds an additional cache tag.
$alter_add_tag = $this->randomName();
\Drupal::state()->set('block_test_view_alter_cache_tag', $alter_add_tag);
$expected_tags = NestedArray::mergeDeep($default_tags, array($alter_add_tag => TRUE));
$build = $this->getBlockRenderArray();
$this->assertIdentical($expected_tags, $build['#cache']['tags'], 'An altered cacheable block has the expected cache tags.');
$cid = drupal_render_cid_create(array('#cache' => array('keys' => $expected_keys)));
$this->assertIdentical(drupal_render($build), '');
$cache_entry = $this->container->get('cache.block')->get($cid);
$this->assertTrue($cache_entry, 'The block render element has been cached with the expected cache ID.');
$expected_flattened_tags = array('content:1', 'block_view:1', 'block:test_block', 'block_plugin:test_cache', $alter_add_tag . ':1');
$this->assertIdentical($cache_entry->tags, array_combine($expected_flattened_tags, $expected_flattened_tags)); //, 'The block render element has been cached with the expected cache tags.');
$this->container->get('cache.block')->delete($cid);
// Advanced: cached block, but an alter hook adds a #pre_render callback to
// alter the eventual content.
\Drupal::state()->set('block_test_view_alter_append_pre_render_prefix', TRUE);
$build = $this->getBlockRenderArray();
$this->assertFalse(isset($build['#prefix']), 'The appended #pre_render callback has not yet run before calling drupal_render().');
$this->assertIdentical(drupal_render($build), 'Hiya!<br>');
$this->assertTrue(isset($build['#prefix']) && $build['#prefix'] === 'Hiya!<br>', 'A cached block without content is altered.');
// Restore the previous request method.
$this->container->get('request')->setMethod($request_method);
}
/**
* Tests block render cache handling with configurable cache contexts.
*
* This is only intended to test that an existing block can be configured with
* additional contexts, not to test that each context works correctly.
*
* @see \Drupal\block\Tests\BlockCacheTest
*/
public function testBlockViewBuilderCacheContexts() {
// Force a request via GET so we can get drupal_render() cache working.
$request_method = \Drupal::request()->server->get('REQUEST_METHOD');
$this->container->get('request')->setMethod('GET');
// First: no cache context.
$this->setBlockCacheConfig(array(
'max_age' => 600,
));
$build = $this->getBlockRenderArray();
$cid = drupal_render_cid_create($build);
drupal_render($build);
$this->assertTrue($this->container->get('cache.block', $cid), 'The block render element has been cached.');
// Second: the "per URL" cache context.
$this->setBlockCacheConfig(array(
'max_age' => 600,
'contexts' => array('cache_context.url'),
));
$old_cid = $cid;
$build = $this->getBlockRenderArray();
$cid = drupal_render_cid_create($build);
drupal_render($build);
$this->assertTrue($this->container->get('cache.block', $cid), 'The block render element has been cached.');
$this->assertNotEqual($cid, $old_cid, 'The cache ID has changed.');
// Third: the same block configuration, but a different URL.
$original_url_cache_context = $this->container->get('cache_context.url');
$temp_context = new UrlCacheContext(Request::create('/foo'));
$this->container->set('cache_context.url', $temp_context);
$old_cid = $cid;
$build = $this->getBlockRenderArray();
$cid = drupal_render_cid_create($build);
drupal_render($build);
$this->assertTrue($this->container->get('cache.block', $cid), 'The block render element has been cached.');
$this->assertNotEqual($cid, $old_cid, 'The cache ID has changed.');
$this->container->set('cache_context.url', $original_url_cache_context);
// Restore the previous request method.
$this->container->get('request')->setMethod($request_method);
}
/**
* Sets the test block's cache configuration.
*
* @param array $cache_config
* The desired cache configuration.
*/
protected function setBlockCacheConfig(array $cache_config) {
$block = $this->block->getPlugin();
$block->setConfigurationValue('cache', $cache_config);
$this->block->save();
}
/**
* Get a fully built render array for a block.
*
* @return array
* The render array.
*/
protected function getBlockRenderArray() {
$build = $this->container->get('entity.manager')->getViewBuilder('block')->view($this->block, 'block');
// Mock the build array to not require the theme registry.
unset($build['#theme']);
return $build;
}
}

View File

@ -176,19 +176,6 @@ class DisplayBlockTest extends ViewTestBase {
$this->assertBlockAppears($block_4);
}
/**
* Tests views block plugin definitions.
*/
public function testViewsBlockPlugins() {
// Ensures that the cache setting gets to the block settings.
$instance = $this->container->get('plugin.manager.block')->createInstance('views_block:test_view_block2-block_2');
$configuration = $instance->getConfiguration();
$this->assertEqual($configuration['cache'], DRUPAL_NO_CACHE);
$instance = $this->container->get('plugin.manager.block')->createInstance('views_block:test_view_block2-block_3');
$configuration = $instance->getConfiguration();
$this->assertEqual($configuration['cache'], DRUPAL_CACHE_PER_USER);
}
/**
* Test the block form for a Views block.
*/
@ -283,19 +270,23 @@ class DisplayBlockTest extends ViewTestBase {
public function testBlockContextualLinks() {
$this->drupalLogin($this->drupalCreateUser(array('administer views', 'access contextual links', 'administer blocks')));
$block = $this->drupalPlaceBlock('views_block:test_view_block-block_1');
$cached_block = $this->drupalPlaceBlock('views_block:test_view_block-block_1', array('cache' => array('max_age' => 3600)));
$this->drupalGet('test-page');
$id = 'block:block=' . $block->id() . ':|views_ui_edit:view=test_view_block:location=block&name=test_view_block&display_id=block_1';
$cached_id = 'block:block=' . $cached_block->id() . ':|views_ui_edit:view=test_view_block:location=block&name=test_view_block&display_id=block_1';
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
$this->assertRaw('<div' . new Attribute(array('data-contextual-id' => $id)) . '></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
$this->assertRaw('<div' . new Attribute(array('data-contextual-id' => $cached_id)) . '></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $cached_id)));
// Get server-rendered contextual links.
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
$post = array('ids[0]' => $id);
$post = array('ids[0]' => $id, 'ids[1]' => $cached_id);
$response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
$this->assertResponse(200);
$json = Json::decode($response);
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
$this->assertIdentical($json[$cached_id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $cached_block->id() . '">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
}
}

View File

@ -12,11 +12,6 @@ use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Transliteration\PHPTransliteration;
use Drupal\Tests\UnitTestCase;
// @todo Remove once the constants are replaced with constants on classes.
if (!defined('DRUPAL_NO_CACHE')) {
define('DRUPAL_NO_CACHE', -1);
}
/**
* Tests the base block plugin.
*

View File

@ -5,6 +5,8 @@
* Provide test blocks.
*/
use Drupal\block\BlockPluginInterface;
/**
* Implements hook_block_alter().
*/
@ -13,3 +15,29 @@ function block_test_block_alter(&$block_info) {
$block_info['test_block_instantiation']['category'] = t('Custom category');
}
}
/**
* Implements hook_block_view_BASE_BLOCK_ID_alter().
*/
function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterface $block) {
if (\Drupal::state()->get('block_test_view_alter_suffix') !== NULL) {
$build['#suffix'] = '<br>Goodbye!';
}
if (\Drupal::state()->get('block_test_view_alter_cache_key') !== NULL) {
$build['#cache']['keys'][] = \Drupal::state()->get('block_test_view_alter_cache_key');
}
if (\Drupal::state()->get('block_test_view_alter_cache_tag') !== NULL) {
$build['#cache']['tags'][\Drupal::state()->get('block_test_view_alter_cache_tag')] = TRUE;
}
if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) {
$build['#pre_render'][] = 'block_test_pre_render_alter_content';
}
}
/**
* #pre_render callback for a block to alter its content.
*/
function block_test_pre_render_alter_content($build) {
$build['#prefix'] = 'Hiya!<br>';
return $build;
}

View File

@ -9,7 +9,6 @@ settings:
label: 'Test HTML block'
module: block_test
label_display: 'hidden'
cache: 1
visibility:
path:
visibility: 0

View File

@ -19,24 +19,17 @@ use Drupal\block\BlockBase;
*/
class TestCacheBlock extends BlockBase {
/**
* {@inheritdoc}
*
* Sets a different caching strategy for testing purposes.
*/
public function defaultConfiguration() {
return array(
'cache' => DRUPAL_CACHE_PER_ROLE,
);
}
/**
* {@inheritdoc}
*/
public function build() {
return array(
'#children' => \Drupal::state()->get('block_test.content'),
);
$content = \Drupal::state()->get('block_test.content');
$build = array();
if (!empty($content)) {
$build['#markup'] = $content;
}
return $build;
}
}

View File

@ -16,16 +16,4 @@ namespace Drupal\block_test\Plugin\Block;
* )
*/
class TestXSSTitleBlock extends TestCacheBlock {
/**
* {@inheritdoc}
*
* Sets a different caching strategy for testing purposes.
*/
public function defaultConfiguration() {
return array(
'cache' => DRUPAL_NO_CACHE,
);
}
}

View File

@ -65,7 +65,6 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt
*/
public function defaultConfiguration() {
return array(
'cache' => DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE,
'block_mode' => "all pages",
);
}
@ -101,6 +100,7 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt
*/
public function build() {
$current_bid = 0;
if ($node = $this->request->get('node')) {
$current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
}
@ -145,15 +145,21 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt
$data = array_shift($tree);
$below = \Drupal::service('book.manager')->bookTreeOutput($data['below']);
if (!empty($below)) {
$book_title_link = array('#theme' => 'book_title_link', '#link' => $data['link']);
return array(
'#title' => drupal_render($book_title_link),
$below,
);
return $below;
}
}
}
return array();
}
/**
* {@inheritdoc}
*/
protected function getRequiredCacheContexts() {
// The "Book navigation" block must be cached per URL and per role: the
// "active" menu link may differ per URL and different roles may have access
// to different menu links.
return array('cache_context.url', 'cache_context.user.roles');
}
}

View File

@ -538,23 +538,6 @@ function forum_form_node_form_alter(&$form, &$form_state, $form_id) {
}
}
/**
* Render API callback: Lists nodes based on the element's #query property.
*
* This function can be used as a #pre_render callback.
*
* @see \Drupal\forum\Plugin\block\block\NewTopicsBlock::build()
* @see \Drupal\forum\Plugin\block\block\ActiveTopicsBlock::build()
*/
function forum_block_view_pre_render($elements) {
$result = $elements['#query']->execute();
if ($node_title_list = node_title_list($result)) {
$elements['forum_list'] = $node_title_list;
$elements['forum_more'] = array('#theme' => 'more_link', '#url' => 'forum', '#title' => t('Read the latest forum topics.'));
}
return $elements;
}
/**
* Implements hook_preprocess_HOOK() for block templates.
*/

View File

@ -21,17 +21,13 @@ class ActiveTopicsBlock extends ForumBlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$query = db_select('forum_index', 'f')
protected function buildForumQuery() {
return db_select('forum_index', 'f')
->fields('f')
->addTag('node_access')
->addMetaData('base_table', 'forum_index')
->orderBy('f.last_comment_timestamp', 'DESC')
->range(0, $this->configuration['block_count']);
return array(
drupal_render_cache_by_query($query, 'forum_block_view'),
);
}
}

View File

@ -9,18 +9,42 @@ namespace Drupal\forum\Plugin\Block;
use Drupal\block\BlockBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Cache\Cache;
/**
* Provides a base class for Forum blocks.
*/
abstract class ForumBlockBase extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$result = $this->buildForumQuery()->execute();
if ($node_title_list = node_title_list($result)) {
$elements['forum_list'] = $node_title_list;
$elements['forum_more'] = array(
'#theme' => 'more_link',
'#url' => 'forum',
'#title' => t('Read the latest forum topics.')
);
}
return $elements;
}
/**
* Builds the select query to use for this forum block.
*
* @return \Drupal\Core\Database\Query\Select
* A Select object.
*/
abstract protected function buildForumQuery();
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array(
'cache' => DRUPAL_CACHE_CUSTOM,
'properties' => array(
'administrative' => TRUE,
),
@ -56,4 +80,11 @@ abstract class ForumBlockBase extends BlockBase {
$this->configuration['block_count'] = $form_state['values']['block_count'];
}
/**
* {@inheritdoc}
*/
public function getCacheKeys() {
return array_merge(parent::getCacheKeys(), Cache::keyFromQuery($this->buildForumQuery()));
}
}

View File

@ -21,17 +21,12 @@ class NewTopicsBlock extends ForumBlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$query = db_select('forum_index', 'f')
protected function buildForumQuery() {
return db_select('forum_index', 'f')
->fields('f')
->addTag('node_access')
->addMetaData('base_table', 'forum_index')
->orderBy('f.created', 'DESC')
->range(0, $this->configuration['block_count']);
return array(
drupal_render_cache_by_query($query, 'forum_block_view'),
);
}
}

View File

@ -27,7 +27,6 @@ class LanguageBlock extends DerivativeBase {
foreach ($configurable_types as $type) {
$this->derivatives[$type] = $base_plugin_definition;
$this->derivatives[$type]['admin_label'] = t('Language switcher (!type)', array('!type' => $info[$type]['name']));
$this->derivatives[$type]['cache'] = DRUPAL_NO_CACHE;
}
// If there is just one configurable type then change the title of the
// block.

View File

@ -58,7 +58,14 @@ class MenuCacheTagsTest extends PageCacheTagsTestBase {
$this->verifyPageCache($path, 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$this->verifyPageCache($path, 'HIT', array('content:1', 'menu:llama'));
$expected_tags = array(
'content:1',
'block_view:1',
'block:' . $block->id(),
'block_plugin:system_menu_block__llama',
'menu:llama',
);
$this->verifyPageCache($path, 'HIT', $expected_tags);
// Verify that after modifying the menu, there is a cache miss.
@ -101,7 +108,7 @@ class MenuCacheTagsTest extends PageCacheTagsTestBase {
$this->verifyPageCache($path, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($path, 'HIT', array('content:1', 'menu:llama'));
$this->verifyPageCache($path, 'HIT', $expected_tags);
// Verify that after deleting the menu, there is a cache miss.

View File

@ -354,6 +354,7 @@ abstract class WebTestBase extends TestBase {
* - region: 'sidebar_first'.
* - theme: The default theme.
* - visibility: Empty array.
* - cache: array('max_age' => 0).
*
* @return \Drupal\block\Entity\Block
* The block entity.
@ -370,6 +371,9 @@ abstract class WebTestBase extends TestBase {
'label' => $this->randomName(8),
'visibility' => array(),
'weight' => 0,
'cache' => array(
'max_age' => 0,
),
);
foreach (array('region', 'id', 'theme', 'plugin', 'visibility', 'weight') as $key) {
$values[$key] = $settings[$key];

View File

@ -79,10 +79,12 @@ class PerformanceForm extends ConfigFormBase {
'#title' => t('Caching'),
'#open' => TRUE,
);
// Identical options to the ones for block caching.
// @see \Drupal\block\BlockBase::buildConfigurationForm()
$period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
$period = array_map('format_interval', array_combine($period, $period));
$period[0] = '<' . t('none') . '>';
$period[0] = '<' . t('no caching') . '>';
$period[\Drupal\Core\Cache\Cache::PERMANENT] = t('Forever');
$form['caching']['page_cache_maximum_age'] = array(
'#type' => 'select',
'#title' => t('Page cache maximum age'),

View File

@ -117,4 +117,24 @@ class SystemHelpBlock extends BlockBase implements ContainerFactoryPluginInterfa
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
// Modify the default max age for the System Help block: help text is static
// for a given URL, except when a module is updated, in which case
// update.php must be run, which clears all caches. Thus it's safe to cache
// the output for this block forever on a per-URL basis.
return array('cache' => array('max_age' => \Drupal\Core\Cache\Cache::PERMANENT));
}
/**
* {@inheritdoc}
*/
protected function getRequiredCacheContexts() {
// The "System Help" block must be cached per URL: help is defined for a
// given path, and does not come with any access restrictions.
return array('cache_context.url');
}
}

View File

@ -28,4 +28,26 @@ class SystemMainBlock extends BlockBase {
);
}
/**
* {@inheritdoc}
*/
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']['#disabled'] = TRUE;
$form['cache']['#description'] = t('This block is never cacheable, it is not configurable.');
$form['cache']['max_age']['#value'] = 0;
return $form;
}
/**
* {@inheritdoc}
*/
public function isCacheable() {
// The main content block is never cacheable, because it may be dynamic.
return FALSE;
}
}

View File

@ -7,8 +7,8 @@
namespace Drupal\system\Plugin\Block;
use Drupal\Component\Utility\NestedArray;
use Drupal\block\BlockBase;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a generic Menu block.
@ -30,4 +30,41 @@ class SystemMenuBlock extends BlockBase {
return menu_tree($menu);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
// Modify the default max age for menu blocks: modifications made to menus,
// menu links and menu blocks will automatically invalidate corresponding
// cache tags, therefore allowing us to cache menu blocks forever. This is
// only not the case if there are user-specific or dynamic alterations (e.g.
// hook_node_access()), but in that:
// 1) it is possible to set a different max age for individual blocks, since
// this is just the default value.
// 2) modules can modify caching by implementing hook_block_view_alter()
return array('cache' => array('max_age' => \Drupal\Core\Cache\Cache::PERMANENT));
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
// Even when the menu block renders to the empty string for a user, we want
// the cache tag for this menu to be set: whenever the menu is changed, this
// menu block must also be re-rendered for that user, because maybe a menu
// link that is accessible for that user has been added.
$tags = array('menu' => array($this->getDerivativeId()));
return NestedArray::mergeDeep(parent::getCacheTags(), $tags);
}
/**
* {@inheritdoc}
*/
protected function getRequiredCacheContexts() {
// Menu blocks must be cached per URL and per role: the "active" menu link
// may differ per URL and different roles may have access to different menu
// links.
return array('cache_context.url', 'cache_context.user.roles');
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\system\Plugin\Block;
use Drupal\block\BlockBase;
use Drupal\Core\Cache\Cache;
/**
* Provides a 'Powered by Drupal' block.
@ -26,4 +27,34 @@ class SystemPoweredByBlock extends BlockBase {
return array('#markup' => '<span>' . t('Powered by <a href="@poweredby">Drupal</a>', array('@poweredby' => 'http://drupal.org')) . '</span>');
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, array &$form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// The 'Powered by Drupal' block is permanently cacheable, because its
// contents can never change.
$form['cache']['#disabled'] = TRUE;
$form['cache']['max_age']['#value'] = Cache::PERMANENT;
$form['cache']['#description'] = t('This block is always cached forever, it is not configurable.');
return $form;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function isCacheable() {
return TRUE;
}
}

View File

@ -52,7 +52,6 @@ class SystemMenuBlock extends DerivativeBase implements ContainerDerivativeInter
foreach ($this->menuStorage->loadMultiple() as $menu => $entity) {
$this->derivatives[$menu] = $base_plugin_definition;
$this->derivatives[$menu]['admin_label'] = $entity->label();
$this->derivatives[$menu]['cache'] = DRUPAL_NO_CACHE;
}
return $this->derivatives;
}

View File

@ -64,13 +64,35 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
'promote' => NODE_PROMOTED,
));
// Place a block, but only make it visible on full node page 2.
$block = $this->drupalPlaceBlock('views_block:comments_recent-block_1', array(
'visibility' => array(
'path' => array(
'visibility' => BLOCK_VISIBILITY_LISTED,
'pages' => 'node/' . $node_2->id(),
),
)
));
// Full node page 1.
$this->verifyPageCacheTags('node/' . $node_1->id(), array(
'content:1',
'block_view:1',
'block:bartik_content',
'block:bartik_tools',
'block:bartik_login',
'block:bartik_footer',
'block:bartik_powered',
'block_plugin:system_main_block',
'block_plugin:system_menu_block__tools',
'block_plugin:user_login_block',
'block_plugin:system_menu_block__footer',
'block_plugin:system_powered_by_block',
'node_view:1',
'node:' . $node_1->id(),
'user:' . $author_1->id(),
'filter_format:basic_html',
'menu:tools',
'menu:footer',
'menu:main',
));
@ -78,10 +100,24 @@ class PageCacheTagsIntegrationTest extends WebTestBase {
// Full node page 2.
$this->verifyPageCacheTags('node/' . $node_2->id(), array(
'content:1',
'block_view:1',
'block:bartik_content',
'block:bartik_tools',
'block:bartik_login',
'block:' . $block->id(),
'block:bartik_footer',
'block:bartik_powered',
'block_plugin:system_main_block',
'block_plugin:system_menu_block__tools',
'block_plugin:user_login_block',
'block_plugin:views_block__comments_recent-block_1',
'block_plugin:system_menu_block__footer',
'block_plugin:system_powered_by_block',
'node_view:1',
'node:' . $node_2->id(),
'user:' . $author_2->id(),
'filter_format:full_html',
'menu:tools',
'menu:footer',
'menu:main',
));

View File

@ -144,7 +144,7 @@ class EntityViewBuilderTest extends EntityUnitTestBase {
// Test a view mode in default conditions: render caching is enabled for
// the entity type and the view mode.
$build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
$this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags', 'keys', 'granularity', 'bin') , 'A view mode with render cache enabled has the correct output (cache tags, keys, granularity and bin).');
$this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == array('tags', 'keys', 'bin') , 'A view mode with render cache enabled has the correct output (cache tags, keys and bin).');
// Test that a view mode can opt out of render caching.
$build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'test');

View File

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\user\Cache\UserCacheContext.
*/
namespace Drupal\user\Cache;
use Drupal\Core\Cache\CacheContextInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the UserCacheContext service, for "per user" caching.
*/
class UserCacheContext implements CacheContextInterface {
/**
* Constructs a new UserCacheContext service.
*
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(AccountInterface $user) {
$this->user = $user;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t('User');
}
/**
* {@inheritdoc}
*/
public function getContext() {
return "u." . $this->user->id();
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\user\Cache\UserRolesCacheContext.
*/
namespace Drupal\user\Cache;
use Drupal\Core\Cache\CacheContextInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the UserRolesCacheContext service, for "per role" caching.
*/
class UserRolesCacheContext implements CacheContextInterface {
/**
* Constructs a new UserRolesCacheContext service.
*
* @param \Drupal\Core\Session\AccountInterface $user
* The current user.
*/
public function __construct(AccountInterface $user) {
$this->user = $user;
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t("User's roles");
}
/**
* {@inheritdoc}
*/
public function getContext() {
return 'r.' . implode(',', $this->user->getRoles());
}
}

View File

@ -15,6 +15,16 @@ services:
class: Drupal\user\Access\LoginStatusCheck
tags:
- { name: access_check, applies_to: _user_is_logged_in }
cache_context.user:
class: Drupal\user\Cache\UserCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context}
cache_context.user.roles:
class: Drupal\user\Cache\UserRolesCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context}
user.data:
class: Drupal\user\UserData
arguments: ['@database']

View File

@ -29,13 +29,11 @@ class ViewsBlock extends ViewsBlockBase {
$this->view->display_handler->preBlockBuild($this);
if ($output = $this->view->executeDisplay($this->displayID)) {
// Set the label to the title configured in the view.
if (empty($this->configuration['views_label'])) {
$this->configuration['label'] = Xss::filterAdmin($this->view->getTitle());
}
else {
$this->configuration['label'] = $this->configuration['views_label'];
// Override the label to the dynamic title configured in the view.
if (empty($this->configuration['views_label']) && $this->view->getTitle()) {
$output['#title'] = Xss::filterAdmin($this->view->getTitle());
}
// Before returning the block output, convert it to a renderable array
// with contextual links.
$this->addContextualLinks($output);
@ -45,6 +43,20 @@ class ViewsBlock extends ViewsBlockBase {
return array();
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
$configuration = parent::getConfiguration();
// Set the label to the static title configured in the view.
if (!empty($configuration['views_label'])) {
$configuration['label'] = $configuration['views_label'];
}
return $configuration;
}
/**
* {@inheritdoc}
*/

View File

@ -99,10 +99,7 @@ abstract class ViewsBlockBase extends BlockBase implements ContainerFactoryPlugi
* {@inheritdoc}
*/
public function defaultConfiguration() {
$settings = array();
$settings['views_label'] = '';
return $settings;
return array('views_label' => '');
}
/**

View File

@ -102,7 +102,6 @@ class ViewsBlock implements ContainerDerivativeInterface {
$this->derivatives[$delta] = array(
'category' => $display->getOption('block_category'),
'admin_label' => $desc,
'cache' => $display->getCacheType()
);
$this->derivatives[$delta] += $base_plugin_definition;
}

View File

@ -93,7 +93,6 @@ class ViewsExposedFilterBlock implements ContainerDerivativeInterface {
$desc = t('Exposed form: @view-@display_id', array('@view' => $view->id(), '@display_id' => $display->display['id']));
$this->derivatives[$delta] = array(
'admin_label' => $desc,
'cache' => DRUPAL_NO_CACHE,
);
$this->derivatives[$delta] += $base_plugin_definition;
}

View File

@ -2383,7 +2383,6 @@ abstract class DisplayPluginBase extends PluginBase {
$blocks[$delta] = array(
'info' => $desc,
'cache' => DRUPAL_NO_CACHE,
);
}

View File

@ -15,9 +15,6 @@ use Drupal\block\Plugin\views\display\Block;
if (!defined('BLOCK_LABEL_VISIBLE')) {
define('BLOCK_LABEL_VISIBLE', 'visible');
}
if (!defined('DRUPAL_NO_CACHE')) {
define('DRUPAL_NO_CACHE', -1);
}
/**
* Tests the views block plugin.

View File

@ -9,7 +9,6 @@ settings:
label: Administration
module: system
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'User login'
module: user
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: Tools
module: system
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: Breadcrumbs
module: system
label_display: '0'
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'Main page content'
module: system
label_display: '0'
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'Footer menu'
module: system
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'System Help'
module: system
label_display: '0'
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'User login'
module: user
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'Powered by Drupal'
module: system
label_display: '0'
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: Search
module: search
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: Tools
module: system
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: Breadcrumbs
module: system
label_display: '0'
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'Main page content'
module: system
label_display: '0'
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'System Help'
module: system
label_display: '0'
cache: -1
visibility:
path:
visibility: 0

View File

@ -9,7 +9,6 @@ settings:
label: 'User login'
module: user
label_display: visible
cache: -1
visibility:
path:
visibility: 0

View File

@ -0,0 +1,96 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Cache\CacheContextsTest.
*/
namespace Drupal\Tests\Core\Cache;
use Drupal\Core\Cache\CacheContexts;
use Drupal\Core\Cache\CacheContextInterface;
/**
* Fake cache context class.
*/
class FooCacheContext implements CacheContextInterface {
/**
* {@inheritdoc}
*/
public static function getLabel() {
return 'Foo';
}
/**
* {@inheritdoc}
*/
public function getContext() {
return 'bar';
}
}
/**
* Tests the CacheContexts service.
*
* @group Cache
*
* @see \Drupal\Core\Cache\CacheContexts
*/
class CacheContextsTest extends \PHPUnit_Framework_TestCase {
public static function getInfo() {
return array(
'name' => 'CacheContext test',
'description' => 'Tests cache contexts.',
'group' => 'Cache',
);
}
public function testContextPlaceholdersAreReplaced() {
$container = $this->getMockContainer();
$container->expects($this->once())
->method("get")
->with("cache_context.foo")
->will($this->returnValue(new FooCacheContext()));
$cache_contexts = new CacheContexts($container, $this->getContextsFixture());
$new_keys = $cache_contexts->convertTokensToKeys(
array("non-cache-context", "cache_context.foo")
);
$expected = array("non-cache-context", "bar");
$this->assertEquals($expected, $new_keys);
}
public function testAvailableContextStrings() {
$cache_contexts = new CacheContexts($this->getMockContainer(), $this->getContextsFixture());
$contexts = $cache_contexts->getAll();
$this->assertEquals(array("cache_context.foo"), $contexts);
}
public function testAvailableContextLabels() {
$container = $this->getMockContainer();
$container->expects($this->once())
->method("get")
->with("cache_context.foo")
->will($this->returnValue(new FooCacheContext()));
$cache_contexts = new CacheContexts($container, $this->getContextsFixture());
$labels = $cache_contexts->getLabels();
$expected = array("cache_context.foo" => "Foo");
$this->assertEquals($expected, $labels);
}
protected function getContextsFixture() {
return array('cache_context.foo');
}
protected function getMockContainer() {
return $this->getMockBuilder('Drupal\Core\DependencyInjection\Container')
->disableOriginalConstructor()
->getMock();
}
}