Skip to content
Snippets Groups Projects
Commit 0eb49149 authored by Antal Ludescher-Tyukodi's avatar Antal Ludescher-Tyukodi Committed by Wolfgang Ziegler
Browse files

Issue #3049056 by aludescher: Implement cache tags based on field values

parent 0f814e37
No related branches found
No related tags found
No related merge requests found
......@@ -14,7 +14,7 @@ It's using custom BlockViewBuilder to sanitize tags and contexts of blocks.
By default it's stripping several contexts (route, url, url.query_args)
and couple of tags (node_list, taxonomy_term_list). You can also decide
which additional tags and contexts you would like to strip. Everything
is configured in cache_tools.services.yml under paramter section.
is configured in cache_tools.services.yml under parameter section.
You can override parameters by introducing custom services.yml
### 2. Custom cache tag for views.
......@@ -22,7 +22,7 @@ You can override parameters by introducing custom services.yml
Module introduces also custom cache tag for the views. It's optimizing
to place more precise tags for the views. Namely, it places
`{entity}_{bundle}_pub` (like `node_article_pub` or `node_recipe_pub`)
instead of too genereal `node_list`. By doing so it will handle invalidation
instead of too general `node_list`. By doing so it will handle invalidation
of published nodes only respecting the bundle of the node. This tag is placed
out-of-the-box based on the view configuration. Module is auto extracting
the filter and arguments handler settings for particular view. If there is
......@@ -30,51 +30,35 @@ such handler it will auto identify entity and bundle and set\
`{entity}_{bundle}_pub` tag. Invalidation is handled automatically during
entity_insert and entity_update events.
### 2. Custom cache tag for views based on field.
### 2. Custom cache tags based on field values.
You can also use extended version of the cache tag behavior, namely you can
additionally set the field to be included in cache tag. Along the entity id.
You can achieve this by using cache tag for views based on field, where
you can select which field will be contained in that cache tags. As an example,
let's say you'll choose the field `field_author`, then instead of regular
`node_article_pub` you'll get `node_article_pub:field_author:123`. Where 123 is
the node id of currently previewed entity.
Field-based cache tags of the following format
`entitytype_entitybundle_pub:field_name:value` can be configured, e.g.:
```yml
invalidate:
node:
- article:field_author
```
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.
5. Update and unpublish: all non-empty original field values.
Use case for this:
A block or view listing nodes filtered by bundle and a given field value. We
want the cache to be invalidated only when nodes having that bundle and field
value are added, deleted or modified. E.g.
1. You have author/user entity
2. On the author/user page you'll see list of content of this author/user
3. List (or view) should be invalidated only when this author/user is assigned
to some new published content. In case of updating the published content,
both previous and current author/user page should be invalidated.
4. Placing tags like `node_article_pub:field_author:{author_id}` is excellent
way to deal with the problem. Everytime the new content is added which contain
this author, you invalidate `node_article_pub:field_author:123` so only page
way to deal with the problem. Every time the new content is added which contain
this author, `node_article_pub:field_author:123` is invalidated so only page
of author/user 123 will be invalidated.
**NOTE:** Module is only placing the tag in this case. The invalidation
behavior needs to be handled in custom module. Module cannot know how you
field is behaving and when you really want to invalidate such a page.
Example of custom hook on `entity_insert`:
```php
/**
* Implements hook_entity_insert().
*
* Invalidates by field author. Only if field is set and node is published.
*/
function module_entity_insert(EntityInterface $entity) {
// If new entity us upublished there is nothing to invalidate.
if (empty($entity->status->value)) {
return;
}
// If author is not filled there is nothing to invalidate.
if (empty($entity->field_author->entity)) {
return;
}
$tag = 'node_' . $entity->bundle() . '_pub:field_author:'
. $entity->field_author->entity->id();
/** @var \Drupal\Core\Cache\CacheTagsInvalidator $tags_invalidator */
$tags_invalidator = \Drupal::service('cache_tags.invalidator');
$tags_invalidator->invalidateTags([$tag]);
}
```
......@@ -19,23 +19,41 @@ function cache_tools_entity_type_alter(array &$entity_types) {
/**
* Implements hook_entity_insert().
*
* Invalidates `entitytype_entitybundle_pub` if entity is going to be published.
* Entity type needs to be allowed for invalidation.
* 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.
*/
function cache_tools_entity_insert(EntityInterface $entity) {
/** @var \Drupal\cache_tools\Service\CacheSanitizer $cacheSanitizer */
$cacheSanitizer = \Drupal::service('cache_tools.cache.sanitizer');
$cacheSanitizer->invalidatePublishedEntity($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);
}
/**
* Implements hook_entity_update().
*
* Invalidates `entitytype_entitybundle_pub` if entity is going from unpublished
* state to published. Entity type needs to be allowed for invalidation.
* state to published and `entitytype_entitybundle_pub:field_name:value` if
* a published state and/or a configured field value is changed. Entity type or
* a field needs to be allowed for invalidation.
*/
function cache_tools_entity_update(EntityInterface $entity) {
/** @var \Drupal\cache_tools\Service\CacheSanitizer $cacheSanitizer */
$cacheSanitizer = \Drupal::service('cache_tools.cache.sanitizer');
$cacheSanitizer->invalidatePublishedEntity($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);
}
/**
* Implements hook_entity_delete().
*
* Invalidates `entitytype_entitybundle_pub:field_name:value` if deleting
* published entity.
* Entity type or a field needs to be allowed for invalidation.
*/
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);
}
......@@ -3,8 +3,11 @@
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.
......@@ -140,29 +143,29 @@ class CacheSanitizer {
}
/**
* 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.
*/
public function getPublishedEntityCacheTag(EntityInterface $entity) {
return $entity->getEntityTypeId() . '_' . $entity->bundle() . '_pub';
public function getPublishedEntityTypeCacheTag(EntityTypeInterface $entityType) {
return $entityType->id() . '_pub';
}
/**
* 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.
*/
public function getPublishedEntityTypeCacheTag(EntityTypeInterface $entityType) {
return $entityType->id() . '_pub';
public function getPublishedEntityCacheTag(EntityInterface $entity) {
return $entity->getEntityTypeId() . '_' . $entity->bundle() . '_pub';
}
/**
......@@ -214,4 +217,125 @@ class CacheSanitizer {
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->cacheTagInvalidator->invalidateTags($tags);
return TRUE;
}
return FALSE;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment