Skip to content

Commit 9a653ca

Browse files
authored
8.1.5
- HSD8-1046 Invalidate entities whos date fields have recently passed (#16)
2 parents eef6988 + 9859f35 commit 9a653ca

File tree

8 files changed

+306
-1
lines changed

8 files changed

+306
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Stanford Fields
22

3+
8.x-1.5
4+
--------------------------------------------------------------------------------
5+
_Release Date: 2021-05-07_
6+
7+
- HSD8-1046 Invalidate entities whos date fields have recently passed (#16) (2e56375)
8+
39
8.1.4
410
--------------------------------------------------------------------------------
511
_Release Date: 2021-03-05_

src/Service/FieldCache.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Drupal\stanford_fields\Service;
4+
5+
use Drupal\Core\Cache\Cache;
6+
use Drupal\Core\Datetime\DrupalDateTime;
7+
use Drupal\Core\Entity\EntityFieldManagerInterface;
8+
use Drupal\Core\Entity\EntityTypeManagerInterface;
9+
use Drupal\Core\State\StateInterface;
10+
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
11+
12+
/**
13+
* Cache invalidator based on field values.
14+
*
15+
* @package Drupal\stanford_fields\Service
16+
*/
17+
class FieldCache implements FieldCacheInterface {
18+
19+
/**
20+
* Drupal core field manager service.
21+
*
22+
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
23+
*/
24+
protected $fieldManager;
25+
26+
/**
27+
* Drupal core entity manager service.
28+
*
29+
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
30+
*/
31+
protected $entityTypeManager;
32+
33+
/**
34+
* Drupal core state service.
35+
*
36+
* @var \Drupal\Core\State\StateInterface
37+
*/
38+
protected $state;
39+
40+
/**
41+
* FieldCron constructor.
42+
*
43+
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
44+
* Drupal core field manager service.
45+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
46+
* Drupal core entity manager service.
47+
* @param \Drupal\Core\State\StateInterface $state
48+
* Drupal core state service.
49+
*/
50+
public function __construct(EntityFieldManagerInterface $field_manager, EntityTypeManagerInterface $entity_type_manager, StateInterface $state) {
51+
$this->fieldManager = $field_manager;
52+
$this->entityTypeManager = $entity_type_manager;
53+
$this->state = $state;
54+
}
55+
56+
/**
57+
* {@inheritDoc}
58+
*/
59+
public function invalidateDateFieldsCache() {
60+
$date_field_types = ['datetime', 'daterange', 'smartdate'];
61+
$cache_tags = [];
62+
63+
// Loop through all fields to invalidate entities on date fields.
64+
foreach ($this->fieldManager->getFieldMap() as $entity_type => $fields) {
65+
foreach ($fields as $field_name => $field_info) {
66+
67+
// Only invalidate desired date fields.
68+
if (in_array($field_info['type'], $date_field_types)) {
69+
$tags = $this->getExpiredDateCacheTags($entity_type, $field_name, $field_info['bundles']);
70+
$cache_tags = array_merge($cache_tags, $tags);
71+
}
72+
}
73+
}
74+
75+
Cache::invalidateTags(array_unique($cache_tags));
76+
}
77+
78+
/**
79+
* Use an entity query to check for date fields that have recently passed.
80+
*
81+
* @param string $entity_type
82+
* Entity type machine name.
83+
* @param string $field_name
84+
* Field definition name.
85+
* @param array $bundles
86+
* Array of entity bundle names.
87+
*
88+
* @return int[]
89+
* Keyed array of entity ids.
90+
*
91+
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
92+
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
93+
*/
94+
protected function getExpiredDateCacheTags($entity_type, $field_name, array $bundles = []) {
95+
$entity_storage = $this->entityTypeManager->getStorage($entity_type);
96+
$bundle_key = $this->entityTypeManager->getDefinition($entity_type)
97+
->getKey('bundle');
98+
99+
$field_storage = $this->entityTypeManager->getStorage('field_storage_config');
100+
$now = new DrupalDateTime();
101+
$last_ran = DrupalDateTime::createFromTimestamp($this->state->get('stanford_fields.dates_cleared', 0));
102+
103+
/** @var \Drupal\field\FieldStorageConfigInterface $field_definition */
104+
$field_definition = $field_storage->load("$entity_type.$field_name");
105+
$field_date_format = $field_definition->getSetting('datetime_type') == 'date' ? DateTimeItemInterface::DATE_STORAGE_FORMAT : DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
106+
107+
// Smartdate fields are formatted differently.
108+
if ($field_definition->getType() == 'smartdate') {
109+
$field_date_format = 'U';
110+
}
111+
112+
// Query all entities for the given date field that is between the last ran
113+
// time and the current time.
114+
$query = $entity_storage->getQuery()
115+
->accessCheck(FALSE)
116+
->exists($field_name)
117+
->condition($field_name, $now->format($field_date_format), '<=')
118+
->condition($field_name, $last_ran->format($field_date_format), '>=');
119+
120+
// Some entity types don't have bundles, so don't add the condition if not
121+
// applicable.
122+
if ($bundle_key) {
123+
$query->condition($bundle_key, $bundles, 'IN');
124+
}
125+
$tags = [];
126+
127+
// If no entity ids were found, no tags should be invalidated.
128+
if ($entity_ids = $query->execute()) {
129+
$entities = $entity_storage->loadMultiple($entity_ids);
130+
foreach ($entities as $entity) {
131+
$tags = array_merge($tags, $entity->getCacheTagsToInvalidate());
132+
}
133+
}
134+
return $tags;
135+
}
136+
137+
}

src/Service/FieldCacheInterface.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Drupal\stanford_fields\Service;
4+
5+
/**
6+
* Interface FieldCacheInterface.
7+
*
8+
* @package Drupal\stanford_fields\Service
9+
*/
10+
interface FieldCacheInterface {
11+
12+
/**
13+
* Invalidate entities that have date field values that recently passed by.
14+
*/
15+
public function invalidateDateFieldsCache();
16+
17+
}

stanford_fields.info.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ type: module
33
description: 'Field types, widgets and formatters to enhance Drupal and Contrib.'
44
core_version_requirement: ^8 || ^9
55
package: Stanford
6-
version: 8.x-1.4
6+
version: 8.x-1.5
77
dependencies:
88
- drupal:field

stanford_fields.install

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* stanford_fields.install
6+
*/
7+
8+
/**
9+
* Implements hook_uninstall().
10+
*/
11+
function stanford_fields_uninstall() {
12+
\Drupal::state()->delete('stanford_fields.dates_cleared');
13+
}

stanford_fields.module

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ function stanford_fields_field_storage_add_validate(&$form, FormStateInterface $
3535
$form_state->setError($form['new_storage_wrapper']['field_name'], t('Field name is too long. Please keep this field name under @count characters', ['@count' => $allowed_length]));
3636
}
3737
}
38+
39+
/**
40+
* Implements hook_cron().
41+
*/
42+
function stanford_fields_cron() {
43+
\Drupal::service('stanford_fields.field_cache')->invalidateDateFieldsCache();
44+
\Drupal::state()->set('stanford_fields.dates_cleared', time() - 5);
45+
}

stanford_fields.services.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
services:
2+
stanford_fields.field_cache:
3+
class: 'Drupal\stanford_fields\Service\FieldCache'
4+
arguments: ['@entity_field.manager', '@entity_type.manager', '@state']
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
namespace Drupal\Tests\stanford_fields\Kernel\Service;
4+
5+
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
6+
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
7+
use Drupal\field\Entity\FieldConfig;
8+
use Drupal\field\Entity\FieldStorageConfig;
9+
use Drupal\KernelTests\KernelTestBase;
10+
use Drupal\node\Entity\Node;
11+
use Drupal\node\Entity\NodeType;
12+
13+
/**
14+
* Class FieldCacheTest
15+
*
16+
* @package Drupal\Tests\stanford_fields\Kernel\Service
17+
*/
18+
class FieldCacheTest extends KernelTestBase {
19+
20+
/**
21+
* {@inheritDoc}
22+
*/
23+
protected static $modules = [
24+
'system',
25+
'stanford_fields',
26+
'node',
27+
'user',
28+
'datetime',
29+
'field',
30+
];
31+
32+
/**
33+
* Array of cache tags scheduled for invalidation.
34+
*
35+
* @var array
36+
*/
37+
protected $invalidatedTags = [];
38+
39+
/**
40+
* Date field format to save the date as.
41+
*
42+
* @var string
43+
*/
44+
protected $dateFieldFormat = DateTimeItemInterface::DATE_STORAGE_FORMAT;
45+
46+
/**
47+
* {@inheritDoc}
48+
*/
49+
protected function setUp(): void {
50+
parent::setUp();
51+
$this->installEntitySchema('user');
52+
$this->installEntitySchema('node');
53+
$this->installSchema('node', ['node_access']);
54+
55+
$cache_invalidator = $this->createMock(CacheTagsInvalidatorInterface::class);
56+
$cache_invalidator->method('invalidateTags')
57+
->will($this->returnCallback([$this, 'invalidateTagsCallback']));
58+
59+
NodeType::create(['type' => 'page', 'name' => 'Page'])->save();
60+
// Create a comment field attached to a host 'entity_test' entity.
61+
FieldStorageConfig::create([
62+
'entity_type' => 'node',
63+
'type' => 'datetime',
64+
'field_name' => 'field_date',
65+
])->save();
66+
FieldConfig::create([
67+
'entity_type' => 'node',
68+
'bundle' => 'page',
69+
'field_name' => 'field_date',
70+
])->save();
71+
\Drupal::getContainer()->set('cache_tags.invalidator', $cache_invalidator);
72+
}
73+
74+
/**
75+
* Test that the field value will trigger an invalidation.
76+
*/
77+
public function testDateInvalidation() {
78+
\Drupal::service('stanford_fields.field_cache')
79+
->invalidateDateFieldsCache();
80+
$this->assertEmpty($this->invalidatedTags);
81+
82+
$node = Node::create([
83+
'type' => 'page',
84+
'title' => 'foo',
85+
'field_date' => date($this->dateFieldFormat, time() + 60 * 60 * 24 * 3),
86+
]);
87+
$node->save();
88+
89+
$this->invalidatedTags = [];
90+
\Drupal::service('stanford_fields.field_cache')
91+
->invalidateDateFieldsCache();
92+
$this->assertEmpty($this->invalidatedTags);
93+
94+
$node->set('field_date', date($this->dateFieldFormat, time() - 60 * 60 * 24 * 3))
95+
->save();
96+
$this->invalidatedTags = [];
97+
\Drupal::service('stanford_fields.field_cache')
98+
->invalidateDateFieldsCache();
99+
$this->assertTrue(in_array('node:' . $node->id(), $this->invalidatedTags));
100+
}
101+
102+
/**
103+
* Run the same tests as above, but with a date time storage.
104+
*/
105+
public function testDateTimeInvalidation() {
106+
$this->dateFieldFormat = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
107+
FieldStorageConfig::load('node.field_date')
108+
->setSetting('datetime_type', 'datetime')
109+
->save();
110+
$this->testDateInvalidation();
111+
}
112+
113+
/**
114+
* Cache invalidation callback.
115+
*/
116+
public function invalidateTagsCallback($tags) {
117+
$this->invalidatedTags = $tags;
118+
}
119+
120+
}

0 commit comments

Comments
 (0)