diff --git a/README.md b/README.md index 54564663..a7944b16 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ # QuantCDN Drupal module -This module acts as a static site generator for Drupal and integrates with the [QuantCDN static edge](https://www.quantcdn.io), and is compatible with Drupal 9.x and 10.x. +This module acts as a static site generator for Drupal and integrates with the +[QuantCDN static edge](https://www.quantcdn.io), and is compatible with Drupal +9.x and 10.x. -QuantCDN is a global CDN engineered specifically for the static web and Jamstack. +QuantCDN is a global CDN engineered specifically for the static web and +Jamstack. -It allows one-time compilation and push of an entire Drupal site, as well as tracking and pushes of ongoing content change; the simplest way to export and maintain a static export of your Drupal site. +It allows one-time compilation and push of an entire Drupal site, as well as +tracking and pushes of ongoing content change; the simplest way to export and +maintain a static export of your Drupal site. Development, issues and feature roadmap occurs in the [GitHub repository](https://github.com/quantcdn/drupal). diff --git a/modules/quant_api/src/Client/QuantClient.php b/modules/quant_api/src/Client/QuantClient.php index 1f03c168..bfa0b670 100644 --- a/modules/quant_api/src/Client/QuantClient.php +++ b/modules/quant_api/src/Client/QuantClient.php @@ -231,6 +231,45 @@ public function search() { return FALSE; } + /** + * {@inheritdoc} + */ + public function purgePath(string $path) : array { + + $response = $this->client->post($this->endpoint . '/purge', [ + RequestOptions::JSON => [], + 'headers' => [ + 'Quant-Customer' => $this->username, + 'Quant-Project' => $this->project, + 'Quant-Token' => $this->token, + 'Quant-Url' => $path, + ], + 'verify' => $this->tlsDisabled ? FALSE : TRUE, + ]); + + return json_decode($response->getBody(), TRUE); + } + + /** + * {@inheritdoc} + */ + public function purgeTags(array $tags) : array { + + $response = $this->client->post($this->endpoint . '/purge', [ + RequestOptions::JSON => [], + 'headers' => [ + 'Quant-Customer' => $this->username, + 'Quant-Project' => $this->project, + 'Quant-Token' => $this->token, + 'Cache-Keys' => implode(' ', $tags), + ], + 'verify' => $this->tlsDisabled ? FALSE : TRUE, + ]); + + return json_decode($response->getBody(), TRUE); + + } + /** * {@inheritdoc} */ diff --git a/modules/quant_api/src/Client/QuantClientInterface.php b/modules/quant_api/src/Client/QuantClientInterface.php index cc7a40fc..60dc8930 100644 --- a/modules/quant_api/src/Client/QuantClientInterface.php +++ b/modules/quant_api/src/Client/QuantClientInterface.php @@ -47,6 +47,34 @@ public function search(); */ public function send(array $data) : array; + /** + * Sends a purge payload (path-based) to the API. + * + * @param string $path + * The path to purge. + * + * @return array + * Return array of response data. + * + * @throws \Drupal\quant_api\Exception\InvalidPayload + * @throws \Drupal\quant_api\Exception\InvalidResponse + */ + public function purgePath(string $path) : array; + + /** + * Sends a purge payload (tags-based) to the API. + * + * @param array $tags + * The array of tags to purge. + * + * @return array + * Return array of response data. + * + * @throws \Drupal\quant_api\Exception\InvalidPayload + * @throws \Drupal\quant_api\Exception\InvalidResponse + */ + public function purgeTags(array $tags) : array; + /** * Send a file to the API. * diff --git a/modules/quant_purger/README.md b/modules/quant_purger/README.md index d57b0926..baf51f5f 100644 --- a/modules/quant_purger/README.md +++ b/modules/quant_purger/README.md @@ -1,7 +1,25 @@ -# Quant cache tag purger +# Quant Purger -Adds a cache tag plugin which listens to Drupal invalidation events in order to -queue Quant updates for related content. +The Quant Purger helps you keep content fresh on your static Quant site +after content updates within Drupal. + +## Purge Plugins + +This module is built on top of the [Purge module suite](https://www.drupal.org/project/purge). + +### Purger Plugin + +Processes cache invalidations based on type: 'everything', 'path' and 'tag'. +The Quant cache will be purged based on these invalidations. + +- *Everything:* A site-wide cache purge, e.g. `/*`. +- *Path:* Purges the given path. +- *Tag:* Purges the given tag. + +### Queuer Plugin + +Adds a cache tag queuer plugin which listens to Drupal invalidation events in +order to queue Quant updates for related content. For example, this allows node edits to trigger the main (`/node`) page to update along with any other pages associated with the node through cache tags (e.g. @@ -14,6 +32,12 @@ To ensure that queued content is processed in a timely manner, you can set up a Quant cron process that is separate from the core cron which just processes the Quant queue. This Quant cron can be run more regularly than the core cron. +### TagsHeader Plugin + +Sets and formats the default response header with hashed cache tags. + +## Documentation + See [Quant Purger documentation](https://docs.quantcdn.io/docs/integrations/drupal/purger) for additional information. diff --git a/modules/quant_purger/config/schema/quant_purger.data_types.schema.yml b/modules/quant_purger/config/schema/quant_purger.data_types.schema.yml new file mode 100644 index 00000000..1d372b05 --- /dev/null +++ b/modules/quant_purger/config/schema/quant_purger.data_types.schema.yml @@ -0,0 +1,10 @@ +quant_purger_header: + type: mapping + label: 'Quant Purger Header' + mapping: + field: + type: string + translatable: false + value: + type: string + translatable: false diff --git a/modules/quant_purger/config/schema/quant_purger.schema.yml b/modules/quant_purger/config/schema/quant_purger.schema.yml index 887cf0d5..20666544 100644 --- a/modules/quant_purger/config/schema/quant_purger.schema.yml +++ b/modules/quant_purger/config/schema/quant_purger.schema.yml @@ -1,21 +1,80 @@ - # Schema for the configuration files of the purge_queuer_url module. quant_purger.settings: type: config_object - label: 'Quant purger settings.' + label: 'Quant Purger Queuer Settings' mapping: - tag_blacklist: - label: 'A list of string tags that will not trigger a queue.' + tag_blocklist: + label: 'A list of tags that will not get queued.' type: sequence translatable: false sequence: type: string - label: 'String that cannot be present in the ccache tag.' + label: 'String that cannot be present in the cache tag.' translatable: false - path_blacklist: - label: 'A list of string patterns that will not get queued.' + tag_allowlist: + label: 'A list of tags that can get queued.' + type: sequence + translatable: false + sequence: + type: string + label: 'String that can be present in the cache tag.' + translatable: false + path_blocklist: + label: 'A list of paths that will not get queued.' type: sequence translatable: false sequence: type: string label: 'String that cannot be present in a fully qualified URL.' translatable: false + path_allowlist: + label: 'A list of paths that can not get queued.' + type: sequence + translatable: false + sequence: + type: string + label: 'String that can be present in a fully qualified URL.' + translatable: false + +quant_purger.settings.*: + type: config_entity + label: 'Quant Purger Settings' + mapping: + + # + # Instance metadata: + # + id: + type: string + translatable: false + name: + type: string + translatable: false + invalidation_type: + type: string + translatable: false + + # + # Performance settings: + # + runtime_measurement: + type: boolean + translatable: false + timeout: + type: float + translatable: false + connect_timeout: + type: float + translatable: false + cooldown_time: + type: float + translatable: false + max_requests: + type: integer + translatable: false + + # + # Success resolution: + # + http_errors: + type: boolean + translatable: false diff --git a/modules/quant_purger/quant_purger.info.yml b/modules/quant_purger/quant_purger.info.yml index 5c819d3c..6990a86a 100644 --- a/modules/quant_purger/quant_purger.info.yml +++ b/modules/quant_purger/quant_purger.info.yml @@ -1,5 +1,5 @@ name: Quant Purger -description: Cache tag purger for Quant. +description: Purge content in Quant based on paths and cache tags. package: Quant type: module diff --git a/modules/quant_purger/quant_purger.services.yml b/modules/quant_purger/quant_purger.services.yml index 01ec8b59..2120f704 100644 --- a/modules/quant_purger/quant_purger.services.yml +++ b/modules/quant_purger/quant_purger.services.yml @@ -11,7 +11,7 @@ services: tags: - { name: http_middleware, priority: 250 } quant_purger.queuer: - class: Drupal\quant_purger\Plugin\Purge\Queuer\QuantPurger + class: Drupal\quant_purger\Plugin\Purge\Queuer\QuantPurgerQueuerInvalidator tags: - { name: cache_tags_invalidator } calls: diff --git a/modules/quant_purger/src/Entity/Hash.php b/modules/quant_purger/src/Entity/Hash.php new file mode 100644 index 00000000..e9880037 --- /dev/null +++ b/modules/quant_purger/src/Entity/Hash.php @@ -0,0 +1,70 @@ + 4) { + $hashes[] = self::hashInput($tag, 4); + } + else { + $hashes[] = $tag; + } + } + return $hashes; + } + + /** + * Create a unique hash that identifies this site. + * + * @param string $site_name + * The identifier of the site on QuantCDN. + * @param string $site_path + * The path of the site, e.g. 'site/default' or 'site/database_a'. + * + * @return string + * Cryptographic hash that's long enough to be unique. + */ + public static function siteIdentifier($site_name, $site_path) { + return self::hashInput($site_name . $site_path, 16); + } + +} diff --git a/modules/quant_purger/src/Entity/QuantPurgerSettings.php b/modules/quant_purger/src/Entity/QuantPurgerSettings.php new file mode 100644 index 00000000..dbbe21b5 --- /dev/null +++ b/modules/quant_purger/src/Entity/QuantPurgerSettings.php @@ -0,0 +1,116 @@ +setConfigFactory($config_factory); + $this->purgeInvalidationFactory = $purge_invalidation_factory; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('purge.invalidation.factory'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'quant_purger.configuration_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $settings = QuantPurgerSettings::load($this->getId($form_state)); + $form['tabs'] = ['#type' => 'vertical_tabs', '#weight' => 10]; + $this->buildFormMetadata($form, $form_state, $settings); + $this->buildFormPerformance($form, $form_state, $settings); + return parent::buildForm($form, $form_state); + } + + /** + * Build the 'metadata' section of the form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param \Drupal\quant_purger\Entity\QuantPurgerSettings $settings + * Configuration entity for the purger being configured. + */ + public function buildFormMetadata(array &$form, FormStateInterface $form_state, QuantPurgerSettings $settings) { + $form['name'] = [ + '#title' => $this->t('Name'), + '#type' => 'textfield', + '#description' => $this->t('Purger to purge QuantCDN content.'), + '#default_value' => $settings->name, + '#required' => TRUE, + ]; + $types = []; + foreach ($this->purgeInvalidationFactory->getPlugins() as $type => $definition) { + $types[$type] = (string) $definition['label']; + } + } + + /** + * Build the 'performance' section of the form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param \Drupal\quant_purger\Entity\QuantPurgerSettings $settings + * Configuration entity for the purger being configured. + */ + public function buildFormPerformance(array &$form, FormStateInterface $form_state, QuantPurgerSettings $settings) { + $form['performance'] = [ + '#type' => 'details', + '#group' => 'tabs', + '#title' => $this->t('Performance'), + ]; + $form['performance']['cooldown_time'] = [ + '#type' => 'number', + '#step' => 0.1, + '#min' => 0.0, + '#max' => 3.0, + '#title' => $this->t('Cooldown time'), + '#default_value' => $settings->cooldown_time, + '#required' => TRUE, + '#description' => $this->t('Number of seconds to wait after a group of HTTP requests so that other purgers get fresh content.'), + ]; + $form['performance']['max_requests'] = [ + '#type' => 'number', + '#step' => 1, + '#min' => 1, + '#max' => 500, + '#title' => $this->t('Maximum requests'), + '#default_value' => $settings->max_requests, + '#required' => TRUE, + '#description' => $this->t("Maximum number of HTTP requests that can be made during Drupal's execution lifetime. Usually PHP resource restraints lower this value dynamically, but can be met at the CLI."), + ]; + $form['performance']['runtime_measurement'] = [ + '#title' => $this->t('Runtime measurement'), + '#type' => 'checkbox', + '#default_value' => $settings->runtime_measurement, + ]; + $form['performance']['runtime_measurement_help'] = [ + '#type' => 'item', + '#states' => [ + 'visible' => [ + ':input[name="runtime_measurement"]' => ['checked' => FALSE], + ], + ], + '#description' => $this->t('When you uncheck this setting, capacity will be based on the sum of both timeouts. By default, capacity will automatically adjust (up and down) based on measured time data.'), + ]; + $form['performance']['timeout'] = [ + '#type' => 'number', + '#step' => 0.1, + '#min' => 0.1, + '#max' => 8.0, + '#title' => $this->t('Timeout'), + '#default_value' => $settings->timeout, + '#required' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name="runtime_measurement"]' => ['checked' => FALSE], + ], + ], + '#description' => $this->t('The timeout of the request in seconds.'), + ]; + $form['performance']['connect_timeout'] = [ + '#type' => 'number', + '#step' => 0.1, + '#min' => 0.1, + '#max' => 4.0, + '#title' => $this->t('Connection timeout'), + '#default_value' => $settings->connect_timeout, + '#required' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name="runtime_measurement"]' => ['checked' => FALSE], + ], + ], + '#description' => $this->t('The number of seconds to wait while trying to connect to a server.'), + ]; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + + // Validate that our timeouts stay between the boundaries purge demands. + $timeout = $form_state->getValue('connect_timeout') + $form_state->getValue('timeout'); + if ($timeout > 10) { + $form_state->setErrorByName('connect_timeout'); + $form_state->setErrorByName('timeout', $this->t('The sum of both timeouts cannot be higher than 10.00 as this would affect performance too negatively.')); + } + elseif ($timeout < 0.4) { + $form_state->setErrorByName('connect_timeout'); + $form_state->setErrorByName('timeout', $this->t('The sum of both timeouts cannot be lower as 0.4 as this can lead to too many failures under real usage conditions.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitFormSuccess(array &$form, FormStateInterface $form_state) { + $settings = QuantPurgerSettings::load($this->getId($form_state)); + + // Iterate the config object and overwrite values found in the form state. + foreach ($settings as $key => $default_value) { + if (!is_null($value = $form_state->getValue($key))) { + $settings->$key = $value; + } + } + $settings->save(); + } + +} diff --git a/modules/quant_purger/src/Form/ConfigurationForm.php b/modules/quant_purger/src/Form/QuantPurgerQueuerConfigForm.php similarity index 91% rename from modules/quant_purger/src/Form/ConfigurationForm.php rename to modules/quant_purger/src/Form/QuantPurgerQueuerConfigForm.php index cde184a4..f16cacdb 100644 --- a/modules/quant_purger/src/Form/ConfigurationForm.php +++ b/modules/quant_purger/src/Form/QuantPurgerQueuerConfigForm.php @@ -8,14 +8,16 @@ use Drupal\purge_ui\Form\QueuerConfigFormBase; /** - * Configuration form for the Quant queuer. + * Configuration form for the Quant Purger Queuer. */ -class ConfigurationForm extends QueuerConfigFormBase { +class QuantPurgerQueuerConfigForm extends QueuerConfigFormBase { /** * {@inheritdoc} */ protected function getEditableConfigNames() { + // @todo There are multiple purger config forms. Should this be + // 'quant_purger.queuer_settings'? Requires an update hook. return ['quant_purger.settings']; } @@ -23,6 +25,8 @@ protected function getEditableConfigNames() { * {@inheritdoc} */ public function getFormId() { + // @todo There are multiple purger config forms. Should this be + // 'quant_purger.queuer_config_form'? Requires an update hook. return 'quant_purger.configuration_form'; } @@ -39,7 +43,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { ]; $form['info'] = [ - '#markup' => $this->t('

After making changes to the Quant Purger configuration, all content must be re-seeded so the database reflects the changes.

'), + '#markup' => $this->t('

After making changes to the Quant Purger Queuer configuration, all content must be re-seeded so the database reflects the changes.

'), '#weight' => -10, ]; @@ -153,7 +157,7 @@ public function submitFormSuccess(array &$form, FormStateInterface $form_state) ->set('path_allowlist', $form_state->getValue('path_allowlist')) ->save(); - \Drupal::messenger()->addMessage($this->t('Successfully saved the Quant Purger configuration. All content must be re-seeded so the database reflects the changes.')); + \Drupal::messenger()->addMessage($this->t('Successfully saved the Quant Purger Queuer configuration. All content must be re-seeded so the database reflects the changes.')); } /** diff --git a/modules/quant_purger/src/Plugin/Purge/Purger/QuantPurger.php b/modules/quant_purger/src/Plugin/Purge/Purger/QuantPurger.php new file mode 100644 index 00000000..3df3c4e8 --- /dev/null +++ b/modules/quant_purger/src/Plugin/Purge/Purger/QuantPurger.php @@ -0,0 +1,314 @@ +settings = QuantPurgerSettings::load($this->getId()); + // Note: We use the Quant HTTP client rather than the generic Guzzle client. + $this->client = \Drupal::service('quant_api.client'); + $this->token = $token; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('http_client'), + $container->get('token') + ); + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + if ($this->settings->name) { + return $this->settings->name; + } + else { + return parent::getLabel(); + } + } + + /** + * {@inheritdoc} + */ + public function getTypes() { + // @todo Add url? + return [ + 'everything', + 'path', + 'tag', + ]; + } + + /** + * {@inheritdoc} + */ + public function delete() { + QuantPurgerSettings::load($this->getId())->delete(); + } + + /** + * {@inheritdoc} + */ + public function invalidate(array $invalidations) { + + // The processed data may include 'everything', 'paths' and/or 'tags'. + $processed = $this->processInvalidations($invalidations); + + // The 'everything' invaldiation type takes precedence. + if (!empty($processed['everything'])) { + $this->invalidateEverything($invalidations); + return; + } + + // Otherwise handle any paths and tags. + if (!empty($processed['paths'])) { + $this->invalidatePaths($processed['paths']); + } + + if (!empty($processed['tags'])) { + $this->invalidateTags($processed['tags']); + } + + } + + /** + * Process invalidations based on type. + * + * @param array $invalidations + * The array of Invalidation items to process. + * + * @return array + * The processed array with 'everything', 'paths' and 'tags' keys. + */ + public function processInvalidations(array $invalidations) { + $everything = FALSE; + $paths = []; + $tags = []; + + foreach ($invalidations as $invalidation) { + + if ($invalidation->getType() == 'everything') { + // 'Everything' takes precedence and will issue a site-wide purge. + $invalidation->setState(InvalidationInterface::PROCESSING); + $everything = TRUE; + $paths = []; + $tags = []; + break; + } + elseif ($invalidation->getType() == 'path') { + $invalidation->setState(InvalidationInterface::PROCESSING); + $paths[] = $invalidation; + } + elseif ($invalidation->getType() == 'tag') { + $invalidation->setState(InvalidationInterface::PROCESSING); + $tags[] = $invalidation; + } + else { + $invalidation->setState(InvalidationInterface::NOT_SUPPORTED); + $this->logger()->warning('Invalidation type not supported: @type', ['@type' => $invalidation->getType()]); + } + } + + $processed = [ + 'everything' => $everything, + 'paths' => $paths, + 'tags' => $tags, + ]; + + return $processed; + } + + /** + * Invalidate with the path '/*' to purge the entire project cache. + * + * @param array $invalidations + * Array of Invalidation objects to process. + */ + public function invalidateEverything(array $invalidations) { + + try { + $this->logger()->debug('[everything] Purging entire site cache (/*)'); + $this->purgePath('/*'); + foreach ($invalidations as $invalidation) { + $invalidation->setState(InvalidationInterface::SUCCEEDED); + } + } + catch (\Exception $e) { + $this->logger()->error('Error attempting to purge entire cache: @message', ['@message' => $e->getMessage()]); + error_log($e->getMessage()); + foreach ($invalidations as $invalidation) { + $invalidation->setState(InvalidationInterface::FAILED); + } + } + } + + /** + * Invalidate path-based invalidations. + * + * @param array $invalidations + * Array of Invalidation objects to process. + */ + public function invalidatePaths(array $invalidations) { + + foreach ($invalidations as $invalidation) { + try { + $path = '/' . $invalidation->getExpression(); + $this->logger()->debug('[path] Purging path: @path', ['@path' => $path]); + $this->purgePath($path); + $invalidation->setState(InvalidationInterface::SUCCEEDED); + } + catch (\Exception $e) { + $this->logger()->error('Error attempting to purge paths: @message', ['@message' => $e->getMessage()]); + error_log($e->getMessage()); + $invalidation->setState(InvalidationInterface::FAILED); + } + } + } + + /** + * Invalidate tag-based invalidations. + * + * @param array $invalidations + * Array of Invalidation objects to process. + */ + public function invalidateTags(array $invalidations) { + try { + // Log tags prior to hashing. + $this->logger()->debug('[tags] Purging tags: @tags', ['@tags' => implode(' ', $invalidations)]); + + $tags = []; + foreach ($invalidations as $invalidation) { + $tags[] = Hash::cacheTags([$invalidation->getExpression()])[0]; + } + + $this->purgeTags($tags); + $invalidation->setState(InvalidationInterface::SUCCEEDED); + } + catch (\Exception $e) { + $this->logger()->error('Error attempting to purge tags: @message', ['@message' => $e->getMessage()]); + error_log($e->getMessage()); + $invalidation->setState(InvalidationInterface::FAILED); + } + } + + /** + * {@inheritdoc} + */ + public function getCooldownTime() { + return $this->settings->cooldown_time; + } + + /** + * {@inheritdoc} + */ + public function getIdealConditionsLimit() { + return $this->settings->max_requests; + } + + /** + * {@inheritdoc} + */ + public function getTimeHint() { + // When runtime measurement is enabled, we just use the base implementation. + if ($this->settings->runtime_measurement) { + return parent::getTimeHint(); + } + // Theoretically connection timeouts and general timeouts can add up, so + // we add up our assumption of the worst possible time it takes as well. + return $this->settings->connect_timeout + $this->settings->timeout; + } + + /** + * {@inheritdoc} + */ + public function hasRuntimeMeasurement() { + return (bool) $this->settings->runtime_measurement; + } + + /** + * Sends a path-based purge request to the Quant API. + * + * @param string $path + * The path to purge. + */ + public function purgePath(string $path) { + $this->client->purgePath($path); + } + + /** + * Sends a tags-based purge request to the Quant API. + * + * @param array $tags + * The array of tags to purge. + */ + public function purgeTags(array $tags) { + $this->client->purgeTags($tags); + } + +} diff --git a/modules/quant_purger/src/Plugin/Purge/Queuer/QuantPurgerPlugin.php b/modules/quant_purger/src/Plugin/Purge/Queuer/QuantPurgerPlugin.php deleted file mode 100644 index 7ab7e9c2..00000000 --- a/modules/quant_purger/src/Plugin/Purge/Queuer/QuantPurgerPlugin.php +++ /dev/null @@ -1,20 +0,0 @@ -tags = array_unique($tags); + $this->tagsHashed = array_unique($tags_hashed); + } + + /** + * Generate the header value for a cache tags header. + * + * @return string + * String representation of the cache tags for use on headers. + */ + public function __toString() { + return implode(self::SEPARATOR, $this->tagsHashed); + } + + /** + * Get an associative array mapping keys. + * + * @return array + * Associative mapping original and hashed cache tags. + */ + public function getTagsMap() { + return array_combine($this->tags, $this->tagsHashed); + } + +}