From 1966f9e7ef0d7a5ce77a09832c94c245c7432763 Mon Sep 17 00:00:00 2001 From: Senem Hartung Date: Thu, 26 Feb 2026 11:01:03 -0600 Subject: [PATCH 1/3] fix(next): update AJAX method and enhance CacheTag revalidator - Fix AJAX method from 'replace' to 'replaceWith' in NextEntityTypeConfigForm - Add configuration options for CacheTag revalidator (entity_tag, entity_list_tag, additional_tags) - Add user-friendly configuration form with contextual help text --- .../src/Form/NextEntityTypeConfigForm.php | 8 +- .../src/Plugin/Next/Revalidator/CacheTag.php | 119 ++++++++++++++++-- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/modules/next/src/Form/NextEntityTypeConfigForm.php b/modules/next/src/Form/NextEntityTypeConfigForm.php index bf35b141..ac3d44b0 100644 --- a/modules/next/src/Form/NextEntityTypeConfigForm.php +++ b/modules/next/src/Form/NextEntityTypeConfigForm.php @@ -103,7 +103,7 @@ public function form(array $form, FormStateInterface $form_state) { '#ajax' => [ 'callback' => '::ajaxReplaceSettingsForm', 'wrapper' => 'settings-container', - 'method' => 'replace', + 'method' => 'replaceWith', ], ]; @@ -120,7 +120,7 @@ public function form(array $form, FormStateInterface $form_state) { '#ajax' => [ 'callback' => '::ajaxReplaceSiteResolverSettingsForm', 'wrapper' => 'site-resolver-settings', - 'method' => 'replace', + 'method' => 'replaceWith', ], ]; @@ -167,7 +167,7 @@ public function form(array $form, FormStateInterface $form_state) { '#ajax' => [ 'callback' => '::ajaxReplaceSettingsForm', 'wrapper' => 'settings-container', - 'method' => 'replace', + 'method' => 'replaceWith', ], ]; @@ -191,7 +191,7 @@ public function form(array $form, FormStateInterface $form_state) { '#ajax' => [ 'callback' => '::ajaxReplaceRevalidatorSettingsForm', 'wrapper' => 'revalidator-settings', - 'method' => 'replace', + 'method' => 'replaceWith', ], ]; diff --git a/modules/next/src/Plugin/Next/Revalidator/CacheTag.php b/modules/next/src/Plugin/Next/Revalidator/CacheTag.php index ea9a8987..9f0429d1 100644 --- a/modules/next/src/Plugin/Next/Revalidator/CacheTag.php +++ b/modules/next/src/Plugin/Next/Revalidator/CacheTag.php @@ -25,13 +25,85 @@ class CacheTag extends ConfigurableRevalidatorBase implements RevalidatorInterfa * {@inheritdoc} */ public function defaultConfiguration() { - return []; + return [ + 'entity_tag' => TRUE, + 'entity_list_tag' => TRUE, + 'additional_tags' => NULL, + ]; } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + // Get entity type and bundle from form callback object. + $entity_type_id = NULL; + $bundle = NULL; + + try { + $entity_bundle_string = $form_state->getBuildInfo()['callback_object']->getEntity()->id(); + // Split "node.quote" into entity type and bundle. + if (strpos($entity_bundle_string, '.') !== FALSE) { + [$entity_type_id, $bundle] = explode('.', $entity_bundle_string, 2); + } + } + catch (\Exception) { + // Fallback if we can't get the entity info. + $entity_type_id = NULL; + $bundle = NULL; + } + + $form['entity_tag'] = [ + '#title' => $this->t('Revalidate entity cache tag'), + '#description' => $this->t('Revalidate pages with the individual entity cache tag (e.g., @entity_type:123).', [ + '@entity_type' => $entity_type_id ?: 'node', + ]), + '#type' => 'checkbox', + '#default_value' => $this->configuration['entity_tag'] ?? TRUE, + ]; + + // Generate specific label and description based on detected entity type. + if ($entity_type_id && $bundle) { + if ($entity_type_id === 'node') { + $list_tag_example = 'node_list:' . $bundle; + $next_js_example = 'tags: ["node_list:' . $bundle . '"]'; + } + elseif ($entity_type_id === 'taxonomy_term') { + $list_tag_example = 'taxonomy_list:' . $bundle; + $next_js_example = 'tags: ["taxonomy_list:' . $bundle . '"]'; + } + else { + $list_tag_example = $entity_type_id . '_list:' . $bundle; + $next_js_example = 'tags: ["' . $entity_type_id . '_list:' . $bundle . '"]'; + } + + $title = $this->t('Revalidate @tag cache tags', ['@tag' => $list_tag_example]); + $description = $this->t('Revalidates pages tagged with @tag when @entity_type entities of type @bundle change.

In Next.js use: @example', [ + '@tag' => $list_tag_example, + '@entity_type' => $entity_type_id, + '@bundle' => $bundle, + '@example' => $next_js_example, + ]); + } + else { + $title = $this->t('Revalidate [entity_type]_list:[bundle] cache tags'); + $description = $this->t('Revalidates pages tagged with entity type and bundle list cache tags when entities change.
Node entities: generates node_list:[bundle] (e.g., node_list:article, node_list:person)
Taxonomy terms: generates taxonomy_list:[vocabulary] (e.g., taxonomy_list:tags)
Other entities: generates [entity_type]_list:[bundle]

In Next.js use: tags: ["node_list:article"] or tags: ["taxonomy_list:tags"]'); + } + + $form['entity_list_tag'] = [ + '#title' => $title, + '#description' => $description, + '#type' => 'checkbox', + '#default_value' => $this->configuration['entity_list_tag'] ?? TRUE, + ]; + + $form['additional_tags'] = [ + '#type' => 'textarea', + '#title' => $this->t('Additional cache tags to revalidate'), + '#default_value' => $this->configuration['additional_tags'] ?? '', + '#description' => $this->t('Additional cache tags to revalidate when this entity type changes. Enter one tag per line. Examples:
node_list:all
search_results
homepage'), + ]; + return $form; } @@ -39,7 +111,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - // No configuration form. + $this->configuration['entity_tag'] = $form_state->getValue('entity_tag'); + $this->configuration['entity_list_tag'] = $form_state->getValue('entity_list_tag'); + $this->configuration['additional_tags'] = $form_state->getValue('additional_tags'); } /** @@ -57,18 +131,41 @@ public function revalidate(EntityActionEvent $event): bool { return FALSE; } - // Get all available cache tags (including list tags). - $list_tags = $entity->getEntityType()->getListCacheTags(); - if ($entity->getEntityType()->hasKey('bundle')) { - $list_tags[] = $entity->getEntityTypeId() . '_list:' . $entity->bundle(); + $cache_tags = []; + + // Add individual entity cache tags if enabled. + if (!empty($this->configuration['entity_tag'])) { + $cache_tags = array_merge($cache_tags, $entity->getCacheTags()); + } + + // Add entity list cache tags if enabled. + if (!empty($this->configuration['entity_list_tag'])) { + $list_tags = $entity->getEntityType()->getListCacheTags(); + if ($entity->getEntityType()->hasKey('bundle')) { + $list_tags[] = $entity->getEntityTypeId() . '_list:' . $entity->bundle(); + } + $cache_tags = array_merge($cache_tags, $list_tags); + } + + // Add additional cache tags. + if (!empty($this->configuration['additional_tags'])) { + $additional_tags = array_map('trim', explode("\n", $this->configuration['additional_tags'])); + $additional_tags = array_filter($additional_tags); + $cache_tags = array_merge($cache_tags, $additional_tags); + } + + if (!count($cache_tags)) { + return FALSE; } - $combined_tags = array_merge($entity->getCacheTags(), $list_tags); - $cache_tags = implode(',', $combined_tags); + + // Remove duplicates and convert to comma-separated string. + $cache_tags = array_unique($cache_tags); + $cache_tags_string = implode(',', $cache_tags); /** @var \Drupal\next\Entity\NextSite $site */ foreach ($sites as $site) { try { - $revalidate_url = $site->buildRevalidateUrl(['tags' => $cache_tags]); + $revalidate_url = $site->buildRevalidateUrl(['tags' => $cache_tags_string]); if (!$revalidate_url) { throw new \Exception('No revalidate url set.'); } @@ -76,7 +173,7 @@ public function revalidate(EntityActionEvent $event): bool { if ($this->nextSettingsManager->isDebug()) { $this->logger->notice('(@action): Revalidating tags %list for the site %site. URL: %url', [ '@action' => $event->getAction(), - '%list' => $cache_tags, + '%list' => $cache_tags_string, '%site' => $site->label(), '%url' => $revalidate_url->toString(), ]); @@ -87,7 +184,7 @@ public function revalidate(EntityActionEvent $event): bool { if ($this->nextSettingsManager->isDebug()) { $this->logger->notice('(@action): Successfully revalidated tags %list for the site %site. URL: %url', [ '@action' => $event->getAction(), - '%list' => $cache_tags, + '%list' => $cache_tags_string, '%site' => $site->label(), '%url' => $revalidate_url->toString(), ]); From 9b8c874f88c5d366abcc0a406c2536abdfd296ac Mon Sep 17 00:00:00 2001 From: Senem Hartung Date: Tue, 10 Mar 2026 16:32:07 -0500 Subject: [PATCH 2/3] fix(next): improve CacheTag revalidator with validation and error handling - Add validation for additional_tags textarea (character restrictions, length limits) - Log exceptions instead of silent catch in buildConfigurationForm - Add debug logging when no cache tags found - Log non-200 HTTP response codes with warning level - Fix taxonomy_list to taxonomy_term_list in examples and descriptions --- .../src/Plugin/Next/Revalidator/CacheTag.php | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/modules/next/src/Plugin/Next/Revalidator/CacheTag.php b/modules/next/src/Plugin/Next/Revalidator/CacheTag.php index 9f0429d1..cea7f545 100644 --- a/modules/next/src/Plugin/Next/Revalidator/CacheTag.php +++ b/modules/next/src/Plugin/Next/Revalidator/CacheTag.php @@ -47,8 +47,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta [$entity_type_id, $bundle] = explode('.', $entity_bundle_string, 2); } } - catch (\Exception) { + catch (\Exception $exception) { // Fallback if we can't get the entity info. + Error::logException($this->logger, $exception); $entity_type_id = NULL; $bundle = NULL; } @@ -69,8 +70,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta $next_js_example = 'tags: ["node_list:' . $bundle . '"]'; } elseif ($entity_type_id === 'taxonomy_term') { - $list_tag_example = 'taxonomy_list:' . $bundle; - $next_js_example = 'tags: ["taxonomy_list:' . $bundle . '"]'; + $list_tag_example = 'taxonomy_term_list:' . $bundle; + $next_js_example = 'tags: ["taxonomy_term_list:' . $bundle . '"]'; } else { $list_tag_example = $entity_type_id . '_list:' . $bundle; @@ -87,7 +88,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta } else { $title = $this->t('Revalidate [entity_type]_list:[bundle] cache tags'); - $description = $this->t('Revalidates pages tagged with entity type and bundle list cache tags when entities change.
Node entities: generates node_list:[bundle] (e.g., node_list:article, node_list:person)
Taxonomy terms: generates taxonomy_list:[vocabulary] (e.g., taxonomy_list:tags)
Other entities: generates [entity_type]_list:[bundle]

In Next.js use: tags: ["node_list:article"] or tags: ["taxonomy_list:tags"]'); + $description = $this->t('Revalidates pages tagged with entity type and bundle list cache tags when entities change.
Node entities: generates node_list:[bundle] (e.g., node_list:article, node_list:person)
Taxonomy terms: generates taxonomy_term_list:[vocabulary] (e.g., taxonomy_term_list:tags)
Other entities: generates [entity_type]_list:[bundle]

In Next.js use: tags: ["node_list:article"] or tags: ["taxonomy_term_list:tags"]'); } $form['entity_list_tag'] = [ @@ -107,6 +108,38 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta return $form; } + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $additional_tags = $form_state->getValue('additional_tags'); + + if (!empty($additional_tags)) { + $tags = array_map('trim', explode("\n", $additional_tags)); + $tags = array_filter($tags); + + foreach ($tags as $tag) { + // Validate that each tag is a string and doesn't contain invalid characters. + if (!is_string($tag) || empty($tag)) { + $form_state->setErrorByName('additional_tags', $this->t('Each cache tag must be a non-empty string.')); + break; + } + + // Check for invalid characters (spaces, special characters that could break cache tags). + if (preg_match('/[^\w\-:._]/', $tag)) { + $form_state->setErrorByName('additional_tags', $this->t('Cache tags can only contain letters, numbers, hyphens, colons, periods, and underscores. Invalid tag: @tag', ['@tag' => $tag])); + break; + } + + // Check for reasonable length limit. + if (strlen($tag) > 255) { + $form_state->setErrorByName('additional_tags', $this->t('Cache tags must be 255 characters or less. Invalid tag: @tag', ['@tag' => $tag])); + break; + } + } + } + } + /** * {@inheritdoc} */ @@ -155,6 +188,12 @@ public function revalidate(EntityActionEvent $event): bool { } if (!count($cache_tags)) { + if ($this->nextSettingsManager->isDebug()) { + $this->logger->debug('No cache tags found for revalidation. Entity: @entity_type:@entity_id', [ + '@entity_type' => $entity->getEntityTypeId(), + '@entity_id' => $entity->id(), + ]); + } return FALSE; } @@ -192,6 +231,16 @@ public function revalidate(EntityActionEvent $event): bool { $revalidated = TRUE; } + else { + $status_code = $response ? $response->getStatusCode() : 'unknown'; + $this->logger->warning('(@action): Failed to revalidate tags %list for the site %site. HTTP status: %status. URL: %url', [ + '@action' => $event->getAction(), + '%list' => $cache_tags_string, + '%site' => $site->label(), + '%status' => $status_code, + '%url' => $revalidate_url->toString(), + ]); + } } catch (\Exception $exception) { Error::logException($this->logger, $exception); From 6269b44a0a93206bcd2d83301200b4bceb306330 Mon Sep 17 00:00:00 2001 From: Senem Hartung Date: Tue, 10 Mar 2026 16:39:04 -0500 Subject: [PATCH 3/3] fix(next): resolve PHPCS code style violations in CacheTag revalidator - Remove trailing whitespace from validation function - Split long comments to comply with 80-character line limit - Reformat array parameters to use proper multi-line format --- .../src/Plugin/Next/Revalidator/CacheTag.php | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/modules/next/src/Plugin/Next/Revalidator/CacheTag.php b/modules/next/src/Plugin/Next/Revalidator/CacheTag.php index cea7f545..6dfee5b2 100644 --- a/modules/next/src/Plugin/Next/Revalidator/CacheTag.php +++ b/modules/next/src/Plugin/Next/Revalidator/CacheTag.php @@ -113,27 +113,33 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { $additional_tags = $form_state->getValue('additional_tags'); - + if (!empty($additional_tags)) { $tags = array_map('trim', explode("\n", $additional_tags)); $tags = array_filter($tags); - + foreach ($tags as $tag) { - // Validate that each tag is a string and doesn't contain invalid characters. + // Validate that each tag is a string and doesn't contain invalid + // characters. if (!is_string($tag) || empty($tag)) { $form_state->setErrorByName('additional_tags', $this->t('Each cache tag must be a non-empty string.')); break; } - - // Check for invalid characters (spaces, special characters that could break cache tags). + + // Check for invalid characters (spaces, special characters that could + // break cache tags). if (preg_match('/[^\w\-:._]/', $tag)) { - $form_state->setErrorByName('additional_tags', $this->t('Cache tags can only contain letters, numbers, hyphens, colons, periods, and underscores. Invalid tag: @tag', ['@tag' => $tag])); + $form_state->setErrorByName('additional_tags', $this->t('Cache tags can only contain letters, numbers, hyphens, colons, periods, and underscores. Invalid tag: @tag', [ + '@tag' => $tag, + ])); break; } - + // Check for reasonable length limit. if (strlen($tag) > 255) { - $form_state->setErrorByName('additional_tags', $this->t('Cache tags must be 255 characters or less. Invalid tag: @tag', ['@tag' => $tag])); + $form_state->setErrorByName('additional_tags', $this->t('Cache tags must be 255 characters or less. Invalid tag: @tag', [ + '@tag' => $tag, + ])); break; } }