From 7b31aa24c55ef06cc680961e659b4c6545eea5a5 Mon Sep 17 00:00:00 2001 From: chaozheng ye <yechaozheng@2950237.no-reply.drupal.org> Date: Sun, 14 Mar 2021 19:58:09 +1000 Subject: [PATCH 1/2] [#3203460] by yechaozheng: Apply custom cache tag for views row cache. --- src/Plugin/views/cache/AdvancedViewsCache.php | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Plugin/views/cache/AdvancedViewsCache.php b/src/Plugin/views/cache/AdvancedViewsCache.php index 21d2fbe..9c3f696 100644 --- a/src/Plugin/views/cache/AdvancedViewsCache.php +++ b/src/Plugin/views/cache/AdvancedViewsCache.php @@ -12,6 +12,7 @@ use Drupal\Core\Link; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\views\Views; use Drupal\views\Plugin\views\cache\CachePluginBase; +use Drupal\views\ResultRow; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -116,6 +117,7 @@ class AdvancedViewsCache extends CachePluginBase { // Cache Tags. $options['cache_tags'] = ['default' => []]; $options['cache_tags_exclude'] = ['default' => []]; + $options['cache_tags_for_row'] = ['default' => FALSE]; // Cache Contexts. $options['cache_contexts'] = ['default' => []]; $options['cache_contexts_exclude'] = ['default' => []]; @@ -156,7 +158,10 @@ class AdvancedViewsCache extends CachePluginBase { // Filter out some of the default cache tags we don't care about. // This should mostly be the entity_type list cache tags ex. node_list. $default_cache_tags = array_diff(parent::getCacheTags(), ['extensions', 'config:views.view.' . $this->view->id()]); - $cache_tags = !empty($this->options['cache_tags']) ? $this->options['cache_tags'] : $default_cache_tags; + $cache_tags = !empty($this->options['cache_tags']) + ? $this->options['cache_tags'] : $default_cache_tags; + $cache_tags_for_row = !empty($this->options['cache_tags_for_row']) + ? $this->options['cache_tags_for_row'] : FALSE; $form['cache_tags'] = [ '#type' => 'details', @@ -203,6 +208,12 @@ class AdvancedViewsCache extends CachePluginBase { $output['list'] = $item_list; $form['cache_tags']['tokens'] = $output; } + + $form['cache_tags']['cache_tags_for_row'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Apply the cache tags for views row.'), + '#default_value' => $cache_tags_for_row, + ]; } /** @@ -323,6 +334,9 @@ class AdvancedViewsCache extends CachePluginBase { $form_state->setValue(['cache_options', 'cache_tags', 'cache_tags'], $cache_tags); $form_state->setValue(['cache_options', 'cache_tags', 'cache_tags_exclude'], $cache_tags_exclude); + $cache_tags_for_row = $form_state->getValue(['cache_options', 'cache_tags', 'cache_tags_for_row']); + $form_state->setValue(['cache_options', 'cache_tags', 'cache_tags_for_row'], $cache_tags_for_row); + $cache_contexts = $form_state->getValue(['cache_options', 'cache_contexts', 'cache_contexts']); $cache_contexts = preg_split('/\r\n|[\r\n]+/', $cache_contexts) ?: []; $cache_contexts = array_filter(array_map('trim', $cache_contexts)); @@ -390,6 +404,34 @@ class AdvancedViewsCache extends CachePluginBase { return $cache_tags; } + /** + * Returns the row cache tags. + * + * @param \Drupal\views\ResultRow $row + * A result row. + * + * @return string[] + * The row cache tags. + */ + public function getRowCacheTags(ResultRow $row) { + $row_cache_tags = parent::getRowCacheTags($row); + + if (!$this->options['cache_tags_for_row']) { + return $row_cache_tags; + } + + $cache_tags = $this->options['cache_tags'] ?: []; + $tags = Cache::mergeTags($row_cache_tags, $cache_tags); + + // Remove cache tags marked for exclusion. + if (!empty($this->options['cache_tags_exclude'])) { + $cache_tags_exclude = $this->options['cache_tags_exclude']; + $cache_tags = array_diff($cache_tags, $cache_tags_exclude); + } + + return $tags; + } + /** * {@inheritdoc} */ -- GitLab From 97075f26fd99f901b6ff1cccbf3a5524f2743788 Mon Sep 17 00:00:00 2001 From: Ted Cooper <elc@784944.no-reply.drupal.org> Date: Fri, 25 Apr 2025 00:33:31 +1000 Subject: [PATCH 2/2] Add test for views row caching. --- tests/src/Kernel/ViewsRowCacheTest.php | 287 +++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 tests/src/Kernel/ViewsRowCacheTest.php diff --git a/tests/src/Kernel/ViewsRowCacheTest.php b/tests/src/Kernel/ViewsRowCacheTest.php new file mode 100644 index 0000000..bb9cc66 --- /dev/null +++ b/tests/src/Kernel/ViewsRowCacheTest.php @@ -0,0 +1,287 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\views_advanced_cache\Kernel; + +use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Html; +use Drupal\Core\Session\AccountInterface; +use Drupal\Tests\user\Traits\UserCreationTrait; +use Drupal\Tests\views\Kernel\ViewsKernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; +use Drupal\node\NodeInterface; +use Drupal\views\Entity\View; +use Drupal\views\Views; + +/** + * Tests row render caching. + * + * @see Drupal\Tests\views\Kernel\Plugin\RowRenderCacheTest. + * + * @group views + */ +class ViewsRowCacheTest extends ViewsKernelTestBase { + + use UserCreationTrait; + + /** + * Turn off strict config schema check. + * + * @var bool + * + * @todo Add config schema to this module and set this back to TRUE. + */ + protected $strictConfigSchema = FALSE; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'user', + 'node', + 'views_advanced_cache', + ]; + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = [ + 'test_row_render_cache', + 'test_row_render_cache_none', + ]; + + /** + * An editor user account. + * + * @var \Drupal\user\UserInterface + */ + protected $editorUser; + + /** + * A power user account. + * + * @var \Drupal\user\UserInterface + */ + protected $powerUser; + + /** + * A regular user account. + * + * @var \Drupal\user\UserInterface + */ + protected $regularUser; + + /** + * {@inheritdoc} + */ + protected function setUpFixtures() { + parent::setUpFixtures(); + + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installSchema('node', 'node_access'); + + NodeType::create([ + 'type' => 'test', + 'name' => 'Test', + ])->save(); + + $this->editorUser = $this->createUser(['bypass node access']); + $this->powerUser = $this->createUser([ + 'access content', + 'create test content', + 'edit own test content', + 'delete own test content', + ]); + $this->regularUser = $this->createUser(['access content']); + + // Create some test entities. + for ($i = 0; $i < 5; $i++) { + Node::create([ + 'title' => 'b' . $i . $this->randomMachineName(), + 'type' => 'test', + ])->save(); + } + + // Create a power user node. + Node::create([ + 'title' => 'z' . $this->randomMachineName(), + 'uid' => $this->powerUser->id(), + 'type' => 'test', + ])->save(); + } + + /** + * Tests complex field rewriting and uncacheable field handlers. + */ + public function testAdvancedCaching(): void { + // Test the users using normal caching. + $this->doTestRenderedOutput($this->editorUser); + $this->doTestRenderedOutput($this->editorUser, TRUE, FALSE); + $this->doTestRenderedOutput($this->powerUser); + $this->doTestRenderedOutput($this->powerUser, TRUE, FALSE); + $this->doTestRenderedOutput($this->regularUser); + $this->doTestRenderedOutput($this->regularUser, TRUE, FALSE); + + // Setup caching on test view to use VAC. + /** @var Drupal\views\Entity\Vie $view */ + $view = View::load('test_row_render_cache'); + $display = &$view->getDisplay('default'); + $cache_options = $display['display_options']['cache']['options'] ?? []; + $cache_options['cache_tags'] = ['node_test']; + $cache_options['cache_tags_exclude'] = []; + $cache_options['cache_contexts'] = []; + $cache_options['cache_contexts_exclude'] = []; + $cache_options['cache_tags_for_row'] = TRUE; + $display['display_options']['cache']['type'] = 'advanced_views_cache'; + $display['display_options']['cache']['options'] = $cache_options; + $view->save(); + + // Test that row field output is cached with the extra tags. + $this->doTestRenderedOutput($this->editorUser); + $this->doTestRenderedOutput($this->editorUser, TRUE, TRUE); + $this->doTestRenderedOutput($this->powerUser); + $this->doTestRenderedOutput($this->powerUser, TRUE, TRUE); + $this->doTestRenderedOutput($this->regularUser); + $this->doTestRenderedOutput($this->regularUser, TRUE, TRUE); + + // Alter the result set order and check that counter is still working + // correctly. + $this->doTestRenderedOutput($this->editorUser); + /** @var \Drupal\node\NodeInterface $node */ + $node = Node::load(6); + $node->setTitle('a' . $this->randomMachineName()); + $node->save(); + $this->doTestRenderedOutput($this->editorUser); + } + + /** + * Tests that rows are not cached when the none cache plugin is used. + */ + public function testNoCaching(): void { + + $this->setCurrentUser($this->regularUser); + $view = Views::getView('test_row_render_cache_none'); + $view->setDisplay(); + $view->preview(); + + /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */ + $render_cache = $this->container->get('render_cache'); + + /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ + $cache_plugin = $view->display_handler->getPlugin('cache'); + + foreach ($view->result as $row) { + $keys = $cache_plugin->getRowCacheKeys($row); + $cache = [ + '#cache' => [ + 'keys' => $keys, + 'contexts' => ['languages:language_interface', 'theme', 'user.permissions'], + ], + ]; + $element = $render_cache->get($cache); + $this->assertFalse($element); + } + } + + /** + * Check whether the rendered output matches expectations. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The user account to tests rendering with. + * @param bool $check_cache + * (optional) Whether explicitly test render cache entries. + * @param bool $is_vac + * (optional) Whether caching must be Views Advanced Cache (VAC). + */ + protected function doTestRenderedOutput( + AccountInterface $account, + ?bool $check_cache = FALSE, + ?bool $is_vac = FALSE, + ) { + $this->setCurrentUser($account); + $view = Views::getView('test_row_render_cache'); + $view->setDisplay(); + $view->preview(); + + /** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */ + $render_cache = $this->container->get('render_cache'); + + /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ + $cache_plugin = $view->display_handler->getPlugin('cache'); + if ($is_vac) { + $this->assertSame($cache_plugin->getPluginId(), 'advanced_views_cache'); + } + + // Retrieve nodes and sort them in alphabetical order to match view results. + $nodes = Node::loadMultiple(); + usort($nodes, function (NodeInterface $a, NodeInterface $b) { + return strcmp($a->label(), $b->label()); + }); + + $index = 0; + foreach ($nodes as $node) { + $nid = $node->id(); + $access = $node->access('update'); + + $counter = $index + 1; + $expected = "$nid: $counter (just in case: $nid)"; + $counter_output = $view->style_plugin->getField($index, 'counter'); + $this->assertSame($expected, (string) $counter_output); + + $node_url = $node->toUrl()->toString(); + $expected = "<a href=\"$node_url\"><span class=\"da-title\">{$node->label()}</span> <span class=\"counter\">$counter_output</span></a>"; + $output = $view->style_plugin->getField($index, 'title'); + $this->assertSame($expected, (string) $output); + + $expected = $access ? "<a href=\"$node_url/edit?destination=/\" hreflang=\"en\">edit</a>" : ""; + $output = $view->style_plugin->getField($index, 'edit_node'); + $this->assertSame($expected, (string) $output); + + $expected = $access ? "<a href=\"$node_url/delete?destination=/\" hreflang=\"en\">delete</a>" : ""; + $output = $view->style_plugin->getField($index, 'delete_node'); + $this->assertSame($expected, (string) $output); + $expected = $access ? ' <div class="dropbutton-wrapper" data-drupal-ajax-container><div class="dropbutton-widget"><ul class="dropbutton">' . + '<li><a href="' . $node_url . '/edit?destination=/" aria-label="Edit ' . $node->label() . '" hreflang="en">Edit</a></li>' . + '<li><a href="' . $node_url . '/delete?destination=/" aria-label="Delete ' . $node->label() . '" class="use-ajax" data-dialog-type="modal" data-dialog-options="' . Html::escape(Json::encode(['width' => 880])) . '" hreflang="en">Delete</a></li>' . + '</ul></div></div>' : ''; + $output = $view->style_plugin->getField($index, 'operations'); + $this->assertSame($expected, (string) $output); + + if ($check_cache) { + $keys = $cache_plugin->getRowCacheKeys($view->result[$index]); + $cache = [ + '#cache' => [ + 'keys' => $keys, + 'contexts' => ['languages:language_interface', 'theme', 'user.permissions'], + ], + ]; + $element = $render_cache->get($cache); + $this->assertNotEmpty($element); + + // If VAC is caching, expect there to be extra tags. + $tags = $cache_plugin->getRowCacheTags($view->result[$index]); + $this->assertNotEmpty($tags, 'No row tags present when expected'); + if ($is_vac) { + $this->assertTrue( + in_array('node_test', $tags), + 'Tag node_test was missing from row cache tags.' + ); + } + else { + $this->assertFalse( + in_array('node_test', $tags), + 'Tag node_test was present when it should not have been.' + ); + } + } + + $index++; + } + } + +} -- GitLab