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