Commit 2b90b083 authored by alexpott's avatar alexpott

Issue #2444231 by Wim Leers, nlisgo: Fix CacheableInterface so it makes sense...

Issue #2444231 by Wim Leers, nlisgo: Fix CacheableInterface so it makes sense (was: "Make Config objects & Entities implement CacheableInterface + add BubbleableMetadata::createFromCacheableObject()")
parent 0cfeca14
......@@ -104,7 +104,7 @@ public function checkNamedRoute($route_name, array $parameters = array(), Accoun
catch (ParamNotConvertedException $e) {
// Uncacheable because conversion of the parameter may not have been
// possible due to dynamic circumstances.
$result = AccessResult::forbidden()->setCacheable(FALSE);
$result = AccessResult::forbidden()->setCacheMaxAge(0);
return $return_as_object ? $result : $result->isAllowed();
}
}
......
......@@ -7,7 +7,7 @@
namespace Drupal\Core\Access;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Config\ConfigBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
......@@ -26,14 +26,7 @@
* When using ::orIf() and ::andIf(), cacheability metadata will be merged
* accordingly as well.
*/
abstract class AccessResult implements AccessResultInterface, CacheableInterface {
/**
* Whether the access result is cacheable.
*
* @var bool
*/
protected $isCacheable;
abstract class AccessResult implements AccessResultInterface, CacheableDependencyInterface {
/**
* The cache context IDs (to vary a cache item ID based on active contexts).
......@@ -63,9 +56,9 @@ abstract class AccessResult implements AccessResultInterface, CacheableInterface
* Constructs a new AccessResult object.
*/
public function __construct() {
$this->setCacheable(TRUE)
->resetCacheContexts()
$this->resetCacheContexts()
->resetCacheTags()
// Max-age must be non-zero for an access result to be cacheable.
// Typically, cache items are invalidated via associated cache tags, not
// via a maximum age.
->setCacheMaxAge(Cache::PERMANENT);
......@@ -215,13 +208,6 @@ public function isNeutral() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheKeys() {
return [];
}
/**
* {@inheritdoc}
*/
......@@ -237,16 +223,6 @@ public function getCacheTags() {
return $this->tags;
}
/**
* {@inheritdoc}
*
* It's not very useful to cache individual access results, but the interface
* forces us to implement this method, so just use the default cache bin.
*/
public function getCacheBin() {
return 'default';
}
/**
* {@inheritdoc}
*/
......@@ -254,26 +230,6 @@ public function getCacheMaxAge() {
return $this->maxAge;
}
/**
* {@inheritdoc}
*/
public function isCacheable() {
return $this->isCacheable;
}
/**
* Sets whether this access result is cacheable. It is cacheable by default.
*
* @param bool $is_cacheable
* Whether this access result is cacheable.
*
* @return $this
*/
public function setCacheable($is_cacheable) {
$this->isCacheable = $is_cacheable;
return $this;
}
/**
* Adds cache contexts associated with the access result.
*
......@@ -392,19 +348,19 @@ public function orIf(AccessResultInterface $other) {
$merge_other = FALSE;
if ($this->isForbidden() || $other->isForbidden()) {
$result = static::forbidden();
if (!$this->isForbidden() || (!$this->isCacheable() && $other->isForbidden())) {
if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
$merge_other = TRUE;
}
}
elseif ($this->isAllowed() || $other->isAllowed()) {
$result = static::allowed();
if (!$this->isAllowed() || (!$this->isCacheable() && $other->isAllowed())) {
if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed())) {
$merge_other = TRUE;
}
}
else {
$result = static::neutral();
if (!$this->isNeutral() || (!$this->isCacheable() && $other->isNeutral())) {
if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral())) {
$merge_other = TRUE;
}
}
......@@ -446,8 +402,8 @@ public function andIf(AccessResultInterface $other) {
// result must also not be cacheable, except if the other access result
// has isForbidden() === TRUE. isForbidden() access results are contagious
// in that they propagate regardless of the other value.
if (!$this->isCacheable() && !$result->isForbidden()) {
$result->setCacheable(FALSE);
if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
$result->setCacheMaxAge(0);
}
}
return $result;
......@@ -462,17 +418,21 @@ public function andIf(AccessResultInterface $other) {
* @return $this
*/
public function inheritCacheability(AccessResultInterface $other) {
if ($other instanceof CacheableInterface) {
$this->setCacheable($other->isCacheable());
if ($other instanceof CacheableDependencyInterface) {
if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
}
else {
$this->setCacheMaxAge($other->getCacheMaxAge());
}
$this->addCacheContexts($other->getCacheContexts());
$this->addCacheTags($other->getCacheTags());
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
}
// If any of the access results don't provide cacheability metadata, then
// we cannot cache the combined access result, for we may not make
// assumptions.
else {
$this->setCacheable(FALSE);
$this->setCacheMaxAge(0);
}
return $this;
}
......
......@@ -66,7 +66,7 @@ public function access(Route $route, Request $request, RouteMatchInterface $rout
$result = AccessResult::forbidden();
}
// Not cacheable because the CSRF token is highly dynamic.
return $result->setCacheable(FALSE);
return $result->setCacheMaxAge(0);
}
}
......@@ -131,7 +131,7 @@ public function access(AccountInterface $account, $return_as_object = FALSE) {
$access = AccessResult::forbidden();
}
$access->setCacheable(FALSE);
$access->setCacheMaxAge(0);
return $return_as_object ? $access : $access->isAllowed();
}
......
<?php
/**
* @file
* Contains \Drupal\Core\CacheableDependencyInterface
*/
namespace Drupal\Core\Cache;
/**
* Defines an interface for objects which may be used by other cached objects.
*
* All cacheability metadata exposed in this interface is bubbled to parent
* objects when they are cached: if a child object needs to be varied by certain
* cache contexts, invalidated by certain cache tags, expire after a certain
* maximum age, then so should any parent object.
*
* @ingroup cache
*/
interface CacheableDependencyInterface {
/**
* The cache contexts associated with this object.
*
* These identify a specific variation/representation of the object.
*
* Cache contexts are tokens: placeholders that are converted to cache keys by
* the @cache_contexts service. The replacement value depends on the request
* context (the current URL, language, and so on). They're converted before
* storing an object in cache.
*
* @return string[]
* An array of cache context tokens, used to generate a cache ID.
*
* @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
*/
public function getCacheContexts();
/**
* The cache tags associated with this object.
*
* When this object is modified, these cache tags will be invalidated.
*
* @return string[]
* A set of cache tags.
*/
public function getCacheTags();
/**
* The maximum age for which this object may be cached.
*
* @return int
* The maximum time in seconds that this object may be cached.
*/
public function getCacheMaxAge();
}
......@@ -20,7 +20,7 @@
*
* @ingroup cache
*/
interface CacheableInterface {
interface CacheableInterface extends CacheableDependencyInterface {
/**
* The cache keys associated with this potentially cacheable object.
......@@ -32,39 +32,6 @@ interface CacheableInterface {
*/
public function getCacheKeys();
/**
* The cache contexts associated with this potentially cacheable object.
*
* These identify a specific variation/representation of the object.
*
* Cache contexts are tokens: placeholders that are converted to cache keys by
* the @cache_contexts service. The replacement value depends on the request
* context (the current URL, language, and so on). They're converted before
* storing an object in cache.
*
* @return string[]
* An array of cache context tokens, used to generate a cache ID.
*
* @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
*/
public function getCacheContexts();
/**
* The cache tags associated with this potentially cacheable object.
*
* @return string[]
* An array of cache tags.
*/
public function getCacheTags();
/**
* The maximum age for which this object may be cached.
*
* @return int
* The maximum time in seconds that this object may be cached.
*/
public function getCacheMaxAge();
/**
* Indicates whether this object is cacheable.
*
......
......@@ -66,7 +66,8 @@ public function getContext($type = NULL) {
return implode(',', $context_parts);
}
else {
if (!in_array($type, $this->languageManager->getLanguageTypes())) {
$language_types = $this->languageManager->getDefinedLanguageTypesInfo();
if (!isset($language_types[$type])) {
throw new \RuntimeException(sprintf('The language type "%s" is invalid.', $type));
}
return $this->languageManager->getCurrentLanguage($type)->getId();
......
......@@ -9,6 +9,8 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
......@@ -26,7 +28,7 @@
* @see \Drupal\Core\Config\Config
* @see \Drupal\Core\Theme\ThemeSettings
*/
abstract class ConfigBase {
abstract class ConfigBase implements CacheableDependencyInterface {
use DependencySerializationTrait;
/**
......@@ -264,13 +266,24 @@ public function merge(array $data_to_merge) {
}
/**
* The unique cache tag associated with this configuration object.
*
* @return string[]
* An array of cache tags.
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return ['config:' . $this->name];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}
......@@ -426,6 +426,13 @@ public function referencedEntities() {
return array();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
}
/**
* {@inheritdoc}
*/
......@@ -434,6 +441,13 @@ public function getCacheTags() {
return [$this->entityTypeId . ':' . $this->id()];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*
......
......@@ -8,13 +8,14 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
/**
* Defines a common interface for all entity objects.
*
* @ingroup entity_api
*/
interface EntityInterface extends AccessibleInterface {
interface EntityInterface extends AccessibleInterface, CacheableDependencyInterface {
/**
* Returns the entity UUID (Universally Unique Identifier).
......@@ -396,14 +397,6 @@ public function toArray();
*/
public function getTypedData();
/**
* The unique cache tag associated with this entity.
*
* @return string[]
* An array of cache tags.
*/
public function getCacheTags();
/**
* Gets the key that is used to store configuration dependencies.
*
......
......@@ -169,12 +169,16 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
// Collect cache defaults for this entity.
'#cache' => array(
'tags' => Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags()),
'contexts' => [
'user.roles',
],
'contexts' => $entity->getCacheContexts(),
'max-age' => $entity->getCacheMaxAge(),
),
);
// @todo Remove when https://www.drupal.org/node/2099137 lands.
$build['#cache']['contexts'] = Cache::mergeContexts($build['#cache']['contexts'], [
'user.roles',
]);
// Cache the rendered output if permitted by the view mode and global entity
// type configuration.
if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) {
......
......@@ -9,7 +9,7 @@
use Drupal\Component\Datetime\DateTimePlus;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheContexts;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
......@@ -135,7 +135,7 @@ public function onRespond(FilterResponseEvent $event) {
// Apply the request's access result cacheability metadata, if it has any.
$access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
if ($access_result instanceof CacheableInterface) {
if ($access_result instanceof CacheableDependencyInterface) {
$this->updateDrupalCacheHeaders($response, $access_result);
}
......@@ -161,10 +161,10 @@ public function onRespond(FilterResponseEvent $event) {
/**
* Updates Drupal's cache headers using the route's cacheable access result.
*
* @param Response $response
* @param CacheableInterface $cacheable_access_result
* @param \Symfony\Component\HttpFoundation\Response $response
* @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheable_access_result
*/
protected function updateDrupalCacheHeaders(Response $response, CacheableInterface $cacheable_access_result) {
protected function updateDrupalCacheHeaders(Response $response, CacheableDependencyInterface $cacheable_access_result) {
// X-Drupal-Cache-Tags
$cache_tags = $cacheable_access_result->getCacheTags();
if ($response->headers->has('X-Drupal-Cache-Tags')) {
......
......@@ -9,13 +9,14 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
/**
* Value object used for bubbleable rendering metadata.
*
* @see \Drupal\Core\Render\RendererInterface::render()
*/
class BubbleableMetadata {
class BubbleableMetadata implements CacheableDependencyInterface {
/**
* Cache contexts.
......@@ -107,6 +108,22 @@ public static function createFromRenderArray(array $build) {
return $meta;
}
/**
* Creates a bubbleable metadata object from a cacheable depended object.
*
* @param \Drupal\Core\Cache\CacheableDependencyInterface $object
* The object whose cacheability metadata to retrieve.
*
* @return static
*/
public static function createFromObject(CacheableDependencyInterface $object) {
$meta = new static();
$meta->contexts = $object->getCacheContexts();
$meta->tags = $object->getCacheTags();
$meta->maxAge = $object->getCacheMaxAge();
return $meta;
}
/**
* Gets cache tags.
*
......
......@@ -10,6 +10,7 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheContexts;
use Drupal\Core\Cache\CacheFactoryInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
......@@ -788,6 +789,15 @@ public static function mergeBubbleableMetadata(array $a, array $b) {
return $a;
}
/**
* {@inheritdoc}
*/
public function addDependency(array &$elements, CacheableDependencyInterface $dependency) {
$meta_a = BubbleableMetadata::createFromRenderArray($elements);
$meta_b = BubbleableMetadata::createFromObject($dependency);
$meta_a->merge($meta_b)->applyTo($elements);
}
/**
* {@inheritdoc}
*/
......
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Render;
use Drupal\Core\Cache\CacheableDependencyInterface;
/**
* Defines an interface for turning a render array into a string.
*/
......@@ -340,6 +342,20 @@ public function getCacheableRenderArray(array $elements);
*/
public static function mergeBubbleableMetadata(array $a, array $b);
/**
* Adds a dependency on an object: merges its cacheability metadata.
*
* E.g. when a render array depends on some configuration, an entity, or an
* access result, we must make sure their cacheability metadata is present on
* the render array. This method makes doing that simple.
*
* @param array &$elements
* The render array to update.
* @param \Drupal\Core\Cache\CacheableDependencyInterface $dependency
* The dependency.
*/
public function addDependency(array &$elements, CacheableDependencyInterface $dependency);
/**
* Merges two attachments arrays (which live under the '#attached' key).
*
......
......@@ -93,7 +93,7 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
$this->contextHandler->applyContextMapping($condition, $contexts);
}
catch (ContextException $e) {
return AccessResult::forbidden()->setCacheable(FALSE);
return AccessResult::forbidden()->setCacheMaxAge(0);
}
}
$conditions[$condition_id] = $condition;
......@@ -111,7 +111,7 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
// result should be varied.
// @todo Change this to use $access->cacheUntilEntityChanges($entity) once
// https://www.drupal.org/node/2375695 is resolved.
return $access->setCacheable(FALSE);
return $access->setCacheMaxAge(0);
}
}
......
......@@ -6,7 +6,7 @@ services:
- { name: breadcrumb_builder, priority: 701 }
book.manager:
class: Drupal\book\BookManager
arguments: ['@entity.manager', '@string_translation', '@config.factory', '@book.outline_storage']
arguments: ['@entity.manager', '@string_translation', '@config.factory', '@book.outline_storage', '@renderer']
book.outline:
class: Drupal\book\BookOutline
arguments: ['@book.manager']
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
......@@ -63,14 +64,22 @@ class BookManager implements BookManagerInterface {
*/
protected $bookTreeFlattened;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a BookManager object.
*/
public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookOutlineStorageInterface $book_outline_storage) {
public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookOutlineStorageInterface $book_outline_storage, RendererInterface $renderer) {
$this->entityManager = $entity_manager;
$this->stringTranslation = $translation;
$this->configFactory = $config_factory;
$this->bookOutlineStorage = $book_outline_storage;
$this->renderer = $renderer;
}
/**
......@@ -353,7 +362,7 @@ protected function addParentSelectFormElements(array $book_link) {
'#suffix' => '</div>',
);
}
$form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [], $config->getCacheTags());
$this->renderer->addDependency($form, $config);
return $form;
}
......
......@@ -37,6 +37,13 @@ class BookManagerTest extends UnitTestCase {
*/
protected $translation;
/**
* The mocked renderer.
*
* @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $renderer;
/**
* The tested book manager.
*
......@@ -59,7 +66,8 @@ protected function setUp() {
$this->translation = $this->getStringTranslationStub();
$this->configFactory = $this->getConfigFactoryStub(array());
$this->bookOutlineStorage = $this->getMock('Drupal\book\BookOutlineStorageInterface');
$this->bookManager = new BookManager($this->entityManager, $this->translation, $this->configFactory, $this->bookOutlineStorage);
$this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
$this->bookManager = new BookManager($this->entityManager, $this->translation, $this->configFactory, $this->bookOutlineStorage, $this->renderer);
}
/**
......
......@@ -16,6 +16,7 @@
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -31,13 +32,21 @@ class CommentForm extends ContentEntityForm {
*/
protected $currentUser;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('current_user')
$container->get('current_user'),
$container->get('renderer')
);
}
......@@ -48,10 +57,13 @@ public static function create(ContainerInterface $container) {
* The entity manager service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(EntityManagerInterface $entity_manager, AccountInterface $current_user) {
public function __construct(EntityManagerInterface $entity_manager, AccountInterface $current_user, RendererInterface $renderer) {
parent::__construct($entity_manager);
$this->currentUser = $current_user;
$this->renderer = $renderer;
}
/**
......@@ -213,12 +225,9 @@ public function form(array $form, FormStateInterface $form_state) {
'#access' => $is_admin,
);
$form['#cache']['tags'] = Cache::mergeTags(
isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],
$config->getCacheTags(),
// The form depends on the field definition.
$field_definition->getConfig($entity->bundle())->getCacheTags()
);
$this->renderer->addDependency($form, $config);
// The form depends on the field definition.
$this->renderer->addDependency($form, $field_definition->getConfig($entity->bundle()));
return parent::form($form, $form_state, $comment);
}
......
......@@ -12,6 +12,7 @@
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Flood\FloodInterface;
use Drupal\contact\ContactFormInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\user\UserInterface;
use Drupal\Component\Utility\SafeMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -37,6 +38,13 @@ class ContactController extends ControllerBase {
*/
protected $dateFormatter;
/**
* The renderer.
*