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..6dfee5b2 100644
--- a/modules/next/src/Plugin/Next/Revalidator/CacheTag.php
+++ b/modules/next/src/Plugin/Next/Revalidator/CacheTag.php
@@ -25,21 +25,134 @@ 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 $exception) {
+ // Fallback if we can't get the entity info.
+ Error::logException($this->logger, $exception);
+ $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_term_list:' . $bundle;
+ $next_js_example = 'tags: ["taxonomy_term_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_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'] = [
+ '#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;
}
+ /**
+ * {@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}
*/
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 +170,47 @@ 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)) {
+ 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;
}
- $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 +218,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 +229,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(),
]);
@@ -95,6 +237,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);