diff --git a/core/includes/common.inc b/core/includes/common.inc index a4805c0..9ef18d0 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -4060,7 +4060,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); } @@ -4108,6 +4108,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 2b3885e..2565630 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -48,6 +48,18 @@ function entity_info_cache_clear() { } /** + * Clears the entity render cache for all entity types. + */ +function entity_render_cache_clear() { + $entity_manager = Drupal::entityManager(); + foreach ($entity_manager->getDefinitions() as $entity_type => $info) { + if ($entity_manager->hasController($entity_type, 'render')) { + $entity_manager->getRenderController($entity_type)->resetCache(); + } + } +} + +/** * Returns the entity bundle info. * * @param string|null $entity_type @@ -622,14 +634,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::entityManager() - ->getRenderController($entity->entityType()) - ->view($entity, $view_mode, $langcode); +function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL, $reset = FALSE) { + $render_controller = Drupal::entityManager()->getRenderController($entity->entityType()); + if ($reset) { + $render_controller->resetCache(array($entity)); + } + return $render_controller->view($entity, $view_mode, $langcode); } /** @@ -642,15 +659,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::entityManager() - ->getRenderController(reset($entities)->entityType()) - ->viewMultiple($entities, $view_mode, $langcode); +function entity_view_multiple(array $entities, $view_mode, $langcode = NULL, $reset = FALSE) { + $render_controller = Drupal::entityManager()->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/Annotation/EntityType.php b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php index b8e4061..79e5c52 100644 --- a/core/lib/Drupal/Core/Entity/Annotation/EntityType.php +++ b/core/lib/Drupal/Core/Entity/Annotation/EntityType.php @@ -138,6 +138,14 @@ class EntityType extends Plugin { public $static_cache = TRUE; /** + * Boolean indicating whether the rendered output of entities should be + * cached. + * + * @var bool (optional) + */ + public $render_cache = TRUE; + + /** * Boolean indicating whether entities of this type have multilingual support. * * At an entity level, this indicates language support and at a bundle level diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index ec05c1a..36e5287 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -8,11 +8,13 @@ namespace Drupal\Core\Entity; use Drupal\Component\Uuid\Uuid; +use Drupal\Core\Entity\Plugin\DataType\EntityReferenceItem; use Drupal\Core\Language\Language; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TypedDataInterface; use IteratorAggregate; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Cache\Cache; /** * Defines a base entity class. @@ -354,7 +356,9 @@ public function getTranslationLanguages($include_default = TRUE) { * Implements \Drupal\Core\Entity\EntityInterface::save(). */ public function save() { - return \Drupal::entityManager()->getStorageController($this->entityType)->save($this); + $return = \Drupal::entityManager()->getStorageController($this->entityType)->save($this); + $this->changed(); + return $return; } /** @@ -363,6 +367,7 @@ public function save() { public function delete() { if (!$this->isNew()) { \Drupal::entityManager()->getStorageController($this->entityType)->delete(array($this->id() => $this)); + $this->changed(); } } @@ -642,4 +647,51 @@ public function initTranslation($langcode) { // http://drupal.org/node/2004244 } + /** + * {@inheritdoc} + */ + public function relatedEntities() { + $relationships = array(); + $id = $this->id(); + + // @todo Remove when all entities are converted to EntityNG. + if (!$this->getPropertyDefinitions()) { + return $relationships; + } + + // Add all the referenced entity types and IDs to the tags that will be + // cleared. + foreach ($this->getProperties() as $name => $definition) { + $property = $this->get($name)->offsetGet(0); + if ($property instanceof EntityReferenceItem && $entity = $property->entity) { + $relationships[] = $entity; + } + } + + return $relationships; + } + + /** + * {@inheritdoc} + */ + public function changed() { + $tags = array( + $this->entityType() => array($this->id()), + ); + + foreach ($this->relatedEntities() as $related_entity) { + $related_entity_type = $related_entity->entityType(); + + if (!isset($tags[$related_entity_type])) { + $tags[$related_entity_type] = array(); + } + + $tags[$related_entity_type][] = $related_entity->id(); + } + + // @todo: Move cache tag invalidation to an event listener so that the + // service isn't required here. + Cache::deleteTags($tags); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php index a3ffcb9..55ee3b4 100644 --- a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php +++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php @@ -626,4 +626,20 @@ public function initTranslation($langcode) { $this->decorated->initTranslation($langcode); } + /** + * {@inheritdoc} + * + * @return array + */ + public function relatedEntities() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function changed(array $tags = array()) { + $this->decorated->changed($tags); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index e965b8e..a1a6642 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -326,4 +326,16 @@ public function isTranslatable(); */ public function initTranslation($langcode); + /** + * @todo + * + * @return array + */ + public function relatedEntities(); + + /** + * @todo + */ + public function changed(); + } diff --git a/core/lib/Drupal/Core/Entity/EntityRenderController.php b/core/lib/Drupal/Core/Entity/EntityRenderController.php index cd92f52..54f8ca1 100644 --- a/core/lib/Drupal/Core/Entity/EntityRenderController.php +++ b/core/lib/Drupal/Core/Entity/EntityRenderController.php @@ -6,9 +6,10 @@ */ namespace Drupal\Core\Entity; -use Drupal\entity\Plugin\Core\Entity\EntityDisplay; +use Drupal\Core\Entity\Plugin\DataType\EntityReferenceItem; use Drupal\Core\Language\Language; +use Drupal\entity\Plugin\Core\Entity\EntityDisplay; /** * Base class for entity view controllers. @@ -22,8 +23,37 @@ class EntityRenderController implements EntityRenderControllerInterface { */ protected $entityType; + /** + * The entity info array. + * + * @var array + * + * @see entity_get_info() + */ + protected $entityInfo; + + /** + * 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->entityInfo = entity_get_info($entity_type); + $this->viewModesInfo = entity_get_view_modes($entity_type); } /** @@ -62,6 +92,22 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco '#view_mode' => $view_mode, '#langcode' => $langcode, ); + + // Cache the rendered output if permitted by the view mode settings. + $view_mode_is_cacheable = !isset($this->viewModesInfo[$view_mode]) || (isset($this->viewModesInfo[$view_mode]) && $this->viewModesInfo[$view_mode]['cache']); + if (!isset($entity->in_preview) && $this->entityInfo['render_cache'] && $view_mode_is_cacheable) { + $request = \Drupal::request(); + $return['#cache'] = array( + 'keys' => array('entity_view', $this->entityType ,$entity->id(), $view_mode, $request->getQueryString()), + 'granularity' => DRUPAL_CACHE_PER_ROLE, + 'bin' => $this->cacheBin, + 'tags' => array( + $this->entityType . '_view' => TRUE, + $this->entityType => array($entity->id()), + ), + ); + } + return $return; } @@ -164,4 +210,40 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la return $build; } + + /** + * {@inheritdoc} + */ + public function resetCache(array $entities = NULL) { + if (isset($entities)) { + $tags = array(); + foreach ($entities as $entity) { + $id = $entity->id(); + $tags[$this->entityType][$id] = $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->getProperties() as $name => $definition) { + if ($entity->get($name)->offsetGet(0) instanceof EntityReferenceItem && $field_values = $entity->get($name)->getValue()) { + foreach (array_filter($field_values) as $value) { + // @todo This looks very ugly, ask the Entity API people for a + // nicer way to do it. + $field_item_definition = $entity->get($name)->offsetGet(0)->getPropertyDefinitions(); + $target_entity_type = $field_item_definition['entity']['constraints']['EntityType']; + $tags[$target_entity_type][$value['target_id']] = $value['target_id']; + } + } + } + } + \Drupal::cache($this->cacheBin)->deleteTags($tags); + } + else { + \Drupal::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..eb8f41b 100644 --- a/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityRenderControllerInterface.php @@ -75,4 +75,13 @@ 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/custom_block/config/entity.view_mode.custom_block.full.yml b/core/modules/block/custom_block/config/entity.view_mode.custom_block.full.yml index ebacec5..6c2a3d1 100644 --- a/core/modules/block/custom_block/config/entity.view_mode.custom_block.full.yml +++ b/core/modules/block/custom_block/config/entity.view_mode.custom_block.full.yml @@ -1,4 +1,5 @@ id: custom_block.full label: Full status: '0' +cache: '1' targetEntityType: custom_block diff --git a/core/modules/block/lib/Drupal/block/BlockRenderController.php b/core/modules/block/lib/Drupal/block/BlockRenderController.php index 45a85d9..5523696 100644 --- a/core/modules/block/lib/Drupal/block/BlockRenderController.php +++ b/core/modules/block/lib/Drupal/block/BlockRenderController.php @@ -62,4 +62,10 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la return $build; } + /** + * {@inheritdoc} + */ + public function resetCache(array $entities = NULL) { + // @todo Move block render caching logic to this controller? + } } diff --git a/core/modules/book/config/entity.view_mode.node.print.yml b/core/modules/book/config/entity.view_mode.node.print.yml index fe45f50..6f7333f 100644 --- a/core/modules/book/config/entity.view_mode.node.print.yml +++ b/core/modules/book/config/entity.view_mode.node.print.yml @@ -1,4 +1,5 @@ id: node.print label: Print status: '0' +cache: '1' targetEntityType: node diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc index 4998f83..9a913ee 100644 --- a/core/modules/comment/comment.pages.inc +++ b/core/modules/comment/comment.pages.inc @@ -81,6 +81,7 @@ function comment_reply(EntityInterface $node, $pid = NULL) { // This is the case where the comment is in response to a node. Display the node. elseif (user_access('access content')) { $build['comment_node'] = node_view($node); + unset($build['comment_node']['#cache']); } // Should we show the reply box? diff --git a/core/modules/comment/config/entity.view_mode.comment.full.yml b/core/modules/comment/config/entity.view_mode.comment.full.yml index e48fbd7..abfc646 100644 --- a/core/modules/comment/config/entity.view_mode.comment.full.yml +++ b/core/modules/comment/config/entity.view_mode.comment.full.yml @@ -1,4 +1,5 @@ id: comment.full label: Full comment status: '0' +cache: '1' targetEntityType: comment diff --git a/core/modules/comment/lib/Drupal/comment/CommentRenderController.php b/core/modules/comment/lib/Drupal/comment/CommentRenderController.php index c6b21f0..14585d6 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentRenderController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentRenderController.php @@ -79,9 +79,10 @@ protected function alterBuild(array &$build, EntityInterface $comment, EntityDis $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $comment->bundle(), COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED; - // Add 'new' anchor if needed. + // Add 'new' anchor and disable render cache if needed. if (!empty($comment->first_new)) { $prefix .= "\n"; + unset($build['#cache']); } // Add indentation div or close open divs as needed. diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentAnonymousTest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentAnonymousTest.php index f4ef8cf..f6d595c 100644 --- a/core/modules/comment/lib/Drupal/comment/Tests/CommentAnonymousTest.php +++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentAnonymousTest.php @@ -148,6 +148,8 @@ function testAnonymous() { 'post comments' => FALSE, 'skip comment approval' => FALSE, )); + \Drupal::entityManager()->getRenderController('node')->resetCache(); + \Drupal::entityManager()->getRenderController('comment')->resetCache(); $this->drupalGet('node/' . $this->node->nid); $this->assertPattern('@