diff --git a/core/includes/common.inc b/core/includes/common.inc index 14e4357..cb9beef 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -5769,7 +5769,7 @@ function drupal_render_cache_set(&$markup, $elements) { } $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache'; $expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CacheBackendInterface::CACHE_PERMANENT; - $tags = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); + $tags = drupal_render_collect_cache_tags($elements, TRUE); cache($bin)->set($cid, $data, $expire, $tags); } @@ -5817,6 +5817,57 @@ function drupal_render_collect_attached($elements, $return = FALSE) { } /** + * Collects cache tags for an element and its children into a single array. + * + * When caching elements, it is necessary to collect all cache tags into a + * single array, from both the element itself and all child elements. This + * allows items to be invalidated based on all tags attached to the content + * they're constituted from. + * + * @param array $elements + * The element to collect cache tags from. + * @param bool $return + * Whether to return the attached elements and reset the internal static. + * + * @return array + * The cache tags array for this element and its descendants. + */ +function drupal_render_collect_cache_tags($elements, $return = FALSE) { + $tags = &drupal_static(__FUNCTION__, array()); + + // Collect all cache tags for this element. + if (isset($elements['#cache']['tags'])) { + foreach ($elements['#cache']['tags'] as $namespace => $values) { + if (is_array($values)) { + foreach ($values as $value) { + if (!isset($tags[$namespace][$value])) { + $tags[$namespace][$value] = $value; + } + } + } + else { + if (!isset($tags[$namespace])) { + $tags[$namespace] = $values; + } + } + } + } + if ($children = element_children($elements)) { + foreach ($children as $child) { + drupal_render_collect_cache_tags($elements[$child]); + } + } + + // If this was the first call to the function, return all attached elements + // and reset the static cache. + if ($return) { + $return = $tags; + $tags = array(); + return $return; + } +} + +/** * Prepares an element for caching based on a query. * * This smart caching strategy saves Drupal from querying and rendering to HTML diff --git a/core/includes/entity.inc b/core/includes/entity.inc index 37fb310..acc5b65 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -105,9 +105,13 @@ function entity_get_view_modes($entity_type = NULL) { else { $view_modes = module_invoke_all('entity_view_mode_info'); foreach ($view_modes as $type => $entity_info) { + $view_modes[$type] += array( + 'cache_default' => TRUE, + ); foreach ($entity_info as $view_mode => $view_mode_info) { $view_modes[$type][$view_mode] += array( 'custom_settings' => FALSE, + 'cache' => TRUE, ); } } @@ -588,14 +592,19 @@ function entity_render_controller($entity_type) { * @param string $langcode * (optional) For which language the entity should be rendered, defaults to * the current content language. + * @param bool $reset + * (optional) Whether to reset the render cache for the requested entity. + * Defaults to FALSE. * * @return array * A render array for the entity. */ -function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) { - return drupal_container()->get('plugin.manager.entity') - ->getRenderController($entity->entityType()) - ->view($entity, $view_mode, $langcode); +function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL, $reset = FALSE) { + $render_controller = drupal_container()->get('plugin.manager.entity')->getRenderController($entity->entityType()); + if ($reset) { + $render_controller->resetCache(array($entity)); + } + return $render_controller->view($entity, $view_mode, $langcode); } /** @@ -608,15 +617,20 @@ function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) { * @param string $langcode * (optional) For which language the entity should be rendered, defaults to * the current content language. + * @param bool $reset + * (optional) Whether to reset the render cache for the requested entities. + * Defaults to FALSE. * * @return array * A render array for the entities, indexed by the same keys as the * entities array passed in $entities. */ -function entity_view_multiple(array $entities, $view_mode, $langcode = NULL) { - return drupal_container()->get('plugin.manager.entity') - ->getRenderController(reset($entities)->entityType()) - ->viewMultiple($entities, $view_mode, $langcode); +function entity_view_multiple(array $entities, $view_mode, $langcode = NULL, $reset = FALSE) { + $render_controller = drupal_container()->get('plugin.manager.entity')->getRenderController(reset($entities)->entityType()); + if ($reset) { + $render_controller->resetCache($entities); + } + return $render_controller->viewMultiple($entities, $view_mode, $langcode); } /** diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 9092a13..face9db 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -169,6 +169,21 @@ public function resetCache(array $ids = NULL) { } /** + * Resets the entity render cache. + * + * @param array|null $entities + * (optional) If specified, the cache is reset for the given entities only. + */ + protected function resetRenderCache(array $entities = NULL) { + try { + drupal_container()->get('plugin.manager.entity')->getRenderController($this->entityType)->resetCache($entities); + } + catch (\Exception $e) { + // Nothing to do if the entity type doesn't have a render controller. + } + } + + /** * Implements \Drupal\Core\Entity\EntityStorageControllerInterface::load(). */ public function load(array $ids = NULL) { @@ -529,6 +544,9 @@ public function save(EntityInterface $entity) { $this->saveRevision($entity); } $this->resetCache(array($entity->id())); + // Reset the render cache as well. + $this->resetRenderCache(array($entity)); + $this->postSave($entity, TRUE); $this->invokeHook('update', $entity); } @@ -539,6 +557,8 @@ public function save(EntityInterface $entity) { } // Reset general caches, but keep caches specific to certain entities. $this->resetCache(array()); + // Reset the render cache as well. + $this->resetRenderCache(array($entity)); $entity->enforceIsNew(FALSE); $this->postSave($entity, FALSE); diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index e8998f8..031edb8 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -297,6 +297,9 @@ public function save(EntityInterface $entity) { $this->savePropertyData($entity); } $this->resetCache(array($entity->id())); + // Reset the render cache as well. + $this->resetRenderCache(array($entity)); + $this->postSave($entity, TRUE); $this->invokeHook('update', $entity); } @@ -313,6 +316,8 @@ public function save(EntityInterface $entity) { // Reset general caches, but keep caches specific to certain entities. $this->resetCache(array()); + // Reset the render cache as well. + $this->resetRenderCache(array($entity)); $entity->enforceIsNew(FALSE); $this->postSave($entity, FALSE); diff --git a/core/lib/Drupal/Core/Entity/EntityRenderController.php b/core/lib/Drupal/Core/Entity/EntityRenderController.php index 53457aa..a7297bf 100644 --- a/core/lib/Drupal/Core/Entity/EntityRenderController.php +++ b/core/lib/Drupal/Core/Entity/EntityRenderController.php @@ -20,8 +20,27 @@ class EntityRenderController implements EntityRenderControllerInterface { */ protected $entityType; + /** + * An array of view mode info for the type of entities for which this + * controller is instantiated. + * + * @var array + */ + protected $viewModesInfo; + + /** + * The cache bin used to store the render cache. + * + * @todo Defaults to 'cache' for now, until http://drupal.org/node/1194136 is + * fixed. + * + * @var string + */ + protected $cacheBin = 'cache'; + public function __construct($entity_type) { $this->entityType = $entity_type; + $this->viewModesInfo = entity_get_view_modes($entity_type); } /** @@ -156,10 +175,54 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la $build[$key]['#weight'] = $weight++; + // Cache the rendered output if permitted by the view mode settings. + if ((!isset($this->viewModesInfo[$view_mode]) && $this->viewModesInfo['cache_default']) || $this->viewModesInfo[$view_mode]['cache']) { + $build[$key]['#cache'] = array( + 'keys' => array('entity_view', $this->entityType ,$entity->id(), $view_mode), + 'granularity' => DRUPAL_CACHE_PER_ROLE, + 'bin' => $this->cacheBin, + 'tags' => array( + $this->entityType . '_view' => TRUE, + $this->entityType => array($entity->id()), + ), + ); + } + // Allow modules to modify the render array. drupal_alter(array($view_hook, 'entity_view'), $build[$key], $entity, $display); } return $build; } + + /** + * Implements \Drupal\Core\Entity\EntityRenderControllerInterface::resetCache(). + */ + public function resetCache(array $entities = NULL) { + if (isset($entities)) { + $tags = array(); + foreach ($entities as $entity) { + $tags[$this->entityType][] = $entity->id(); + + // @todo Remove when all entities are converted to EntityNG. + if (!$entity->getPropertyDefinitions()) { + continue; + } + + // Add all the referenced entity types and IDs to the tags that will be + // cleared. + foreach ($entity->getPropertyDefinitions() as $name => $definition) { + if ($definition['type'] == 'entity_reference_field') { + foreach ($entity->$name->getValue() as $target_id) { + $tags[$definition['settings']['target_type']][$target_id] = $target_id; + } + } + } + } + cache($this->cacheBin)->deleteTags($tags); + } + else { + cache($this->cacheBin)->deleteTags(array($this->entityType . '_view' => TRUE)); + } + } } diff --git a/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php index 03ba014..88e1461 100644 --- a/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php @@ -75,4 +75,12 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N * be available for loading. */ public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL); + + /** + * Resets the entity render cache. + * + * @param array|null $entities + * (optional) If specified, the cache is reset for the given entities only. + */ + public function resetCache(array $entities = NULL); } diff --git a/core/modules/block/lib/Drupal/block/BlockRenderController.php b/core/modules/block/lib/Drupal/block/BlockRenderController.php index e705310..0f73347 100644 --- a/core/modules/block/lib/Drupal/block/BlockRenderController.php +++ b/core/modules/block/lib/Drupal/block/BlockRenderController.php @@ -87,4 +87,10 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la return $build; } + /** + * Implements \Drupal\Core\Entity\EntityRenderControllerInterface::resetCache(). + */ + public function resetCache(array $entities = NULL) { + // @todo Move block render caching logic to this controller? + } } diff --git a/core/modules/entity/lib/Drupal/entity/Plugin/Core/Entity/EntityDisplay.php b/core/modules/entity/lib/Drupal/entity/Plugin/Core/Entity/EntityDisplay.php index 9048788..40774da 100644 --- a/core/modules/entity/lib/Drupal/entity/Plugin/Core/Entity/EntityDisplay.php +++ b/core/modules/entity/lib/Drupal/entity/Plugin/Core/Entity/EntityDisplay.php @@ -117,7 +117,12 @@ public function save() { if (empty($this->id)) { $this->id = $this->id(); } - return parent::save(); + $return = parent::save(); + + // Reset the render cache for the target entity type. + drupal_container()->get('plugin.manager.entity')->getRenderController($this->targetEntityType)->resetCache(); + + return $return; } /** diff --git a/core/modules/user/lib/Drupal/user/Tests/UserSignatureTest.php b/core/modules/user/lib/Drupal/user/Tests/UserSignatureTest.php index 31f01b9..41800dc 100644 --- a/core/modules/user/lib/Drupal/user/Tests/UserSignatureTest.php +++ b/core/modules/user/lib/Drupal/user/Tests/UserSignatureTest.php @@ -112,6 +112,9 @@ function testUserSignature() { $edit['comment_body[' . $langcode . '][0][format]'] = $this->full_html_format->format; $this->drupalPost('comment/' . $comment_id . '/edit', $edit, t('Save')); + // @todo Temporary cache clear. + drupal_container()->get('plugin.manager.entity')->getRenderController('node')->resetCache(array($node)); + // Assert that the signature did not make it through unfiltered. $this->drupalGet('node/' . $node->nid); $this->assertNoRaw($signature_text, 'Unfiltered signature text not found.');