Commit d7fa47b4 authored by catch's avatar catch

Issue #1605290 by amateescu, msonnabaum, Caseledde, damiankloip, beejeebus,...

Issue #1605290 by amateescu, msonnabaum, Caseledde, damiankloip, beejeebus, catch, Berdir, Wim Leers: Enable entity render caching with cache tag support.
parent bb29548d
...@@ -4139,7 +4139,7 @@ function drupal_render_cache_set(&$markup, $elements) { ...@@ -4139,7 +4139,7 @@ function drupal_render_cache_set(&$markup, $elements) {
} }
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache'; $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
$expire = isset($elements['#cache']['expire']) ? $elements['#cache']['expire'] : CacheBackendInterface::CACHE_PERMANENT; $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);
cache($bin)->set($cid, $data, $expire, $tags); cache($bin)->set($cid, $data, $expire, $tags);
} }
...@@ -4186,6 +4186,50 @@ function drupal_render_collect_attached($elements, $return = FALSE) { ...@@ -4186,6 +4186,50 @@ function drupal_render_collect_attached($elements, $return = FALSE) {
} }
} }
/**
* Collects cache tags for an element and its children into a single array.
*
* The cache tags array is returned in a format that is valid for
* \Drupal\Core\Cache\CacheBackendInterface::set().
*
* 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 $element
* The element to collect cache tags from.
* @param array $tags
* (optional) An array of already collected cache tags (i.e. from a parent
* element). Defaults to an empty array.
*
* @return array
* The cache tags array for this element and its descendants.
*/
function drupal_render_collect_cache_tags($element, $tags = array()) {
if (isset($element['#cache']['tags'])) {
foreach ($element['#cache']['tags'] as $namespace => $values) {
if (is_array($values)) {
foreach ($values as $value) {
$tags[$namespace][$value] = $value;
}
}
else {
if (!isset($tags[$namespace])) {
$tags[$namespace] = $values;
}
}
}
}
if ($children = element_children($element)) {
foreach ($children as $child) {
$tags = drupal_render_collect_cache_tags($element[$child], $tags);
}
}
return $tags;
}
/** /**
* Prepares an element for caching based on a query. * Prepares an element for caching based on a query.
* *
......
...@@ -46,6 +46,18 @@ function entity_info_cache_clear() { ...@@ -46,6 +46,18 @@ function entity_info_cache_clear() {
\Drupal::entityManager()->clearCachedFieldDefinitions(); \Drupal::entityManager()->clearCachedFieldDefinitions();
} }
/**
* 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. * Returns the entity bundle info.
* *
...@@ -611,14 +623,19 @@ function entity_render_controller($entity_type) { ...@@ -611,14 +623,19 @@ function entity_render_controller($entity_type) {
* @param string $langcode * @param string $langcode
* (optional) For which language the entity should be rendered, defaults to * (optional) For which language the entity should be rendered, defaults to
* the current content language. * the current content language.
* @param bool $reset
* (optional) Whether to reset the render cache for the requested entity.
* Defaults to FALSE.
* *
* @return array * @return array
* A render array for the entity. * A render array for the entity.
*/ */
function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) { function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL, $reset = FALSE) {
return \Drupal::entityManager() $render_controller = \Drupal::entityManager()->getRenderController($entity->entityType());
->getRenderController($entity->entityType()) if ($reset) {
->view($entity, $view_mode, $langcode); $render_controller->resetCache(array($entity->id()));
}
return $render_controller->view($entity, $view_mode, $langcode);
} }
/** /**
...@@ -631,15 +648,20 @@ function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) { ...@@ -631,15 +648,20 @@ function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) {
* @param string $langcode * @param string $langcode
* (optional) For which language the entity should be rendered, defaults to * (optional) For which language the entity should be rendered, defaults to
* the current content language. * the current content language.
* @param bool $reset
* (optional) Whether to reset the render cache for the requested entities.
* Defaults to FALSE.
* *
* @return array * @return array
* A render array for the entities, indexed by the same keys as the * A render array for the entities, indexed by the same keys as the
* entities array passed in $entities. * entities array passed in $entities.
*/ */
function entity_view_multiple(array $entities, $view_mode, $langcode = NULL) { function entity_view_multiple(array $entities, $view_mode, $langcode = NULL, $reset = FALSE) {
return \Drupal::entityManager() $render_controller = \Drupal::entityManager()->getRenderController(reset($entities)->entityType());
->getRenderController(reset($entities)->entityType()) if ($reset) {
->viewMultiple($entities, $view_mode, $langcode); $render_controller->resetCache(array_keys($entities));
}
return $render_controller->viewMultiple($entities, $view_mode, $langcode);
} }
/** /**
......
...@@ -137,6 +137,14 @@ class EntityType extends Plugin { ...@@ -137,6 +137,14 @@ class EntityType extends Plugin {
*/ */
public $static_cache = TRUE; 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. * Boolean indicating whether entities of this type have multilingual support.
* *
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity; namespace Drupal\Core\Entity;
use Drupal\Component\Uuid\Uuid; use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Entity\Plugin\DataType\EntityReferenceItem;
use Drupal\Core\Language\Language; use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataInterface;
...@@ -611,6 +612,7 @@ public function preSave(EntityStorageControllerInterface $storage_controller) { ...@@ -611,6 +612,7 @@ public function preSave(EntityStorageControllerInterface $storage_controller) {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) { public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
$this->changed();
} }
/** /**
...@@ -635,6 +637,9 @@ public static function preDelete(EntityStorageControllerInterface $storage_contr ...@@ -635,6 +637,9 @@ public static function preDelete(EntityStorageControllerInterface $storage_contr
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) { public static function postDelete(EntityStorageControllerInterface $storage_controller, array $entities) {
foreach ($entities as $entity) {
$entity->changed();
}
} }
/** /**
...@@ -698,4 +703,47 @@ public static function baseFieldDefinitions($entity_type) { ...@@ -698,4 +703,47 @@ public static function baseFieldDefinitions($entity_type) {
return array(); return array();
} }
/**
* {@inheritdoc}
*/
public function referencedEntities() {
$referenced_entities = array();
// @todo Remove when all entities are converted to EntityNG.
if (!$this->getPropertyDefinitions()) {
return $referenced_entities;
}
// Gather a list of referenced entities.
foreach ($this->getProperties() as $name => $definition) {
$field_items = $this->get($name);
foreach ($field_items as $offset => $field_item) {
if ($field_item instanceof EntityReferenceItem && $entity = $field_item->entity) {
$referenced_entities[] = $entity;
}
}
}
return $referenced_entities;
}
/**
* {@inheritdoc}
*/
public function changed() {
$referenced_entity_ids = array(
$this->entityType() => array($this->id() => TRUE),
);
foreach ($this->referencedEntities() as $referenced_entity) {
$referenced_entity_ids[$referenced_entity->entityType()][$referenced_entity->id()] = TRUE;
}
foreach ($referenced_entity_ids as $entity_type => $entity_ids) {
if (\Drupal::entityManager()->hasController($entity_type, 'render')) {
\Drupal::entityManager()->getRenderController($entity_type)->resetCache(array_keys($entity_ids));
}
}
}
} }
...@@ -325,4 +325,17 @@ public function initTranslation($langcode); ...@@ -325,4 +325,17 @@ public function initTranslation($langcode);
*/ */
public static function baseFieldDefinitions($entity_type); public static function baseFieldDefinitions($entity_type);
/**
* Returns a list of entities referenced by this entity.
*
* @return array
* An array of entities.
*/
public function referencedEntities();
/**
* Acts on an entity after it was saved or deleted.
*/
public function changed();
} }
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
*/ */
namespace Drupal\Core\Entity; namespace Drupal\Core\Entity;
use Drupal\entity\Entity\EntityDisplay;
use Drupal\entity\Entity\EntityDisplay;
use Drupal\Core\Language\Language; use Drupal\Core\Language\Language;
/** /**
...@@ -22,8 +22,37 @@ class EntityRenderController implements EntityRenderControllerInterface { ...@@ -22,8 +22,37 @@ class EntityRenderController implements EntityRenderControllerInterface {
*/ */
protected $entityType; 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) { public function __construct($entity_type) {
$this->entityType = $entity_type; $this->entityType = $entity_type;
$this->entityInfo = entity_get_info($entity_type);
$this->viewModesInfo = entity_get_view_modes($entity_type);
} }
/** /**
...@@ -80,6 +109,23 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco ...@@ -80,6 +109,23 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
'#view_mode' => $view_mode, '#view_mode' => $view_mode,
'#langcode' => $langcode, '#langcode' => $langcode,
); );
// Cache the rendered output if permitted by the view mode and global entity
// type configuration. The isset() checks below are necessary because
// 'default' is not an actual view mode.
$view_mode_is_cacheable = !isset($this->viewModesInfo[$view_mode]) || (isset($this->viewModesInfo[$view_mode]) && $this->viewModesInfo[$view_mode]['cache']);
if ($view_mode_is_cacheable && !$entity->isNew() && !isset($entity->in_preview) && $this->entityInfo['render_cache']) {
$return['#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()),
),
);
}
return $return; return $return;
} }
...@@ -182,4 +228,20 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la ...@@ -182,4 +228,20 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
return $build; return $build;
} }
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) {
if (isset($ids)) {
$tags = array();
foreach ($ids as $entity_id) {
$tags[$this->entityType][$entity_id] = $entity_id;
}
\Drupal::cache($this->cacheBin)->deleteTags($tags);
}
else {
\Drupal::cache($this->cacheBin)->deleteTags(array($this->entityType . '_view' => TRUE));
}
}
} }
...@@ -75,4 +75,14 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N ...@@ -75,4 +75,14 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
* be available for loading. * be available for loading.
*/ */
public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL); public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL);
/**
* Resets the entity render cache.
*
* @param array|null $ids
* (optional) If specified, the cache is reset for the given entity IDs
* only.
*/
public function resetCache(array $ids = NULL);
} }
...@@ -20,7 +20,7 @@ class IntegrationTest extends ViewUnitTestBase { ...@@ -20,7 +20,7 @@ class IntegrationTest extends ViewUnitTestBase {
* *
* @var array * @var array
*/ */
public static $modules = array('aggregator', 'aggregator_test_views', 'system', 'field'); public static $modules = array('aggregator', 'aggregator_test_views', 'system', 'entity', 'field');
/** /**
* Views used by this test. * Views used by this test.
......
id: custom_block.full id: custom_block.full
label: Full label: Full
status: '0' status: '0'
cache: '1'
targetEntityType: custom_block targetEntityType: custom_block
...@@ -62,4 +62,9 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la ...@@ -62,4 +62,9 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
return $build; return $build;
} }
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) { }
} }
id: node.print id: node.print
label: Print label: Print
status: '0' status: '0'
cache: '1'
targetEntityType: node targetEntityType: node
id: comment.full id: comment.full
label: Full comment label: Full comment
status: '0' status: '0'
cache: '1'
targetEntityType: comment targetEntityType: comment
...@@ -219,6 +219,7 @@ public function getReplyForm(Request $request, NodeInterface $node, $pid = NULL) ...@@ -219,6 +219,7 @@ public function getReplyForm(Request $request, NodeInterface $node, $pid = NULL)
elseif ($account->hasPermission('access content')) { elseif ($account->hasPermission('access content')) {
// Display the node. // Display the node.
$build['comment_node'] = $this->entityManager()->getRenderController('node')->view($node); $build['comment_node'] = $this->entityManager()->getRenderController('node')->view($node);
unset($build['comment_node']['#cache']);
} }
} }
else { else {
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
* uri_callback = "comment_uri", * uri_callback = "comment_uri",
* fieldable = TRUE, * fieldable = TRUE,
* translatable = TRUE, * translatable = TRUE,
* render_cache = FALSE,
* route_base_path = "admin/structure/types/manage/{bundle}/comment", * route_base_path = "admin/structure/types/manage/{bundle}/comment",
* bundle_prefix = "comment_node_", * bundle_prefix = "comment_node_",
* entity_keys = { * entity_keys = {
......
...@@ -7,19 +7,19 @@ ...@@ -7,19 +7,19 @@
namespace Drupal\editor\Tests; namespace Drupal\editor\Tests;
use Drupal\simpletest\DrupalUnitTestBase; use Drupal\system\Tests\Entity\EntityUnitTestBase;
/** /**
* Unit tests for editor.module's entity hooks to track file usage. * Unit tests for editor.module's entity hooks to track file usage.
*/ */
class EditorFileUsageTest extends DrupalUnitTestBase { class EditorFileUsageTest extends EntityUnitTestBase {
/** /**
* Modules to enable. * Modules to enable.
* *
* @var array * @var array
*/ */
public static $modules = array('system', 'editor', 'editor_test', 'filter', 'node', 'entity', 'field', 'text', 'field_sql_storage', 'file'); public static $modules = array('editor', 'editor_test', 'node', 'file');
public static function getInfo() { public static function getInfo() {
return array( return array(
...@@ -31,13 +31,8 @@ public static function getInfo() { ...@@ -31,13 +31,8 @@ public static function getInfo() {
function setUp() { function setUp() {
parent::setUp(); parent::setUp();
$this->installSchema('system', 'url_alias'); $this->installSchema('node', array('node', 'node_access', 'node_field_data', 'node_field_revision'));
$this->installSchema('node', 'node'); $this->installSchema('file', array('file_managed', 'file_usage'));
$this->installSchema('node', 'node_access');
$this->installSchema('node', 'node_field_data');
$this->installSchema('node', 'node_field_revision');
$this->installSchema('file', 'file_managed');
$this->installSchema('file', 'file_usage');
// Add text formats. // Add text formats.
$filtered_html_format = entity_create('filter_format', array( $filtered_html_format = entity_create('filter_format', array(
...@@ -71,13 +66,15 @@ function testEditorEntityHooks() { ...@@ -71,13 +66,15 @@ function testEditorEntityHooks() {
$this->assertIdentical(array(), file_usage()->listUsage($image), 'The image has zero usages.'); $this->assertIdentical(array(), file_usage()->listUsage($image), 'The image has zero usages.');
// Test editor_entity_insert(): increment. // Test editor_entity_insert(): increment.
$this->createUser();
$node = entity_create('node', array( $node = entity_create('node', array(
'type' => 'page', 'type' => 'page',
'title' => 'test', 'title' => 'test',
'body' => array( 'body' => array(
'value' => '<p>Hello, world!</p><img src="awesome-llama.jpg" data-editor-file-uuid="' . $image->uuid() . '" />', 'value' => '<p>Hello, world!</p><img src="awesome-llama.jpg" data-editor-file-uuid="' . $image->uuid() . '" />',
'format' => 'filtered_html', 'format' => 'filtered_html',
) ),
'uid' => 1,
)); ));
$node->save(); $node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), file_usage()->listUsage($image), 'The image has 1 usage.'); $this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), file_usage()->listUsage($image), 'The image has 1 usage.');
......
...@@ -118,6 +118,20 @@ public function id() { ...@@ -118,6 +118,20 @@ public function id() {
return $this->targetEntityType . '.' . $this->bundle . '.' . $this->mode; return $this->targetEntityType . '.' . $this->bundle . '.' . $this->mode;
} }
/**
* {@inheritdoc}
*/
public function save() {
$return = parent::save();
// Reset the render cache for the target entity type.
if (\Drupal::entityManager()->hasController($this->targetEntityType, 'render')) {
\Drupal::entityManager()->getRenderController($this->targetEntityType)->resetCache();
}
return $return;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -57,6 +57,13 @@ abstract class EntityDisplayModeBase extends ConfigEntityBase implements EntityD ...@@ -57,6 +57,13 @@ abstract class EntityDisplayModeBase extends ConfigEntityBase implements EntityD
*/ */
public $status = TRUE; public $status = TRUE;
/**
* Whether or not the rendered output of this view mode is cached by default.
*
* @var bool
*/
public $cache = TRUE;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -96,8 +96,21 @@ public function view(FieldInterface $items) { ...@@ -96,8 +96,21 @@ public function view(FieldInterface $items) {
'#object' => $entity, '#object' => $entity,
'#items' => $items->getValue(TRUE), '#items' => $items->getValue(TRUE),
'#formatter' => $this->getPluginId(), '#formatter' => $this->getPluginId(),
'#cache' => array('tags' => array())
); );
// Gather cache tags from reference fields.
foreach ($items as $item) {
if (isset($item->format)) {
$info['#cache']['tags']['filter_format'] = $item->format;
}
if (isset($item->entity)) {