Added another front page space for Yaffs info. Added roave security for composer.
[yaffs-website] / web / core / lib / Drupal / Core / Entity / ContentEntityStorageBase.php
1 <?php
2
3 namespace Drupal\Core\Entity;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheBackendInterface;
7 use Drupal\Core\Field\FieldDefinitionInterface;
8 use Drupal\Core\Field\FieldStorageDefinitionInterface;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10
11 /**
12  * Base class for content entity storage handlers.
13  */
14 abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
15
16   /**
17    * The entity bundle key.
18    *
19    * @var string|bool
20    */
21   protected $bundleKey = FALSE;
22
23   /**
24    * The entity manager.
25    *
26    * @var \Drupal\Core\Entity\EntityManagerInterface
27    */
28   protected $entityManager;
29
30   /**
31    * Cache backend.
32    *
33    * @var \Drupal\Core\Cache\CacheBackendInterface
34    */
35   protected $cacheBackend;
36
37   /**
38    * Constructs a ContentEntityStorageBase object.
39    *
40    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
41    *   The entity type definition.
42    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
43    *   The entity manager.
44    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
45    *   The cache backend to be used.
46    */
47   public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
48     parent::__construct($entity_type);
49     $this->bundleKey = $this->entityType->getKey('bundle');
50     $this->entityManager = $entity_manager;
51     $this->cacheBackend = $cache;
52   }
53
54   /**
55    * {@inheritdoc}
56    */
57   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
58     return new static(
59       $entity_type,
60       $container->get('entity.manager'),
61       $container->get('cache.entity')
62     );
63   }
64
65   /**
66    * {@inheritdoc}
67    */
68   public function hasData() {
69     return (bool) $this->getQuery()
70       ->accessCheck(FALSE)
71       ->range(0, 1)
72       ->execute();
73   }
74
75   /**
76    * {@inheritdoc}
77    */
78   protected function doCreate(array $values) {
79     // We have to determine the bundle first.
80     $bundle = FALSE;
81     if ($this->bundleKey) {
82       if (!isset($values[$this->bundleKey])) {
83         throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
84       }
85       $bundle = $values[$this->bundleKey];
86     }
87     $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
88     $this->initFieldValues($entity, $values);
89     return $entity;
90   }
91
92   /**
93    * Initializes field values.
94    *
95    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
96    *   An entity object.
97    * @param array $values
98    *   (optional) An associative array of initial field values keyed by field
99    *   name. If none is provided default values will be applied.
100    * @param array $field_names
101    *   (optional) An associative array of field names to be initialized. If none
102    *   is provided all fields will be initialized.
103    */
104   protected function initFieldValues(ContentEntityInterface $entity, array $values = [], array $field_names = []) {
105     // Populate field values.
106     foreach ($entity as $name => $field) {
107       if (!$field_names || isset($field_names[$name])) {
108         if (isset($values[$name])) {
109           $entity->$name = $values[$name];
110         }
111         elseif (!array_key_exists($name, $values)) {
112           $entity->get($name)->applyDefaultValue();
113         }
114       }
115       unset($values[$name]);
116     }
117
118     // Set any passed values for non-defined fields also.
119     foreach ($values as $name => $value) {
120       $entity->$name = $value;
121     }
122
123     // Make sure modules can alter field initial values.
124     $this->invokeHook('field_values_init', $entity);
125   }
126
127   /**
128    * {@inheritdoc}
129    */
130   public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
131     $translation = $entity->getTranslation($langcode);
132     $definitions = array_filter($translation->getFieldDefinitions(), function(FieldDefinitionInterface $definition) { return $definition->isTranslatable(); });
133     $field_names = array_map(function(FieldDefinitionInterface $definition) { return $definition->getName(); }, $definitions);
134     $values[$this->langcodeKey] = $langcode;
135     $values[$this->getEntityType()->getKey('default_langcode')] = FALSE;
136     $this->initFieldValues($translation, $values, $field_names);
137     $this->invokeHook('translation_create', $translation);
138     return $translation;
139   }
140
141   /**
142    * {@inheritdoc}
143    */
144   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { }
145
146   /**
147    * {@inheritdoc}
148    */
149   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { }
150
151   /**
152    * {@inheritdoc}
153    */
154   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { }
155
156   /**
157    * {@inheritdoc}
158    */
159   public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) { }
160
161   /**
162    * {@inheritdoc}
163    */
164   public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) { }
165
166   /**
167    * {@inheritdoc}
168    */
169   public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { }
170
171   /**
172    * {@inheritdoc}
173    */
174   public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
175     $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
176
177     foreach ($items_by_entity as $items) {
178       $items->delete();
179       $this->purgeFieldItems($items->getEntity(), $field_definition);
180     }
181     return count($items_by_entity);
182   }
183
184   /**
185    * Reads values to be purged for a single field.
186    *
187    * This method is called during field data purge, on fields for which
188    * onFieldDefinitionDelete() has previously run.
189    *
190    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
191    *   The field definition.
192    * @param $batch_size
193    *   The maximum number of field data records to purge before returning.
194    *
195    * @return \Drupal\Core\Field\FieldItemListInterface[]
196    *   An array of field item lists, keyed by entity revision id.
197    */
198   abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
199
200   /**
201    * Removes field items from storage per entity during purge.
202    *
203    * @param ContentEntityInterface $entity
204    *   The entity revision, whose values are being purged.
205    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
206    *   The field whose values are bing purged.
207    */
208   abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
209
210   /**
211    * {@inheritdoc}
212    */
213   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { }
214
215   /**
216    * {@inheritdoc}
217    */
218   public function loadRevision($revision_id) {
219     $revision = $this->doLoadRevisionFieldItems($revision_id);
220
221     if ($revision) {
222       $entities = [$revision->id() => $revision];
223       $this->invokeStorageLoadHook($entities);
224       $this->postLoad($entities);
225     }
226
227     return $revision;
228   }
229
230   /**
231    * Actually loads revision field item values from the storage.
232    *
233    * @param int|string $revision_id
234    *   The revision identifier.
235    *
236    * @return \Drupal\Core\Entity\EntityInterface|null
237    *   The specified entity revision or NULL if not found.
238    */
239   abstract protected function doLoadRevisionFieldItems($revision_id);
240
241   /**
242    * {@inheritdoc}
243    */
244   protected function doSave($id, EntityInterface $entity) {
245     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
246
247     if ($entity->isNew()) {
248       // Ensure the entity is still seen as new after assigning it an id, while
249       // storing its data.
250       $entity->enforceIsNew();
251       if ($this->entityType->isRevisionable()) {
252         $entity->setNewRevision();
253       }
254       $return = SAVED_NEW;
255     }
256     else {
257       // @todo Consider returning a different value when saving a non-default
258       //   entity revision. See https://www.drupal.org/node/2509360.
259       $return = $entity->isDefaultRevision() ? SAVED_UPDATED : FALSE;
260     }
261
262     $this->populateAffectedRevisionTranslations($entity);
263     $this->doSaveFieldItems($entity);
264
265     return $return;
266   }
267
268   /**
269    * Writes entity field values to the storage.
270    *
271    * This method is responsible for allocating entity and revision identifiers
272    * and updating the entity object with their values.
273    *
274    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
275    *   The entity object.
276    * @param string[] $names
277    *   (optional) The name of the fields to be written to the storage. If an
278    *   empty value is passed all field values are saved.
279    */
280   abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
281
282   /**
283    * {@inheritdoc}
284    */
285   protected function doPreSave(EntityInterface $entity) {
286     /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
287
288     // Sync the changes made in the fields array to the internal values array.
289     $entity->updateOriginalValues();
290
291     if ($entity->getEntityType()->isRevisionable() && !$entity->isNew() && empty($entity->getLoadedRevisionId())) {
292       // Update the loaded revision id for rare special cases when no loaded
293       // revision is given when updating an existing entity. This for example
294       // happens when calling save() in hook_entity_insert().
295       $entity->updateLoadedRevisionId();
296     }
297
298     $id = parent::doPreSave($entity);
299
300     if (!$entity->isNew()) {
301       // If the ID changed then original can't be loaded, throw an exception
302       // in that case.
303       if (empty($entity->original) || $entity->id() != $entity->original->id()) {
304         throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity while changing the ID is not supported.");
305       }
306       // Do not allow changing the revision ID when resaving the current
307       // revision.
308       if (!$entity->isNewRevision() && $entity->getRevisionId() != $entity->getLoadedRevisionId()) {
309         throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity revision while changing the revision ID is not supported.");
310       }
311     }
312
313     return $id;
314   }
315
316   /**
317    * {@inheritdoc}
318    */
319   protected function doPostSave(EntityInterface $entity, $update) {
320     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
321
322     if ($update && $this->entityType->isTranslatable()) {
323       $this->invokeTranslationHooks($entity);
324     }
325
326     parent::doPostSave($entity, $update);
327
328     // The revision is stored, it should no longer be marked as new now.
329     if ($this->entityType->isRevisionable()) {
330       $entity->updateLoadedRevisionId();
331       $entity->setNewRevision(FALSE);
332     }
333   }
334
335   /**
336    * {@inheritdoc}
337    */
338   protected function doDelete($entities) {
339     /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
340     foreach ($entities as $entity) {
341       $this->invokeFieldMethod('delete', $entity);
342     }
343     $this->doDeleteFieldItems($entities);
344   }
345
346   /**
347    * Deletes entity field values from the storage.
348    *
349    * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
350    *   An array of entity objects to be deleted.
351    */
352   abstract protected function doDeleteFieldItems($entities);
353
354   /**
355    * {@inheritdoc}
356    */
357   public function deleteRevision($revision_id) {
358     /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
359     if ($revision = $this->loadRevision($revision_id)) {
360       // Prevent deletion if this is the default revision.
361       if ($revision->isDefaultRevision()) {
362         throw new EntityStorageException('Default revision can not be deleted');
363       }
364       $this->invokeFieldMethod('deleteRevision', $revision);
365       $this->doDeleteRevisionFieldItems($revision);
366       $this->invokeHook('revision_delete', $revision);
367     }
368   }
369
370   /**
371    * Deletes field values of an entity revision from the storage.
372    *
373    * @param \Drupal\Core\Entity\ContentEntityInterface $revision
374    *   An entity revision object to be deleted.
375    */
376   abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
377
378   /**
379    * Checks translation statuses and invoke the related hooks if needed.
380    *
381    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
382    *   The entity being saved.
383    */
384   protected function invokeTranslationHooks(ContentEntityInterface $entity) {
385     $translations = $entity->getTranslationLanguages(FALSE);
386     $original_translations = $entity->original->getTranslationLanguages(FALSE);
387     $all_translations = array_keys($translations + $original_translations);
388
389     // Notify modules of translation insertion/deletion.
390     foreach ($all_translations as $langcode) {
391       if (isset($translations[$langcode]) && !isset($original_translations[$langcode])) {
392         $this->invokeHook('translation_insert', $entity->getTranslation($langcode));
393       }
394       elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
395         $this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
396       }
397     }
398   }
399
400   /**
401    * Invokes hook_entity_storage_load().
402    *
403    * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
404    *   List of entities, keyed on the entity ID.
405    */
406   protected function invokeStorageLoadHook(array &$entities) {
407     if (!empty($entities)) {
408       // Call hook_entity_storage_load().
409       foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
410         $function = $module . '_entity_storage_load';
411         $function($entities, $this->entityTypeId);
412       }
413       // Call hook_TYPE_storage_load().
414       foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
415         $function = $module . '_' . $this->entityTypeId . '_storage_load';
416         $function($entities);
417       }
418     }
419   }
420
421   /**
422    * {@inheritdoc}
423    */
424   protected function invokeHook($hook, EntityInterface $entity) {
425     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
426
427     switch ($hook) {
428       case 'presave':
429         $this->invokeFieldMethod('preSave', $entity);
430         break;
431
432       case 'insert':
433         $this->invokeFieldPostSave($entity, FALSE);
434         break;
435
436       case 'update':
437         $this->invokeFieldPostSave($entity, TRUE);
438         break;
439     }
440
441     parent::invokeHook($hook, $entity);
442   }
443
444   /**
445    * Invokes a method on the Field objects within an entity.
446    *
447    * Any argument passed will be forwarded to the invoked method.
448    *
449    * @param string $method
450    *   The name of the method to be invoked.
451    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
452    *   The entity object.
453    *
454    * @return array
455    *   A multidimensional associative array of results, keyed by entity
456    *   translation language code and field name.
457    */
458   protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
459     $result = [];
460     $args = array_slice(func_get_args(), 2);
461     $langcodes = array_keys($entity->getTranslationLanguages());
462     // Ensure that the field method is invoked as first on the current entity
463     // translation and then on all other translations.
464     $current_entity_langcode = $entity->language()->getId();
465     if (reset($langcodes) != $current_entity_langcode) {
466       $langcodes = array_diff($langcodes, [$current_entity_langcode]);
467       array_unshift($langcodes, $current_entity_langcode);
468     }
469     foreach ($langcodes as $langcode) {
470       $translation = $entity->getTranslation($langcode);
471       // For non translatable fields, there is only one field object instance
472       // across all translations and it has as parent entity the entity in the
473       // default entity translation. Therefore field methods on non translatable
474       // fields should be invoked only on the default entity translation.
475       $fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields();
476       foreach ($fields as $name => $items) {
477         // call_user_func_array() is way slower than a direct call so we avoid
478         // using it if have no parameters.
479         $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}();
480       }
481     }
482
483     // We need to call the delete method for field items of removed
484     // translations.
485     if ($method == 'postSave' && !empty($entity->original)) {
486       $original_langcodes = array_keys($entity->original->getTranslationLanguages());
487       foreach (array_diff($original_langcodes, $langcodes) as $removed_langcode) {
488         $translation = $entity->original->getTranslation($removed_langcode);
489         $fields = $translation->getTranslatableFields();
490         foreach ($fields as $name => $items) {
491           $items->delete();
492         }
493       }
494     }
495
496     return $result;
497   }
498
499   /**
500    * Invokes the post save method on the Field objects within an entity.
501    *
502    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
503    *   The entity object.
504    * @param bool $update
505    *   Specifies whether the entity is being updated or created.
506    */
507   protected function invokeFieldPostSave(ContentEntityInterface $entity, $update) {
508     // For each entity translation this returns an array of resave flags keyed
509     // by field name, thus we merge them to obtain a list of fields to resave.
510     $resave = [];
511     foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $translation_results) {
512       $resave += array_filter($translation_results);
513     }
514     if ($resave) {
515       $this->doSaveFieldItems($entity, array_keys($resave));
516     }
517   }
518
519   /**
520    * Checks whether the field values changed compared to the original entity.
521    *
522    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
523    *   Field definition of field to compare for changes.
524    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
525    *   Entity to check for field changes.
526    * @param \Drupal\Core\Entity\ContentEntityInterface $original
527    *   Original entity to compare against.
528    *
529    * @return bool
530    *   True if the field value changed from the original entity.
531    */
532   protected function hasFieldValueChanged(FieldDefinitionInterface $field_definition, ContentEntityInterface $entity, ContentEntityInterface $original) {
533     $field_name = $field_definition->getName();
534     $langcodes = array_keys($entity->getTranslationLanguages());
535     if ($langcodes !== array_keys($original->getTranslationLanguages())) {
536       // If the list of langcodes has changed, we need to save.
537       return TRUE;
538     }
539     foreach ($langcodes as $langcode) {
540       $items = $entity->getTranslation($langcode)->get($field_name)->filterEmptyItems();
541       $original_items = $original->getTranslation($langcode)->get($field_name)->filterEmptyItems();
542       // If the field items are not equal, we need to save.
543       if (!$items->equals($original_items)) {
544         return TRUE;
545       }
546     }
547
548     return FALSE;
549   }
550
551   /**
552    * Populates the affected flag for all the revision translations.
553    *
554    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
555    *   An entity object being saved.
556    */
557   protected function populateAffectedRevisionTranslations(ContentEntityInterface $entity) {
558     if ($this->entityType->isTranslatable() && $this->entityType->isRevisionable()) {
559       $languages = $entity->getTranslationLanguages();
560       foreach ($languages as $langcode => $language) {
561         $translation = $entity->getTranslation($langcode);
562         // Avoid populating the value if it was already manually set.
563         $affected = $translation->isRevisionTranslationAffected();
564         if (!isset($affected) && $translation->hasTranslationChanges()) {
565           $translation->setRevisionTranslationAffected(TRUE);
566         }
567       }
568     }
569   }
570
571   /**
572    * Ensures integer entity IDs are valid.
573    *
574    * The identifier sanitization provided by this method has been introduced
575    * as Drupal used to rely on the database to facilitate this, which worked
576    * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
577    *
578    * @param array $ids
579    *   The entity IDs to verify.
580    *
581    * @return array
582    *   The sanitized list of entity IDs.
583    */
584   protected function cleanIds(array $ids) {
585     $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
586     $id_definition = $definitions[$this->entityType->getKey('id')];
587     if ($id_definition->getType() == 'integer') {
588       $ids = array_filter($ids, function ($id) {
589         return is_numeric($id) && $id == (int) $id;
590       });
591       $ids = array_map('intval', $ids);
592     }
593     return $ids;
594   }
595
596   /**
597    * Gets entities from the persistent cache backend.
598    *
599    * @param array|null &$ids
600    *   If not empty, return entities that match these IDs. IDs that were found
601    *   will be removed from the list.
602    *
603    * @return \Drupal\Core\Entity\ContentEntityInterface[]
604    *   Array of entities from the persistent cache.
605    */
606   protected function getFromPersistentCache(array &$ids = NULL) {
607     if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
608       return [];
609     }
610     $entities = [];
611     // Build the list of cache entries to retrieve.
612     $cid_map = [];
613     foreach ($ids as $id) {
614       $cid_map[$id] = $this->buildCacheId($id);
615     }
616     $cids = array_values($cid_map);
617     if ($cache = $this->cacheBackend->getMultiple($cids)) {
618       // Get the entities that were found in the cache.
619       foreach ($ids as $index => $id) {
620         $cid = $cid_map[$id];
621         if (isset($cache[$cid])) {
622           $entities[$id] = $cache[$cid]->data;
623           unset($ids[$index]);
624         }
625       }
626     }
627     return $entities;
628   }
629
630   /**
631    * Stores entities in the persistent cache backend.
632    *
633    * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
634    *   Entities to store in the cache.
635    */
636   protected function setPersistentCache($entities) {
637     if (!$this->entityType->isPersistentlyCacheable()) {
638       return;
639     }
640
641     $cache_tags = [
642       $this->entityTypeId . '_values',
643       'entity_field_info',
644     ];
645     foreach ($entities as $id => $entity) {
646       $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
647     }
648   }
649
650   /**
651    * {@inheritdoc}
652    */
653   public function loadUnchanged($id) {
654     $ids = [$id];
655
656     // The cache invalidation in the parent has the side effect that loading the
657     // same entity again during the save process (for example in
658     // hook_entity_presave()) will load the unchanged entity. Simulate this
659     // by explicitly removing the entity from the static cache.
660     parent::resetCache($ids);
661
662     // The default implementation in the parent class unsets the current cache
663     // and then reloads the entity. That is slow, especially if this is done
664     // repeatedly in the same request, e.g. when validating and then saving
665     // an entity. Optimize this for content entities by trying to load them
666     // directly from the persistent cache again, as in contrast to the static
667     // cache the persistent one will never be changed until the entity is saved.
668     $entities = $this->getFromPersistentCache($ids);
669
670     if (!$entities) {
671       $entities[$id] = $this->load($id);
672     }
673     else {
674       // As the entities are put into the persistent cache before the post load
675       // has been executed we have to execute it if we have retrieved the
676       // entity directly from the persistent cache.
677       $this->postLoad($entities);
678
679       if ($this->entityType->isStaticallyCacheable()) {
680         // As we've removed the entity from the static cache already we have to
681         // put the loaded unchanged entity there to simulate the behavior of the
682         // parent.
683         $this->setStaticCache($entities);
684       }
685     }
686
687     return $entities[$id];
688   }
689
690   /**
691    * {@inheritdoc}
692    */
693   public function resetCache(array $ids = NULL) {
694     if ($ids) {
695       $cids = [];
696       foreach ($ids as $id) {
697         unset($this->entities[$id]);
698         $cids[] = $this->buildCacheId($id);
699       }
700       if ($this->entityType->isPersistentlyCacheable()) {
701         $this->cacheBackend->deleteMultiple($cids);
702       }
703     }
704     else {
705       $this->entities = [];
706       if ($this->entityType->isPersistentlyCacheable()) {
707         Cache::invalidateTags([$this->entityTypeId . '_values']);
708       }
709     }
710   }
711
712   /**
713    * Builds the cache ID for the passed in entity ID.
714    *
715    * @param int $id
716    *   Entity ID for which the cache ID should be built.
717    *
718    * @return string
719    *   Cache ID that can be passed to the cache backend.
720    */
721   protected function buildCacheId($id) {
722     return "values:{$this->entityTypeId}:$id";
723   }
724
725 }