From ae30f136d36d28b4c93eb91af55ed1d7abdfb3b2 Mon Sep 17 00:00:00 2001 From: silversk8r <silversk8r@93768.no-reply.drupal.org> Date: Thu, 25 Apr 2019 08:49:17 +0200 Subject: [PATCH] Issue #3049065 by aludescher: Move cache invalidator out of the sanitizer service --- README.md | 8 +- cache_tools.module | 27 ++-- cache_tools.services.yml | 5 +- src/Service/CacheInvalidator.php | 241 +++++++++++++++++++++++++++++++ src/Service/CacheSanitizer.php | 204 ++++---------------------- 5 files changed, 297 insertions(+), 188 deletions(-) create mode 100644 src/Service/CacheInvalidator.php diff --git a/README.md b/README.md index 41af63c..890d5dd 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ will produce the cache tag `node_article_pub:field_author:123` where 123 is the value of the field_author field (author id). Invalidation is handled automatically during entity operations: -1. Insert published: all non-empty field values. -2. Delete published: all non-empty field values. -3. Update published: only the modified field values. -4. Update and publish: all non-empty field values. +1. Insert published: all non-empty field values. +2. Delete published: all non-empty field values. +3. Update published: only the modified field values. +4. Update and publish: all non-empty field values. 5. Update and unpublish: all non-empty original field values. Use case for this: diff --git a/cache_tools.module b/cache_tools.module index 50422c5..c05ef57 100644 --- a/cache_tools.module +++ b/cache_tools.module @@ -22,12 +22,16 @@ function cache_tools_entity_type_alter(array &$entity_types) { * Invalidates `entitytype_entitybundle_pub` and * `entitytype_entitybundle_pub:field_name:value` if entity is going to be * published. Entity type or a field needs to be allowed for invalidation. + * + * @todo + * Use a PublishedEntityCacheTag event subscriber instead of hooks after: + * https://www.drupal.org/project/drupal/issues/2551893 . */ function cache_tools_entity_insert(EntityInterface $entity) { - /** @var \Drupal\cache_tools\Service\CacheSanitizer $cache_sanitizer */ - $cache_sanitizer = \Drupal::service('cache_tools.cache.sanitizer'); - $cache_sanitizer->invalidatePublishedEntity($entity); - $cache_sanitizer->invalidatePublishedEntityFields($entity); + /** @var \Drupal\cache_tools\Service\CacheInvalidator $cache_invalidator */ + $cache_invalidator = \Drupal::service('cache_tools.cache.invalidator'); + $cache_invalidator->invalidatePublishedEntity($entity); + $cache_invalidator->invalidatePublishedEntityFields($entity); } /** @@ -39,10 +43,10 @@ function cache_tools_entity_insert(EntityInterface $entity) { * a field needs to be allowed for invalidation. */ function cache_tools_entity_update(EntityInterface $entity) { - /** @var \Drupal\cache_tools\Service\CacheSanitizer $cache_sanitizer */ - $cache_sanitizer = \Drupal::service('cache_tools.cache.sanitizer'); - $cache_sanitizer->invalidatePublishedEntity($entity); - $cache_sanitizer->invalidatePublishedEntityFields($entity); + /** @var \Drupal\cache_tools\Service\CacheInvalidator $cache_invalidator */ + $cache_invalidator = \Drupal::service('cache_tools.cache.invalidator'); + $cache_invalidator->invalidatePublishedEntity($entity); + $cache_invalidator->invalidatePublishedEntityFields($entity); } /** @@ -51,9 +55,10 @@ function cache_tools_entity_update(EntityInterface $entity) { * Invalidates `entitytype_entitybundle_pub:field_name:value` if deleting * published entity. * Entity type or a field needs to be allowed for invalidation. + * Note: Deleted entities are invalidated via entity_type:id (eg node:123). */ function cache_tools_entity_delete(EntityInterface $entity) { - /** @var \Drupal\cache_tools\Service\CacheSanitizer $cache_sanitizer */ - $cache_sanitizer = \Drupal::service('cache_tools.cache.sanitizer'); - $cache_sanitizer->invalidatePublishedEntityFields($entity); + /** @var \Drupal\cache_tools\Service\CacheInvalidator $cache_invalidator */ + $cache_invalidator = \Drupal::service('cache_tools.cache.invalidator'); + $cache_invalidator->invalidatePublishedEntityFields($entity); } diff --git a/cache_tools.services.yml b/cache_tools.services.yml index faec1ac..97bf38b 100644 --- a/cache_tools.services.yml +++ b/cache_tools.services.yml @@ -1,7 +1,10 @@ services: + cache_tools.cache.invalidator: + class: Drupal\cache_tools\Service\CacheInvalidator + arguments: ['@cache_tags.invalidator','%cache_tools%'] cache_tools.cache.sanitizer: class: Drupal\cache_tools\Service\CacheSanitizer - arguments: ['@cache_tags.invalidator', '%cache_tools%'] + arguments: ['@cache_tools.cache.invalidator', '%cache_tools%'] parameters: cache_tools: diff --git a/src/Service/CacheInvalidator.php b/src/Service/CacheInvalidator.php new file mode 100644 index 0000000..f524067 --- /dev/null +++ b/src/Service/CacheInvalidator.php @@ -0,0 +1,241 @@ +<?php + +namespace Drupal\cache_tools\Service; + +use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\Core\Entity\EntityPublishedInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\FieldableEntityInterface; + +/** + * For invalidating cache_tools cache tags for entity create, update, delete. + * + * @see cache_tools.module for usage. + */ +class CacheInvalidator { + + /** + * The cache tag invalidator. + * + * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface + */ + protected $cacheTagsInvalidator; + + /** + * Cache tools settings. + * + * @var array + */ + protected $settings; + + /** + * CacheSanitizer constructor. + * + * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator + * The cache tag invalidator. + * @param array $settings + * The cache settings. + */ + public function __construct(CacheTagsInvalidatorInterface $cache_tags_invalidator, array $settings) { + $this->cacheTagsInvalidator = $cache_tags_invalidator; + $this->settings = $settings; + } + + /** + * Get published cache tag in format `entitytype_pub`. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entityType + * Entity. + * + * @return string + * Published cache tag. + */ + public function getPublishedEntityTypeCacheTag(EntityTypeInterface $entityType) { + return $entityType->id() . '_pub'; + } + + /** + * Get published cache tag in format `entitytype_entitybundle_pub`. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Entity. + * + * @return string + * Published ache tag. + */ + public function getPublishedEntityCacheTag(EntityInterface $entity) { + return $entity->getEntityTypeId() . '_' . $entity->bundle() . '_pub'; + } + + /** + * Invalidates published entity. + * + * On entity update where original entity is unpublished and + * going to be published and on entity insert where new entity is published. + * Other cases are already covered by other tags: + * 1. Unpublished entities (created or updated) does not affect anything. + * 2. Published entities which stay published are invalidated via + * entity_type:id (eg node:123). + * 3. Published entities going to be deleted or unpublished are invalidated + * via entity_type:id (eg node:123). + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Entity. + * + * @return bool + * True if successful invalidation. False otherwise. + */ + public function invalidatePublishedEntity(EntityInterface $entity) { + // Skip if entity type is not allowed by entity type and bundle. + if (!isset($this->settings['invalidate'][$entity->getEntityTypeId()])) { + return FALSE; + } + if (!in_array($entity->bundle(), $this->settings['invalidate'][$entity->getEntityTypeId()])) { + return FALSE; + } + $tags = [ + $this->getPublishedEntityTypeCacheTag($entity->getEntityType()), + $this->getPublishedEntityCacheTag($entity), + ]; + // If this is update we need to take a look at original entity as well. + $entities = [$entity]; + if ($entity->original) { + $entities[] = $entity->original; + } + // If entity is going to be unpublished (insert) + // or stays unpublished (update), skip. + foreach ($entities as $index => $entity) { + if (empty($entity->get('status')->value)) { + unset($entities[$index]); + } + } + if ($entities) { + $this->cacheTagsInvalidator->invalidateTags($tags); + return TRUE; + } + return FALSE; + } + + /** + * Get field cache tags for configured fields having (modified) values. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * Entity. + * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity_compare + * (optional) An entity to compare field values with. When provided only + * non-equal field values will be considered. + * + * @return string[] + * The custom cache tags `entitytype_entitybundle_pub:field_name:value`. + */ + public function getPublishedEntityFieldsCacheTags(FieldableEntityInterface $entity, FieldableEntityInterface $entity_compare = NULL) { + $tags = []; + $entity_type = $entity->getEntityTypeId(); + // Get field-based tags configured for current entity bundle. + $bundle = $entity->bundle(); + $tag_prefix = $this->getPublishedEntityCacheTag($entity) . ':'; + foreach ($this->settings['invalidate'][$entity_type] as $cache_parameter) { + $parts = explode(':', $cache_parameter); + if (count($parts) != 2 || $parts[0] != $bundle) { + // This setting is not for the current bundle or not field-based. + continue; + } + $field_name = $parts[1]; + if ($entity->hasField($field_name)) { + // The name of the value property, e.g. 'value' or 'target_id'. + $key = $entity + ->getFieldDefinition($field_name) + ->getFieldStorageDefinition() + ->getMainPropertyName(); + if (is_null($key)) { + // The field has no main value property. + continue; + } + $tag_prefix_field = $tag_prefix . $field_name . ':'; + if (isset($entity_compare)) { + if ($entity->get($field_name)->getValue() === $entity_compare->get($field_name)->getValue()) { + // Skip unmodified field. + continue; + } + if (!$entity_compare->get($field_name)->isEmpty()) { + // Add tag for the original field value. + /** @var \Drupal\Core\Field\EntityReferenceFieldItemList $field_items */ + foreach ($entity_compare->get($field_name)->getValue() as $value) { + $tags[] = $tag_prefix_field . $value[$key]; + } + } + } + if (!$entity->get($field_name)->isEmpty()) { + // Add tag for the new field value. + /** @var \Drupal\Core\Field\EntityReferenceFieldItemList $field_items */ + foreach ($entity->get($field_name)->getValue() as $value) { + $tags[] = $tag_prefix_field . $value[$key]; + } + } + } + } + + return $tags; + } + + /** + * Invalidates published entity field-based cache tags. + * + * Invalidates cache tags of the following format + * `entitytype_entitybundle_pub:field_name:value` during: + * 1. Insert published: all non-empty field values. + * 2. Delete published: all non-empty field values. + * 3. Update published: only the modified field values. + * 4. Update and publish: all non-empty field values. + * 5. Update and unpublish: all non-empty original field values. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Entity. + * + * @return bool + * True if successful invalidation. False otherwise. + */ + public function invalidatePublishedEntityFields(EntityInterface $entity) { + // Skip if entity type is not fieldable or not configured. + if (!$entity instanceof FieldableEntityInterface) { + return FALSE; + } + $entity_type = $entity->getEntityTypeId(); + if (!isset($this->settings['invalidate'][$entity_type]) || !is_iterable($this->settings['invalidate'][$entity_type])) { + return FALSE; + } + // Determine published status and assume the entity is published if it + // doesn't implement the interface. + $is_published = $entity instanceof EntityPublishedInterface ? $entity->isPublished() : TRUE; + if (isset($entity->original)) { + $was_published = $entity->original instanceof EntityPublishedInterface ? $entity->original->isPublished() : TRUE; + } + else { + $was_published = FALSE; + } + // Get the cache tags depending on operation and published status. + $tags = []; + if ($is_published && $was_published) { + // Update published: only the modified field values. + $tags = $this->getPublishedEntityFieldsCacheTags($entity, $entity->original); + } + elseif ($was_published) { + // Update and unpublish: all non-empty original field values. + $tags = $this->getPublishedEntityFieldsCacheTags($entity->original); + } + elseif ($is_published) { + // Insert or delete published, update and publish: all non-empty field + // values. + $entities[] = $entity; + $tags = $this->getPublishedEntityFieldsCacheTags($entity); + } + if (!empty($tags)) { + // Invalidate all the selected tags. + $this->cacheTagsInvalidator->invalidateTags($tags); + return TRUE; + } + return FALSE; + } + +} diff --git a/src/Service/CacheSanitizer.php b/src/Service/CacheSanitizer.php index 301bfcd..41f6e17 100644 --- a/src/Service/CacheSanitizer.php +++ b/src/Service/CacheSanitizer.php @@ -2,12 +2,8 @@ namespace Drupal\cache_tools\Service; -use Drupal\Core\Cache\CacheTagsInvalidatorInterface; -use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\FieldableEntityInterface; -use InvalidArgumentException; /** * Sanitize cache tags. @@ -15,14 +11,14 @@ use InvalidArgumentException; class CacheSanitizer { /** - * The cache tag invalidator. + * The cache tag handler. * - * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface + * @var \Drupal\cache_tools\Service\CacheInvalidator */ - protected $cacheTagInvalidator; + protected $cacheInvalidator; /** - * Cache settings. + * Cache tools settings. * * @var array */ @@ -31,13 +27,13 @@ class CacheSanitizer { /** * CacheSanitizer constructor. * - * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tag_invalidator - * The cache tag invalidator. + * @param \Drupal\cache_tools\Service\CacheInvalidator $cache_invalidator + * PublishedEntityCacheTag object. * @param array $settings - * The cache settings. + * The cache tools settings. */ - public function __construct(CacheTagsInvalidatorInterface $cache_tag_invalidator, array $settings) { - $this->cacheTagInvalidator = $cache_tag_invalidator; + public function __construct(CacheInvalidator $cache_invalidator, array $settings) { + $this->cacheInvalidator = $cache_invalidator; $this->settings = $settings; } @@ -143,29 +139,37 @@ class CacheSanitizer { } /** - * Get published cache tag in format `entitytype_pub`. + * Get published cache tag in format `entitytype_entitybundle_pub`. * - * @param \Drupal\Core\Entity\EntityTypeInterface $entityType + * @param \Drupal\Core\Entity\EntityInterface $entity * Entity. * * @return string - * Published cache tag. + * Published ache tag. + * + * @deprecated + * Please use Drupal\cache_tools\Service\PublishedEntityCacheTag::getPublishedEntityCacheTag() + * instead. */ - public function getPublishedEntityTypeCacheTag(EntityTypeInterface $entityType) { - return $entityType->id() . '_pub'; + public function getPublishedEntityCacheTag(EntityInterface $entity) { + return $this->cacheInvalidator->getPublishedEntityCacheTag($entity); } /** - * Get published cache tag in format `entitytype_entitybundle_pub`. + * Get published cache tag in format `entitytype_pub`. * - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\EntityTypeInterface $entityType * Entity. * * @return string - * Published ache tag. + * Published cache tag. + * + * @deprecated + * Please use Drupal\cache_tools\Service\PublishedEntityCacheTag::getPublishedEntityTypeCacheTag() + * instead. */ - public function getPublishedEntityCacheTag(EntityInterface $entity) { - return $entity->getEntityTypeId() . '_' . $entity->bundle() . '_pub'; + public function getPublishedEntityTypeCacheTag(EntityTypeInterface $entityType) { + return $this->cacheInvalidator->getPublishedEntityTypeCacheTag($entityType); } /** @@ -185,157 +189,13 @@ class CacheSanitizer { * * @return bool * True if successful invalidation. False otherwise. - */ - public function invalidatePublishedEntity(EntityInterface $entity) { - // Skip if entity type is not allowed by entity type and bundle. - if (!isset($this->settings['invalidate'][$entity->getEntityTypeId()])) { - return FALSE; - } - if (!in_array($entity->bundle(), $this->settings['invalidate'][$entity->getEntityTypeId()])) { - return FALSE; - } - $tags = [ - $this->getPublishedEntityTypeCacheTag($entity->getEntityType()), - $this->getPublishedEntityCacheTag($entity), - ]; - // If this is update we need to take a look at original entity as well. - $entities = [$entity]; - if ($entity->original) { - $entities[] = $entity->original; - } - // If entity is going to be unpublished (insert) - // or stays unpublished (update), skip. - foreach ($entities as $index => $entity) { - if (empty($entity->get('status')->value)) { - unset($entities[$index]); - } - } - if ($entities) { - $this->cacheTagInvalidator->invalidateTags($tags); - return TRUE; - } - return FALSE; - } - - /** - * Get field cache tags for configured fields having (modified) values. - * - * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * Entity. - * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity_compare - * (optional) An entity to compare field values with. When provided only - * non-equal field values will be considered. - * - * @return string[] - * The custom cache tags `entitytype_entitybundle_pub:field_name:value`. - */ - public function getPublishedEntityFieldsCacheTags(FieldableEntityInterface $entity, FieldableEntityInterface $entity_compare = NULL) { - $tags = []; - $entity_type = $entity->getEntityTypeId(); - // Get field-based tags configured for current entity bundle. - $bundle = $entity->bundle(); - $tag_prefix = $this->getPublishedEntityCacheTag($entity) . ':'; - foreach ($this->settings['invalidate'][$entity_type] as $cache_parameter) { - $parts = explode(':', $cache_parameter); - if (count($parts) != 2 || $parts[0] != $bundle) { - // This setting is not for the current bundle or not field-based. - continue; - } - $field_name = $parts[1]; - if ($entity->hasField($field_name)) { - // The name of the value property, e.g. 'value' or 'target_id'. - $key = $entity - ->getFieldDefinition($field_name) - ->getFieldStorageDefinition() - ->getMainPropertyName(); - if (is_null($key)) { - // The field has no main value property. - continue; - } - $tag_prefix_field = $tag_prefix . $field_name . ':'; - if (isset($entity_compare)) { - if ($entity->get($field_name)->getValue() === $entity_compare->get($field_name)->getValue()) { - // Skip unmodified field. - continue; - } - if (!$entity_compare->get($field_name)->isEmpty()) { - // Add tag for the original field value. - /** @var \Drupal\Core\Field\EntityReferenceFieldItemList $field_items */ - foreach ($entity_compare->get($field_name)->getValue() as $value) { - $tags[] = $tag_prefix_field . $value[$key]; - } - } - } - if (!$entity->get($field_name)->isEmpty()) { - // Add tag for the new field value. - /** @var \Drupal\Core\Field\EntityReferenceFieldItemList $field_items */ - foreach ($entity->get($field_name)->getValue() as $value) { - $tags[] = $tag_prefix_field . $value[$key]; - } - } - } - } - - return $tags; - } - - /** - * Invalidates published entity field-based cache tags. - * - * Invalidates cache tags of the following format - * `entitytype_entitybundle_pub:field_name:value` during: - * 1. Insert published: all non-empty field values. - * 2. Delete published: all non-empty field values. - * 3. Update published: only the modified field values. - * 4. Update and publish: all non-empty field values. - * 5. Update and unpublish: all non-empty original field values. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * Entity. * - * @return bool - * True if successful invalidation. False otherwise. + * @deprecated + * Please use Drupal\cache_tools\Service\PublishedEntityCacheTag::invalidatePublishedEntity() + * instead. */ - public function invalidatePublishedEntityFields(EntityInterface $entity) { - // Skip if entity type is not fieldable or not configured. - if (!$entity instanceof FieldableEntityInterface) { - return FALSE; - } - $entity_type = $entity->getEntityTypeId(); - if (!isset($this->settings['invalidate'][$entity_type]) || !is_iterable($this->settings['invalidate'][$entity_type])) { - return FALSE; - } - // Determine published status and assume the entity is published if it - // doesn't implement the interface. - $is_published = $entity instanceof EntityPublishedInterface ? $entity->isPublished() : TRUE; - if (isset($entity->original)) { - $was_published = $entity->original instanceof EntityPublishedInterface ? $entity->original->isPublished() : TRUE; - } - else { - $was_published = FALSE; - } - // Get the cache tags depending on operation and published status. - $tags = []; - if ($is_published && $was_published) { - // Update published: only the modified field values. - $tags = $this->getPublishedEntityFieldsCacheTags($entity, $entity->original); - } - elseif ($was_published) { - // Update and unpublish: all non-empty original field values. - $tags = $this->getPublishedEntityFieldsCacheTags($entity->original); - } - elseif ($is_published) { - // Insert or delete published, update and publish: all non-empty field - // values. - $entities[] = $entity; - $tags = $this->getPublishedEntityFieldsCacheTags($entity); - } - if (!empty($tags)) { - // Invalidate all the selected tags. - $this->cacheTagInvalidator->invalidateTags($tags); - return TRUE; - } - return FALSE; + public function invalidatePublishedEntity(EntityInterface $entity) { + return $this->cacheInvalidator->invalidatePublishedEntity($entity); } } -- GitLab