Commit 31dbc6a1 authored by catch's avatar catch

Issue #2151459 by jessebeach, Wim Leers, plach, amateescu, Berdir: Enable node render caching.

parent 11bf64e9
......@@ -3942,6 +3942,16 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
return '';
}
// Collect all #post_render_cache callbacks associated with this element when:
// - about to store this element in the render cache, or when;
// - about to apply #post_render_cache callbacks.
if (isset($elements['#cache']) || !$is_recursive_call) {
$post_render_cache = drupal_render_collect_post_render_cache($elements);
if ($post_render_cache) {
$elements['#post_render_cache'] = $post_render_cache;
}
}
// Add any JavaScript state information associated with the element.
if (!empty($elements['#states'])) {
drupal_process_states($elements);
......@@ -4045,16 +4055,6 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
$suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
// Collect all #post_render_cache callbacks associated with this element when:
// - about to store this element in the render cache, or when;
// - about to apply #post_render_cache callbacks.
if (isset($elements['#cache']) || !$is_recursive_call) {
$post_render_cache = drupal_render_collect_post_render_cache($elements);
if ($post_render_cache) {
$elements['#post_render_cache'] = $post_render_cache;
}
}
// Cache the processed element if #cache is set.
if (isset($elements['#cache'])) {
drupal_render_cache_set($elements['#markup'], $elements);
......@@ -4127,7 +4127,7 @@ function render(&$element) {
}
if (is_array($element)) {
show($element);
return drupal_render($element);
return drupal_render($element, TRUE);
}
else {
// Safe-guard for inappropriate use of render() on flat variables: return
......@@ -4403,8 +4403,19 @@ function _drupal_render_process_post_render_cache(array &$elements) {
* elements. This allows drupal_render() to execute all of them when the element
* is retrieved from the render cache.
*
* @param array $elements
* The element to collect #post_render_cache from.
* Note: the theme system may render child elements directly (e.g. rendering a
* node causes its template to be rendered, which causes the node links to be
* drupal_render()ed). On top of that, the theme system transforms render arrays
* into HTML strings. These two facts combined imply that it is impossible for
* #post_render_cache callbacks to bubble up to the root of the render array.
* Therefore, drupal_render_collect_post_render_cache() must be called *before*
* #theme callbacks, so that it has a chance to examine the full render array.
* In short: in order to examine the full render array for #post_render_cache
* callbacks, it must use post-order tree traversal, whereas drupal_render()
* itself uses pre-order tree traversal.
*
* @param array &$elements
* The element to collect #post_render_cache callbacks for.
* @param array $callbacks
* Internal use only. The #post_render_callbacks array so far.
* @param bool $is_root_element
......@@ -4416,18 +4427,37 @@ function _drupal_render_process_post_render_cache(array &$elements) {
* @see drupal_render()
* @see _drupal_render_process_post_render_cache()
*/
function drupal_render_collect_post_render_cache(array $elements, array $callbacks = array(), $is_root_element = TRUE) {
// Collect all #post_render_cache for this element.
function drupal_render_collect_post_render_cache(array &$elements, array $callbacks = array(), $is_root_element = TRUE) {
// Try to fetch the prerendered element from cache, to determine
// #post_render_cache callbacks for this element and all its children. If we
// don't do this, then the #post_render_cache tokens will be re-generated, but
// they would no longer match the tokens in the render cached markup, causing
// the render cache placeholder markup to be sent to the end user!
$retrieved_from_cache = FALSE;
if (!$is_root_element && isset($elements['#cache'])) {
$cached_element = drupal_render_cache_get($elements);
if ($cached_element !== FALSE && isset($cached_element['#post_render_cache'])) {
$elements['#post_render_cache'] = $cached_element['#post_render_cache'];
$retrieved_from_cache = TRUE;
}
}
// If this is a render cache placeholder that hasn't been rendered yet, then
// render it now, because we must be able to collect its #post_render_cache
// callback.
if (!isset($elements['#post_render_cache']) && isset($elements['#type']) && $elements['#type'] === 'render_cache_placeholder') {
$elements = drupal_pre_render_render_cache_placeholder($elements);
}
// Collect all #post_render_cache callbacks for this element.
if (isset($elements['#post_render_cache'])) {
$callbacks = NestedArray::mergeDeep($callbacks, $elements['#post_render_cache']);
}
// Child elements that have #cache set will already have collected all their
// children's #post_render_cache callbacks, so no need to traverse further.
if (!$is_root_element && isset($elements['#cache'])) {
return $callbacks;
}
else if ($children = element_children($elements)) {
// Collect the #post_render_cache callbacks for all child elements, unless
// we've already collected them above by retrieving this element (and its
// children) from the render cache.
if (!$retrieved_from_cache && $children = element_children($elements)) {
foreach ($children as $child) {
$callbacks = drupal_render_collect_post_render_cache($elements[$child], $callbacks, FALSE);
}
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\entity\Entity\EntityViewDisplay;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -147,7 +148,7 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
// Cache the rendered output if permitted by the view mode and global entity
// type configuration.
if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && !isset($entity->in_preview) && $this->entityType->isRenderCacheable()) {
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,
......@@ -157,6 +158,10 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
$this->entityTypeId => array($entity->id()),
),
);
if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
$return['#cache']['keys'][] = $langcode;
}
}
return $return;
......
This diff is collapsed.
......@@ -37,7 +37,6 @@
* uri_callback = "comment_uri",
* fieldable = TRUE,
* translatable = TRUE,
* render_cache = FALSE,
* entity_keys = {
* "id" = "cid",
* "bundle" = "field_id",
......@@ -188,6 +187,15 @@ public static function postDelete(EntityStorageControllerInterface $storage_cont
}
}
/**
* {@inheritdoc}
*/
public function referencedEntities() {
$referenced_entities = parent::referencedEntities();
$referenced_entities[] = $this->getCommentedEntity();
return $referenced_entities;
}
/**
* {@inheritdoc}
*/
......
......@@ -642,7 +642,7 @@ function theme_field($variables) {
// Render the items.
$output .= '<div class="field-items"' . $variables['content_attributes'] . '>';
foreach ($variables['items'] as $delta => $item) {
$output .= '<div class="field-item"' . $variables['item_attributes'][$delta] . '>' . drupal_render($item) . '</div>';
$output .= '<div class="field-item"' . $variables['item_attributes'][$delta] . '>' . drupal_render($item, TRUE) . '</div>';
}
$output .= '</div>';
......
......@@ -40,7 +40,6 @@
* uri_callback = "node_uri",
* fieldable = TRUE,
* translatable = TRUE,
* render_cache = FALSE,
* entity_keys = {
* "id" = "nid",
* "revision" = "vid",
......
......@@ -195,6 +195,9 @@ public function postSave(EntityStorageControllerInterface $storage_controller, $
else {
// Invalidate the cache tag of the updated node type only.
Cache::invalidateTags(array('node_type' => $this->id()));
// Invalidate the render cache for all nodes.
\Drupal::entityManager()->getViewBuilder('node')->resetCache();
}
}
......
......@@ -58,6 +58,20 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang
}
}
/**
* {@inheritdoc}
*/
protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langcode) {
$defaults = parent::getBuildDefaults($entity, $view_mode, $langcode);
// Don't cache nodes that are in 'preview' mode.
if (isset($defaults['#cache']) && isset($entity->in_preview)) {
unset($defaults['#cache']);
}
return $defaults;
}
/**
* #post_render_cache callback; replaces the placeholder with node links.
*
......
......@@ -49,7 +49,8 @@ function setUp() {
*/
public function testNodeDelete() {
$author = $this->drupalCreateUser();
$node_path = 'node/' . $this->drupalCreateNode(array('uid' => $author->id()))->id();
$node_id = $this->drupalCreateNode(array('uid' => $author->id()))->id();
$node_path = 'node/' . $node_id;
// Populate page cache.
$this->drupalGet($node_path);
......@@ -58,7 +59,7 @@ public function testNodeDelete() {
$cid_parts = array(url($node_path, array('absolute' => TRUE)), 'html');
$cid = sha1(implode(':', $cid_parts));
$cache_entry = \Drupal::cache('page')->get($cid);
$this->assertIdentical($cache_entry->tags, array('content:1', 'user:' . $author->id(), 'filter_format:plain_text'));
$this->assertIdentical($cache_entry->tags, array('content:1', 'node_view:' . $node_id, 'node:' . $node_id, 'user:' . $author->id(), 'filter_format:plain_text'));
// Login and delete the node.
$this->drupalLogin($this->adminUser);
......
......@@ -134,6 +134,7 @@ public function testRowPlugin() {
// Test with links disabled.
$view->rowPlugin->options['links'] = FALSE;
\Drupal::entityManager()->getViewBuilder('node')->resetCache();
$output = $view->preview();
$output = drupal_render($output);
$this->drupalSetContent($output);
......@@ -143,6 +144,7 @@ public function testRowPlugin() {
// Test with links enabled.
$view->rowPlugin->options['links'] = TRUE;
\Drupal::entityManager()->getViewBuilder('node')->resetCache();
$output = $view->preview();
$output = drupal_render($output);
$this->drupalSetContent($output);
......
......@@ -46,27 +46,29 @@ function setUp() {
function testPageCacheTags() {
// Create two nodes.
$author_1 = $this->drupalCreateUser();
$node_1_path = 'node/' . $this->drupalCreateNode(array(
$node_1 = $this->drupalCreateNode(array(
'uid' => $author_1->id(),
'title' => 'Node 1',
'body' => array(
0 => array('value' => 'Body 1', 'format' => 'basic_html'),
),
'promote' => NODE_PROMOTED,
))->id();
));
$author_2 = $this->drupalCreateUser();
$node_2_path = 'node/' . $this->drupalCreateNode(array(
$node_2 = $this->drupalCreateNode(array(
'uid' => $author_2->id(),
'title' => 'Node 2',
'body' => array(
0 => array('value' => 'Body 2', 'format' => 'full_html'),
),
'promote' => NODE_PROMOTED,
))->id();
));
// Full node page 1.
$this->verifyPageCacheTags($node_1_path, array(
$this->verifyPageCacheTags('node/' . $node_1->id(), array(
'content:1',
'node_view:1',
'node:' . $node_1->id(),
'user:' . $author_1->id(),
'filter_format:basic_html',
'menu:footer',
......@@ -74,8 +76,10 @@ function testPageCacheTags() {
));
// Full node page 2.
$this->verifyPageCacheTags($node_2_path, array(
$this->verifyPageCacheTags('node/' . $node_2->id(), array(
'content:1',
'node_view:1',
'node:' . $node_2->id(),
'user:' . $author_2->id(),
'filter_format:full_html',
'menu:footer',
......
......@@ -808,18 +808,26 @@ function testDrupalRenderRenderCachePlaceholder() {
$this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
// GET request: validate cached data.
$tokens = array_keys($element['#post_render_cache']['common_test_post_render_cache_placeholder']);
$expected_token = $tokens[0];
$element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET'));
$cached_element = \Drupal::cache()->get(drupal_render_cid_create($element))->data;
// Parse unique token out of the markup.
// Parse unique token out of the cached markup.
$dom = Html::load($cached_element['#markup']);
$xpath = new \DOMXPath($dom);
$nodes = $xpath->query('//*[@token]');
$token = $nodes->item(0)->getAttribute('token');
$this->assertTrue($nodes->length, 'The token attribute was found in the cached markup');
$token = '';
if ($nodes->length) {
$token = $nodes->item(0)->getAttribute('token');
}
$this->assertIdentical($token, $expected_token, 'The tokens are identical');
// Verify the token is in the cached element.
$expected_element = array(
'#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" context="bar:' . $context['bar'] .';" token="'. $token . '" /></foo>',
'#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" context="bar:' . $context['bar'] .';" token="'. $expected_token . '" /></foo>',
'#post_render_cache' => array(
'common_test_post_render_cache_placeholder' => array(
$token => $context,
$expected_token => $context,
),
),
);
......@@ -840,6 +848,152 @@ function testDrupalRenderRenderCachePlaceholder() {
\Drupal::request()->setMethod($request_method);
}
/**
* Tests post-render cache-integrated 'render_cache_placeholder' child
* element.
*/
function testDrupalRenderChildElementRenderCachePlaceholder() {
$context = array('bar' => $this->randomString());
$container = array(
'#type' => 'container',
);
$test_element = array(
'#type' => 'render_cache_placeholder',
'#context' => $context,
'#callback' => 'common_test_post_render_cache_placeholder',
'#prefix' => '<foo>',
'#suffix' => '</foo>'
);
$container['test_element'] = $test_element;
$expected_output = '<div><foo><bar>' . $context['bar'] . '</bar></foo></div>';
// #cache disabled.
drupal_static_reset('_drupal_add_js');
$element = $container;
$output = drupal_render($element);
$this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
$settings = $this->parseDrupalSettings(drupal_get_js());
$this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
// The cache system is turned off for POST requests.
$request_method = \Drupal::request()->getMethod();
\Drupal::request()->setMethod('GET');
// GET request: #cache enabled, cache miss.
drupal_static_reset('_drupal_add_js');
$element = $container;
$element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
$element['test_element']['#cache'] = array('cid' => 'render_cache_placeholder_test_child_GET');
// Simulate element rendering in a template, where sub-items of a renderable
// can be sent to drupal_render() before the parent.
$child = &$element['test_element'];
$element['#children'] = drupal_render($child, TRUE);
// Eventually, drupal_render() gets called on the root element.
$output = drupal_render($element);
$this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
$this->assertTrue(isset($element['#printed']), 'No cache hit');
$this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
$settings = $this->parseDrupalSettings(drupal_get_js());
$this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
// GET request: validate cached data for child element.
$child_tokens = array_keys($element['test_element']['#post_render_cache']['common_test_post_render_cache_placeholder']);
$parent_tokens = array_keys($element['#post_render_cache']['common_test_post_render_cache_placeholder']);
$expected_token = $child_tokens[0];
$element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
$cached_element = \Drupal::cache()->get(drupal_render_cid_create($element))->data;
// Parse unique token out of the cached markup.
$dom = Html::load($cached_element['#markup']);
$xpath = new \DOMXPath($dom);
$nodes = $xpath->query('//*[@token]');
$this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup');
$token = '';
if ($nodes->length) {
$token = $nodes->item(0)->getAttribute('token');
}
$this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
// Verify the token is in the cached element.
$expected_element = array(
'#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" context="bar:' . $context['bar'] .';" token="'. $expected_token . '" /></foo>',
'#post_render_cache' => array(
'common_test_post_render_cache_placeholder' => array(
$expected_token => $context,
),
),
);
$this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
// GET request: validate cached data (for the parent/entire render array).
$element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET'));
$cached_element = \Drupal::cache()->get(drupal_render_cid_create($element))->data;
// Parse unique token out of the cached markup.
$dom = Html::load($cached_element['#markup']);
$xpath = new \DOMXPath($dom);
$nodes = $xpath->query('//*[@token]');
$this->assertTrue($nodes->length, 'The token attribute was found in the cached parent element markup');
$token = '';
if ($nodes->length) {
$token = $nodes->item(0)->getAttribute('token');
}
$this->assertIdentical($token, $expected_token, 'The tokens are identical for the parent element');
// Verify the token is in the cached element.
$expected_element = array(
'#markup' => '<div><foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" context="bar:' . $context['bar'] .';" token="'. $expected_token . '" /></foo></div>',
'#post_render_cache' => array(
'common_test_post_render_cache_placeholder' => array(
$expected_token => $context,
),
),
);
$this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
// GET request: validate cached data.
// Check the cache of the child element again after the parent has been
// rendered.
$element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET'));
$cached_element = \Drupal::cache()->get(drupal_render_cid_create($element))->data;
// Verify that the child element contains the correct
// render_cache_placeholder markup.
$expected_token = $child_tokens[0];
$dom = Html::load($cached_element['#markup']);
$xpath = new \DOMXPath($dom);
$nodes = $xpath->query('//*[@token]');
$this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup');
$token = '';
if ($nodes->length) {
$token = $nodes->item(0)->getAttribute('token');
}
$this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
// Verify the token is in the cached element.
$expected_element = array(
'#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" context="bar:' . $context['bar'] .';" token="'. $expected_token . '" /></foo>',
'#post_render_cache' => array(
'common_test_post_render_cache_placeholder' => array(
$expected_token => $context,
),
),
);
$this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.');
// GET request: #cache enabled, cache hit.
drupal_static_reset('_drupal_add_js');
$element = $container;
$element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET');
// Simulate element rendering in a template, where sub-items of a renderable
// can be sent to drupal_render before the parent.
$child = &$element['test_element'];
$element['#children'] = drupal_render($child, TRUE);
$output = drupal_render($element);
$this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output');
$this->assertFalse(isset($element['#printed']), 'Cache hit');
$this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.');
$settings = $this->parseDrupalSettings(drupal_get_js());
$this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.');
// Restore the previous request method.
\Drupal::request()->setMethod($request_method);
}
protected function parseDrupalSettings($html) {
$startToken = 'drupalSettings = ';
$endToken = '}';
......
......@@ -65,6 +65,7 @@ function testTimeZoneHandling() {
// Set time zone to Los Angeles time.
$config->set('timezone.default', 'America/Los_Angeles')->save();
\Drupal::entityManager()->getViewBuilder('node')->resetCache(array($node1, $node2));
// Confirm date format and time zone.
$this->drupalGet('node/' . $node1->id());
......
......@@ -620,7 +620,6 @@ function system_element_info() {
$types['render_cache_placeholder'] = array(
'#callback' => '',
'#context' => array(),
'#pre_render' => array('drupal_pre_render_render_cache_placeholder'),
);
return $types;
......
......@@ -118,6 +118,7 @@ function testPictureOnNodeComment() {
->set('features.node_user_picture', FALSE)
->set('features.comment_user_picture', FALSE)
->save();
\Drupal::entityManager()->getViewBuilder('comment')->resetCache();
$this->drupalGet('node/' . $node->id());
$this->assertNoRaw(file_uri_target($file->getFileUri()), 'User picture not found on node and comment.');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment