diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 556c154010c76df6006417b1f358470e642cc771..103073b18cbb53eb3ee00efc19ac1441e30f8c4d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,9 +35,9 @@ include: variables: OPT_IN_TEST_CURRENT: 1 - OPT_IN_TEST_MAX_PHP: 1 + OPT_IN_TEST_MAX_PHP: 0 # Broaden test coverage. OPT_IN_TEST_PREVIOUS_MAJOR: 1 OPT_IN_TEST_PREVIOUS_MINOR: 0 - OPT_IN_TEST_NEXT_MINOR: 1 - OPT_IN_TEST_NEXT_MAJOR: 1 + OPT_IN_TEST_NEXT_MINOR: 0 + OPT_IN_TEST_NEXT_MAJOR: 0 diff --git a/src/Form/TermGlossaryConfigForm.php b/src/Form/TermGlossaryConfigForm.php index a4ee31a066b30ba1742bfcade17682b2d587bf9f..89b5af8374d2ca8c5fba3f3432295ab4c9d9414b 100644 --- a/src/Form/TermGlossaryConfigForm.php +++ b/src/Form/TermGlossaryConfigForm.php @@ -38,7 +38,7 @@ class TermGlossaryConfigForm extends ConfigFormBase { /** * Plugin manager for term glossary handlers. * - * @var \Drupal\term_glossary\Service\TermGlossaryHandlerManager + * @var \Drupal\term_glossary\TermGlossaryHandlerInterface */ protected $handlerManager; @@ -287,11 +287,6 @@ class TermGlossaryConfigForm extends ConfigFormBase { $plugin = $this->handlerManager->createInstance($handler); $plugin->submitConfigurationForm($form, $form_state); } - - // Invalidate glossary terms cache. - foreach ($vocabularies as $vocabulary) { - $this->glossaryManager->invalidateTermsCache($vocabulary); - } } /** diff --git a/src/Service/TermGlossaryManager.php b/src/Service/TermGlossaryManager.php index 00a9e7dc317ddb97dd5681280335e10e5e5f25f3..5788197c9ade2237aa5bc486eaeb939c3816bc44 100644 --- a/src/Service/TermGlossaryManager.php +++ b/src/Service/TermGlossaryManager.php @@ -5,7 +5,6 @@ namespace Drupal\term_glossary\Service; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Cache\CacheTagsInvalidatorInterface; use Drupal\Core\Cache\Context\CacheContextsManager; use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\ImmutableConfig; @@ -82,7 +81,6 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { protected LoggerChannelInterface $logger, protected TermGlossaryHandlerPluginManager $handlerManager, protected EntityRepositoryInterface $entityRepository, - protected CacheTagsInvalidatorInterface $cacheTagsInvalidator, protected CacheContextsManager $cacheContextsManager, ) { $this->termStorage = $this->entityTypeManager->getStorage('taxonomy_term'); @@ -111,6 +109,23 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { return $this->handler; } + /** + * Escapes special characters for use in a regular expression character class. + * + * @param string $string + * The input string to escape. + * + * @return string + * The escaped string safe for use in a regex character class. + */ + private static function escapeCharClass($string) { + return str_replace( + ['\\', '/', '-', ']', '[', '^'], + ['\\\\', '\/', '\-', '\]', '\[', '\^'], + $string + ); + } + /** * Builds the regexp pattern to match a term. */ @@ -135,13 +150,14 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { $boundary_exceptions = $term_boundary_exceptions ?: $global_boundary_exceptions; if (empty($boundary_exceptions)) { // Use \b to match word boundaries. - $pattern = "/\b($quoted_name)\b/u"; + $pattern = "/\b($quoted_name)\b/"; } else { // Use negative lookbehind and lookahead to match word boundaries. // It allows avoiding matching punctuation characters like -:;. - $boundary = "(?:\p{L}|\p{N}|[" . $boundary_exceptions . "])"; - $pattern = "/(?<!$boundary)($quoted_name)(?!$boundary)/u"; + $escaped_boundary_exceptions = self::escapeCharClass($boundary_exceptions); + $boundary = "(?:\p{L}|\p{N}|[" . $escaped_boundary_exceptions . "])"; + $pattern = "/(?<!$boundary)($quoted_name)(?!$boundary)/"; } } else { @@ -250,9 +266,11 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { ? $root_entity->language()->getId() : $this->languageManager->getCurrentLanguage()->getId(); + $cache_tags = []; $term_list = []; foreach ($vocabularies as $vocabulary) { $term_list += $this->getTerms($vocabulary, $langcode); + $cache_tags[] = "taxonomy_term_list:$vocabulary"; } $is_single_match_per_field = $this->config->get('single_match') ?? FALSE; @@ -356,11 +374,9 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { } } - if ($ctx->tagIndex === 0) { - // No replacement has occurred, nothing to do. - return FALSE; - } - else { + $result = []; + $result['cache_tags'] = $cache_tags; + if ($ctx->tagIndex > 0) { // Replace all placeholders by their corresponding tags. $html = preg_replace_callback( '/\{\{(\d+)}}/', @@ -379,8 +395,10 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { }, Html::serialize($html_dom), ); - return ['html' => $html, 'count' => $ctx->tagIndex]; + $result['html'] = $html; + $result['count'] = $ctx->tagIndex; } + return $result; } } @@ -423,16 +441,8 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { /** * {@inheritDoc} */ - public function getConfigValue(string $key): ?string { - return $this->config->get($key); - } - - /** - * {@inheritDoc} - */ - public function invalidateTermsCache($vocabulary) { - $cache_tags = ["taxonomy_term_list:$vocabulary"]; - $this->cacheTagsInvalidator->invalidateTags($cache_tags); + public function getConfig(): ?ImmutableConfig { + return $this->config; } /** @@ -493,9 +503,9 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { $entity_ids = $this->loadTermIds($vocabulary, $langcode); $terms = $this->termStorage->loadMultiple($entity_ids); - $per_term_options = $this->getConfigValue('per_term_options') ?? FALSE; - $term_synonyms = $this->getConfigValue('term_synonyms') ?? FALSE; - $synonyms_field = $this->getConfigValue('synonyms_field') ?? NULL; + $per_term_options = $this->config->get('per_term_options') ?? FALSE; + $term_synonyms = $this->config->get('term_synonyms') ?? FALSE; + $synonyms_field = $this->config->get('synonyms_field') ?? NULL; $has_synonyms = $term_synonyms && $synonyms_field != NULL; $terms_array = []; /** @@ -549,7 +559,7 @@ class TermGlossaryManager implements TermGlossaryManagerInterface { $cid = $this->getCacheId($vocabulary, $langcode); // The cache will be invalidated when a term from this vocabulary is added, // updated, or deleted, due to the use of the following cache tag. - $cache_tags = ["taxonomy_term_list:$vocabulary"]; + $cache_tags = array_merge($this->config->getCacheTags(), ["taxonomy_term_list:$vocabulary"]); $this->cache->set($cid, $terms_array, CacheBackendInterface::CACHE_PERMANENT, $cache_tags); return $terms_array; } diff --git a/src/Service/TermGlossaryManagerInterface.php b/src/Service/TermGlossaryManagerInterface.php index 8a56621a8e948cae3145021fbc8c77f5b034f139..f4021a3e9671eb53c1af75665dcbe67c31068479 100644 --- a/src/Service/TermGlossaryManagerInterface.php +++ b/src/Service/TermGlossaryManagerInterface.php @@ -2,6 +2,7 @@ namespace Drupal\term_glossary\Service; +use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FormatterInterface; @@ -34,15 +35,12 @@ interface TermGlossaryManagerInterface { public function attachLibrariesAndSettings(&$variables); /** - * Get configuration. + * Gets the module's immutable configuration object. * - * @param string $key - * Config key. - * - * @return string|null - * Config value. + * @return \Drupal\Core\Config\ImmutableConfig|null + * The immutable configuration object or NULL if not available. */ - public function getConfigValue(string $key): ?string; + public function getConfig(): ?ImmutableConfig; /** * Get vocabularies from preprocess variables array. @@ -59,11 +57,6 @@ interface TermGlossaryManagerInterface { */ public function getRootEntityFromFieldPreprocessVariables($variables); - /** - * Flush the glossary terms cache. - */ - public function invalidateTermsCache($vocabulary); - /** * Gets the configured term glossary handler. */ diff --git a/term_glossary.module b/term_glossary.module index 2d3b87b08c8835275c1ede241d4b2a66c0567572..464c3af30f30776e7ed63e8f6416c3f9da37584f 100644 --- a/term_glossary.module +++ b/term_glossary.module @@ -8,6 +8,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FormatterInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Routing\RouteMatchInterface; /** @@ -119,12 +120,22 @@ function term_glossary_preprocess_field(&$variables) { $result = $glossaryManager->replaceFieldValue( $markup->jsonSerialize(), $vocabularies, $root_entity); if (is_array($result)) { - $replacements += $result['count']; - // Replace field markup content. - $variables['items'][$key]['content'] = [ - '#type' => 'markup', - '#markup' => $result['html'], - ]; + if (isset($result['html'])) { + $replacements += $result['count']; + // Replace field markup content. + $variables['items'][$key]['content'] = [ + '#type' => 'markup', + '#markup' => $result['html'], + ]; + } + // Attach cache tags to the render array. + $bubbleable_metadata = new BubbleableMetadata(); + $cache_tags = $glossaryManager->getConfig()->getCacheTags(); + if (!empty($result['cache_tags'])) { + $cache_tags = array_merge($cache_tags, $result['cache_tags']); + } + $bubbleable_metadata->setCacheTags($cache_tags); + $bubbleable_metadata->applyTo($variables); } } // Inject libraries and settings when replacements occurred. diff --git a/term_glossary.services.yml b/term_glossary.services.yml index 6e95230d99b96e8ead589105450bf6a7d0f1c7c0..62799af70291bbc9daacdbc3ce7ffbf609553e4b 100644 --- a/term_glossary.services.yml +++ b/term_glossary.services.yml @@ -19,5 +19,4 @@ services: - '@logger.channel.term_glossary' - '@plugin.manager.term_glossary.term_glossary_handler' - '@entity.repository' - - '@cache_tags.invalidator' - '@cache_contexts_manager'