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);