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