Updated Drupal to 8.6. This goes with the following updates because it's possible...
[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\Cache\MemoryCache\MemoryCacheInterface;
8 use Drupal\Core\Field\FieldDefinitionInterface;
9 use Drupal\Core\Field\FieldStorageDefinitionInterface;
10 use Drupal\Core\Language\LanguageInterface;
11 use Drupal\Core\TypedData\TranslationStatusInterface;
12 use Symfony\Component\DependencyInjection\ContainerInterface;
13
14 /**
15  * Base class for content entity storage handlers.
16  */
17 abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
18
19   /**
20    * The entity bundle key.
21    *
22    * @var string|bool
23    */
24   protected $bundleKey = FALSE;
25
26   /**
27    * The entity manager.
28    *
29    * @var \Drupal\Core\Entity\EntityManagerInterface
30    */
31   protected $entityManager;
32
33   /**
34    * Cache backend.
35    *
36    * @var \Drupal\Core\Cache\CacheBackendInterface
37    */
38   protected $cacheBackend;
39
40   /**
41    * Stores the latest revision IDs for entities.
42    *
43    * @var array
44    */
45   protected $latestRevisionIds = [];
46
47   /**
48    * Constructs a ContentEntityStorageBase object.
49    *
50    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
51    *   The entity type definition.
52    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
53    *   The entity manager.
54    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
55    *   The cache backend to be used.
56    * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
57    *   The memory cache backend.
58    */
59   public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, MemoryCacheInterface $memory_cache = NULL) {
60     parent::__construct($entity_type, $memory_cache);
61     $this->bundleKey = $this->entityType->getKey('bundle');
62     $this->entityManager = $entity_manager;
63     $this->cacheBackend = $cache;
64   }
65
66   /**
67    * {@inheritdoc}
68    */
69   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
70     return new static(
71       $entity_type,
72       $container->get('entity.manager'),
73       $container->get('cache.entity'),
74       $container->get('entity.memory_cache')
75     );
76   }
77
78   /**
79    * {@inheritdoc}
80    */
81   protected function doCreate(array $values) {
82     // We have to determine the bundle first.
83     $bundle = FALSE;
84     if ($this->bundleKey) {
85       if (!isset($values[$this->bundleKey])) {
86         throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
87       }
88       $bundle = $values[$this->bundleKey];
89     }
90     $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
91     $this->initFieldValues($entity, $values);
92     return $entity;
93   }
94
95   /**
96    * {@inheritdoc}
97    */
98   public function createWithSampleValues($bundle = FALSE, array $values = []) {
99     // ID and revision should never have sample values generated for them.
100     $forbidden_keys = [
101       $this->entityType->getKey('id'),
102     ];
103     if ($revision_key = $this->entityType->getKey('revision')) {
104       $forbidden_keys[] = $revision_key;
105     }
106     if ($bundle_key = $this->entityType->getKey('bundle')) {
107       if (!$bundle) {
108         throw new EntityStorageException("No entity bundle was specified");
109       }
110       if (!array_key_exists($bundle, $this->entityManager->getBundleInfo($this->entityTypeId))) {
111         throw new EntityStorageException(sprintf("Missing entity bundle. The \"%s\" bundle does not exist", $bundle));
112       }
113       $values[$bundle_key] = $bundle;
114       // Bundle is already set
115       $forbidden_keys[] = $bundle_key;
116     }
117     // Forbid sample generation on any keys whose values were submitted.
118     $forbidden_keys = array_merge($forbidden_keys, array_keys($values));
119     /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
120     $entity = $this->create($values);
121     foreach ($entity as $field_name => $value) {
122       if (!in_array($field_name, $forbidden_keys, TRUE)) {
123         $entity->get($field_name)->generateSampleItems();
124       }
125     }
126     return $entity;
127   }
128
129   /**
130    * Initializes field values.
131    *
132    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
133    *   An entity object.
134    * @param array $values
135    *   (optional) An associative array of initial field values keyed by field
136    *   name. If none is provided default values will be applied.
137    * @param array $field_names
138    *   (optional) An associative array of field names to be initialized. If none
139    *   is provided all fields will be initialized.
140    */
141   protected function initFieldValues(ContentEntityInterface $entity, array $values = [], array $field_names = []) {
142     // Populate field values.
143     foreach ($entity as $name => $field) {
144       if (!$field_names || isset($field_names[$name])) {
145         if (isset($values[$name])) {
146           $entity->$name = $values[$name];
147         }
148         elseif (!array_key_exists($name, $values)) {
149           $entity->get($name)->applyDefaultValue();
150         }
151       }
152       unset($values[$name]);
153     }
154
155     // Set any passed values for non-defined fields also.
156     foreach ($values as $name => $value) {
157       $entity->$name = $value;
158     }
159
160     // Make sure modules can alter field initial values.
161     $this->invokeHook('field_values_init', $entity);
162   }
163
164   /**
165    * Checks whether any entity revision is translated.
166    *
167    * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
168    *   The entity object to be checked.
169    *
170    * @return bool
171    *   TRUE if the entity has at least one translation in any revision, FALSE
172    *   otherwise.
173    *
174    * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
175    * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyStoredRevisionTranslated()
176    */
177   protected function isAnyRevisionTranslated(TranslatableInterface $entity) {
178     return $entity->getTranslationLanguages(FALSE) || $this->isAnyStoredRevisionTranslated($entity);
179   }
180
181   /**
182    * Checks whether any stored entity revision is translated.
183    *
184    * A revisionable entity can have translations in a pending revision, hence
185    * the default revision may appear as not translated. This determines whether
186    * the entity has any translation in the storage and thus should be considered
187    * as multilingual.
188    *
189    * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
190    *   The entity object to be checked.
191    *
192    * @return bool
193    *   TRUE if the entity has at least one translation in any revision, FALSE
194    *   otherwise.
195    *
196    * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
197    * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyRevisionTranslated()
198    */
199   protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) {
200     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
201     if ($entity->isNew()) {
202       return FALSE;
203     }
204
205     if ($entity instanceof TranslationStatusInterface) {
206       foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
207         if ($entity->getTranslationStatus($langcode) === TranslationStatusInterface::TRANSLATION_EXISTING) {
208           return TRUE;
209         }
210       }
211     }
212
213     $query = $this->getQuery()
214       ->condition($this->entityType->getKey('id'), $entity->id())
215       ->condition($this->entityType->getKey('default_langcode'), 0)
216       ->accessCheck(FALSE)
217       ->range(0, 1);
218
219     if ($entity->getEntityType()->isRevisionable()) {
220       $query->allRevisions();
221     }
222
223     $result = $query->execute();
224     return !empty($result);
225   }
226
227   /**
228    * {@inheritdoc}
229    */
230   public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
231     $translation = $entity->getTranslation($langcode);
232     $definitions = array_filter($translation->getFieldDefinitions(), function (FieldDefinitionInterface $definition) {
233       return $definition->isTranslatable();
234     });
235     $field_names = array_map(function (FieldDefinitionInterface $definition) {
236       return $definition->getName();
237     }, $definitions);
238     $values[$this->langcodeKey] = $langcode;
239     $values[$this->getEntityType()->getKey('default_langcode')] = FALSE;
240     $this->initFieldValues($translation, $values, $field_names);
241     $this->invokeHook('translation_create', $translation);
242     return $translation;
243   }
244
245   /**
246    * {@inheritdoc}
247    */
248   public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
249     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
250     $new_revision = clone $entity;
251
252     $original_keep_untranslatable_fields = $keep_untranslatable_fields;
253
254     // For translatable entities, create a merged revision of the active
255     // translation and the other translations in the default revision. This
256     // permits the creation of pending revisions that can always be saved as the
257     // new default revision without reverting changes in other languages.
258     if (!$entity->isNew() && !$entity->isDefaultRevision() && $entity->isTranslatable() && $this->isAnyRevisionTranslated($entity)) {
259       $active_langcode = $entity->language()->getId();
260       $skipped_field_names = array_flip($this->getRevisionTranslationMergeSkippedFieldNames());
261
262       // By default we copy untranslatable field values from the default
263       // revision, unless they are configured to affect only the default
264       // translation. This way we can ensure we always have only one affected
265       // translation in pending revisions. This constraint is enforced by
266       // EntityUntranslatableFieldsConstraintValidator.
267       if (!isset($keep_untranslatable_fields)) {
268         $keep_untranslatable_fields = $entity->isDefaultTranslation() && $entity->isDefaultTranslationAffectedOnly();
269       }
270
271       /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
272       $default_revision = $this->load($entity->id());
273       $translation_languages = $default_revision->getTranslationLanguages();
274       foreach ($translation_languages as $langcode => $language) {
275         if ($langcode == $active_langcode) {
276           continue;
277         }
278
279         $default_revision_translation = $default_revision->getTranslation($langcode);
280         $new_revision_translation = $new_revision->hasTranslation($langcode) ?
281           $new_revision->getTranslation($langcode) : $new_revision->addTranslation($langcode);
282
283         /** @var \Drupal\Core\Field\FieldItemListInterface[] $sync_items */
284         $sync_items = array_diff_key(
285           $keep_untranslatable_fields ? $default_revision_translation->getTranslatableFields() : $default_revision_translation->getFields(),
286           $skipped_field_names
287         );
288         foreach ($sync_items as $field_name => $items) {
289           $new_revision_translation->set($field_name, $items->getValue());
290         }
291
292         // Make sure the "revision_translation_affected" flag is recalculated.
293         $new_revision_translation->setRevisionTranslationAffected(NULL);
294
295         // No need to copy untranslatable field values more than once.
296         $keep_untranslatable_fields = TRUE;
297       }
298
299       // Make sure we do not inadvertently recreate removed translations.
300       foreach (array_diff_key($new_revision->getTranslationLanguages(), $translation_languages) as $langcode => $language) {
301         // Allow a new revision to be created for the active language.
302         if ($langcode !== $active_langcode) {
303           $new_revision->removeTranslation($langcode);
304         }
305       }
306
307       // The "original" property is used in various places to detect changes in
308       // field values with respect to the stored ones. If the property is not
309       // defined, the stored version is loaded explicitly. Since the merged
310       // revision generated here is not stored anywhere, we need to populate the
311       // "original" property manually, so that changes can be properly detected.
312       $new_revision->original = clone $new_revision;
313     }
314
315     // Eventually mark the new revision as such.
316     $new_revision->setNewRevision();
317     $new_revision->isDefaultRevision($default);
318
319     // Actually make sure the current translation is marked as affected, even if
320     // there are no explicit changes, to be sure this revision can be related
321     // to the correct translation.
322     $new_revision->setRevisionTranslationAffected(TRUE);
323
324     // Notify modules about the new revision.
325     $arguments = [$new_revision, $entity, $original_keep_untranslatable_fields];
326     $this->moduleHandler()->invokeAll($this->entityTypeId . '_revision_create', $arguments);
327     $this->moduleHandler()->invokeAll('entity_revision_create', $arguments);
328
329     return $new_revision;
330   }
331
332   /**
333    * Returns an array of field names to skip when merging revision translations.
334    *
335    * @return array
336    *   An array of field names.
337    */
338   protected function getRevisionTranslationMergeSkippedFieldNames() {
339     /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
340     $entity_type = $this->getEntityType();
341
342     // A list of known revision metadata fields which should be skipped from
343     // the comparision.
344     $field_names = [
345       $entity_type->getKey('revision'),
346       $entity_type->getKey('revision_translation_affected'),
347     ];
348     $field_names = array_merge($field_names, array_values($entity_type->getRevisionMetadataKeys()));
349
350     return $field_names;
351   }
352
353   /**
354    * {@inheritdoc}
355    */
356   public function getLatestRevisionId($entity_id) {
357     if (!$this->entityType->isRevisionable()) {
358       return NULL;
359     }
360
361     if (!isset($this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT])) {
362       $result = $this->getQuery()
363         ->latestRevision()
364         ->condition($this->entityType->getKey('id'), $entity_id)
365         ->accessCheck(FALSE)
366         ->execute();
367
368       $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT] = key($result);
369     }
370
371     return $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT];
372   }
373
374   /**
375    * {@inheritdoc}
376    */
377   public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
378     if (!$this->entityType->isRevisionable()) {
379       return NULL;
380     }
381
382     if (!$this->entityType->isTranslatable()) {
383       return $this->getLatestRevisionId($entity_id);
384     }
385
386     if (!isset($this->latestRevisionIds[$entity_id][$langcode])) {
387       $result = $this->getQuery()
388         ->allRevisions()
389         ->condition($this->entityType->getKey('id'), $entity_id)
390         ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
391         ->range(0, 1)
392         ->sort($this->entityType->getKey('revision'), 'DESC')
393         ->accessCheck(FALSE)
394         ->execute();
395
396       $this->latestRevisionIds[$entity_id][$langcode] = key($result);
397     }
398     return $this->latestRevisionIds[$entity_id][$langcode];
399   }
400
401   /**
402    * {@inheritdoc}
403    */
404   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {}
405
406   /**
407    * {@inheritdoc}
408    */
409   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {}
410
411   /**
412    * {@inheritdoc}
413    */
414   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {}
415
416   /**
417    * {@inheritdoc}
418    */
419   public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) {}
420
421   /**
422    * {@inheritdoc}
423    */
424   public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) {}
425
426   /**
427    * {@inheritdoc}
428    */
429   public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {}
430
431   /**
432    * {@inheritdoc}
433    */
434   public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
435     $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
436
437     foreach ($items_by_entity as $items) {
438       $items->delete();
439       $this->purgeFieldItems($items->getEntity(), $field_definition);
440     }
441     return count($items_by_entity);
442   }
443
444   /**
445    * Reads values to be purged for a single field.
446    *
447    * This method is called during field data purge, on fields for which
448    * onFieldDefinitionDelete() has previously run.
449    *
450    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
451    *   The field definition.
452    * @param $batch_size
453    *   The maximum number of field data records to purge before returning.
454    *
455    * @return \Drupal\Core\Field\FieldItemListInterface[]
456    *   An array of field item lists, keyed by entity revision id.
457    */
458   abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
459
460   /**
461    * Removes field items from storage per entity during purge.
462    *
463    * @param ContentEntityInterface $entity
464    *   The entity revision, whose values are being purged.
465    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
466    *   The field whose values are bing purged.
467    */
468   abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
469
470   /**
471    * {@inheritdoc}
472    */
473   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {}
474
475   /**
476    * {@inheritdoc}
477    */
478   public function loadRevision($revision_id) {
479     $revisions = $this->loadMultipleRevisions([$revision_id]);
480
481     return isset($revisions[$revision_id]) ? $revisions[$revision_id] : NULL;
482   }
483
484   /**
485    * {@inheritdoc}
486    */
487   public function loadMultipleRevisions(array $revision_ids) {
488     $revisions = $this->doLoadMultipleRevisionsFieldItems($revision_ids);
489
490     // The hooks are executed with an array of entities keyed by the entity ID.
491     // As we could load multiple revisions for the same entity ID at once we
492     // have to build groups of entities where the same entity ID is present only
493     // once.
494     $entity_groups = [];
495     $entity_group_mapping = [];
496     foreach ($revisions as $revision) {
497       $entity_id = $revision->id();
498       $entity_group_key = isset($entity_group_mapping[$entity_id]) ? $entity_group_mapping[$entity_id] + 1 : 0;
499       $entity_group_mapping[$entity_id] = $entity_group_key;
500       $entity_groups[$entity_group_key][$entity_id] = $revision;
501     }
502
503     // Invoke the entity hooks for each group.
504     foreach ($entity_groups as $entities) {
505       $this->invokeStorageLoadHook($entities);
506       $this->postLoad($entities);
507     }
508
509     return $revisions;
510   }
511
512   /**
513    * Actually loads revision field item values from the storage.
514    *
515    * @param int|string $revision_id
516    *   The revision identifier.
517    *
518    * @return \Drupal\Core\Entity\EntityInterface|null
519    *   The specified entity revision or NULL if not found.
520    *
521    * @deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0.
522    *   \Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()
523    *   should be implemented instead.
524    *
525    * @see https://www.drupal.org/node/2924915
526    */
527   abstract protected function doLoadRevisionFieldItems($revision_id);
528
529   /**
530    * Actually loads revision field item values from the storage.
531    *
532    * @param array $revision_ids
533    *   An array of revision identifiers.
534    *
535    * @return \Drupal\Core\Entity\EntityInterface[]
536    *   The specified entity revisions or an empty array if none are found.
537    */
538   protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
539     $revisions = [];
540     foreach ($revision_ids as $revision_id) {
541       $revisions[] = $this->doLoadRevisionFieldItems($revision_id);
542     }
543
544     return $revisions;
545   }
546
547   /**
548    * {@inheritdoc}
549    */
550   protected function doSave($id, EntityInterface $entity) {
551     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
552
553     if ($entity->isNew()) {
554       // Ensure the entity is still seen as new after assigning it an id, while
555       // storing its data.
556       $entity->enforceIsNew();
557       if ($this->entityType->isRevisionable()) {
558         $entity->setNewRevision();
559       }
560       $return = SAVED_NEW;
561     }
562     else {
563       // @todo Consider returning a different value when saving a non-default
564       //   entity revision. See https://www.drupal.org/node/2509360.
565       $return = $entity->isDefaultRevision() ? SAVED_UPDATED : FALSE;
566     }
567
568     $this->populateAffectedRevisionTranslations($entity);
569
570     // Populate the "revision_default" flag. We skip this when we are resaving
571     // the revision because this is only allowed for default revisions, and
572     // these cannot be made non-default.
573     if ($this->entityType->isRevisionable() && $entity->isNewRevision()) {
574       $revision_default_key = $this->entityType->getRevisionMetadataKey('revision_default');
575       $entity->set($revision_default_key, $entity->isDefaultRevision());
576     }
577
578     $this->doSaveFieldItems($entity);
579
580     return $return;
581   }
582
583   /**
584    * Writes entity field values to the storage.
585    *
586    * This method is responsible for allocating entity and revision identifiers
587    * and updating the entity object with their values.
588    *
589    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
590    *   The entity object.
591    * @param string[] $names
592    *   (optional) The name of the fields to be written to the storage. If an
593    *   empty value is passed all field values are saved.
594    */
595   abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
596
597   /**
598    * {@inheritdoc}
599    */
600   protected function doPreSave(EntityInterface $entity) {
601     /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
602
603     // Sync the changes made in the fields array to the internal values array.
604     $entity->updateOriginalValues();
605
606     if ($entity->getEntityType()->isRevisionable() && !$entity->isNew() && empty($entity->getLoadedRevisionId())) {
607       // Update the loaded revision id for rare special cases when no loaded
608       // revision is given when updating an existing entity. This for example
609       // happens when calling save() in hook_entity_insert().
610       $entity->updateLoadedRevisionId();
611     }
612
613     $id = parent::doPreSave($entity);
614
615     if (!$entity->isNew()) {
616       // If the ID changed then original can't be loaded, throw an exception
617       // in that case.
618       if (empty($entity->original) || $entity->id() != $entity->original->id()) {
619         throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity while changing the ID is not supported.");
620       }
621       // Do not allow changing the revision ID when resaving the current
622       // revision.
623       if (!$entity->isNewRevision() && $entity->getRevisionId() != $entity->getLoadedRevisionId()) {
624         throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity revision while changing the revision ID is not supported.");
625       }
626     }
627
628     return $id;
629   }
630
631   /**
632    * {@inheritdoc}
633    */
634   protected function doPostSave(EntityInterface $entity, $update) {
635     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
636
637     if ($update && $this->entityType->isTranslatable()) {
638       $this->invokeTranslationHooks($entity);
639     }
640
641     parent::doPostSave($entity, $update);
642
643     // The revision is stored, it should no longer be marked as new now.
644     if ($this->entityType->isRevisionable()) {
645       $entity->updateLoadedRevisionId();
646       $entity->setNewRevision(FALSE);
647     }
648   }
649
650   /**
651    * {@inheritdoc}
652    */
653   protected function doDelete($entities) {
654     /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
655     foreach ($entities as $entity) {
656       $this->invokeFieldMethod('delete', $entity);
657     }
658     $this->doDeleteFieldItems($entities);
659   }
660
661   /**
662    * Deletes entity field values from the storage.
663    *
664    * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
665    *   An array of entity objects to be deleted.
666    */
667   abstract protected function doDeleteFieldItems($entities);
668
669   /**
670    * {@inheritdoc}
671    */
672   public function deleteRevision($revision_id) {
673     /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
674     if ($revision = $this->loadRevision($revision_id)) {
675       // Prevent deletion if this is the default revision.
676       if ($revision->isDefaultRevision()) {
677         throw new EntityStorageException('Default revision can not be deleted');
678       }
679       $this->invokeFieldMethod('deleteRevision', $revision);
680       $this->doDeleteRevisionFieldItems($revision);
681       $this->invokeHook('revision_delete', $revision);
682     }
683   }
684
685   /**
686    * Deletes field values of an entity revision from the storage.
687    *
688    * @param \Drupal\Core\Entity\ContentEntityInterface $revision
689    *   An entity revision object to be deleted.
690    */
691   abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
692
693   /**
694    * Checks translation statuses and invoke the related hooks if needed.
695    *
696    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
697    *   The entity being saved.
698    */
699   protected function invokeTranslationHooks(ContentEntityInterface $entity) {
700     $translations = $entity->getTranslationLanguages(FALSE);
701     $original_translations = $entity->original->getTranslationLanguages(FALSE);
702     $all_translations = array_keys($translations + $original_translations);
703
704     // Notify modules of translation insertion/deletion.
705     foreach ($all_translations as $langcode) {
706       if (isset($translations[$langcode]) && !isset($original_translations[$langcode])) {
707         $this->invokeHook('translation_insert', $entity->getTranslation($langcode));
708       }
709       elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
710         $this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
711       }
712     }
713   }
714
715   /**
716    * Invokes hook_entity_storage_load().
717    *
718    * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
719    *   List of entities, keyed on the entity ID.
720    */
721   protected function invokeStorageLoadHook(array &$entities) {
722     if (!empty($entities)) {
723       // Call hook_entity_storage_load().
724       foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
725         $function = $module . '_entity_storage_load';
726         $function($entities, $this->entityTypeId);
727       }
728       // Call hook_TYPE_storage_load().
729       foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
730         $function = $module . '_' . $this->entityTypeId . '_storage_load';
731         $function($entities);
732       }
733     }
734   }
735
736   /**
737    * {@inheritdoc}
738    */
739   protected function invokeHook($hook, EntityInterface $entity) {
740     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
741
742     switch ($hook) {
743       case 'presave':
744         $this->invokeFieldMethod('preSave', $entity);
745         break;
746
747       case 'insert':
748         $this->invokeFieldPostSave($entity, FALSE);
749         break;
750
751       case 'update':
752         $this->invokeFieldPostSave($entity, TRUE);
753         break;
754     }
755
756     parent::invokeHook($hook, $entity);
757   }
758
759   /**
760    * Invokes a method on the Field objects within an entity.
761    *
762    * Any argument passed will be forwarded to the invoked method.
763    *
764    * @param string $method
765    *   The name of the method to be invoked.
766    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
767    *   The entity object.
768    *
769    * @return array
770    *   A multidimensional associative array of results, keyed by entity
771    *   translation language code and field name.
772    */
773   protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
774     $result = [];
775     $args = array_slice(func_get_args(), 2);
776     $langcodes = array_keys($entity->getTranslationLanguages());
777     // Ensure that the field method is invoked as first on the current entity
778     // translation and then on all other translations.
779     $current_entity_langcode = $entity->language()->getId();
780     if (reset($langcodes) != $current_entity_langcode) {
781       $langcodes = array_diff($langcodes, [$current_entity_langcode]);
782       array_unshift($langcodes, $current_entity_langcode);
783     }
784     foreach ($langcodes as $langcode) {
785       $translation = $entity->getTranslation($langcode);
786       // For non translatable fields, there is only one field object instance
787       // across all translations and it has as parent entity the entity in the
788       // default entity translation. Therefore field methods on non translatable
789       // fields should be invoked only on the default entity translation.
790       $fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields();
791       foreach ($fields as $name => $items) {
792         // call_user_func_array() is way slower than a direct call so we avoid
793         // using it if have no parameters.
794         $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}();
795       }
796     }
797
798     // We need to call the delete method for field items of removed
799     // translations.
800     if ($method == 'postSave' && !empty($entity->original)) {
801       $original_langcodes = array_keys($entity->original->getTranslationLanguages());
802       foreach (array_diff($original_langcodes, $langcodes) as $removed_langcode) {
803         $translation = $entity->original->getTranslation($removed_langcode);
804         $fields = $translation->getTranslatableFields();
805         foreach ($fields as $name => $items) {
806           $items->delete();
807         }
808       }
809     }
810
811     return $result;
812   }
813
814   /**
815    * Invokes the post save method on the Field objects within an entity.
816    *
817    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
818    *   The entity object.
819    * @param bool $update
820    *   Specifies whether the entity is being updated or created.
821    */
822   protected function invokeFieldPostSave(ContentEntityInterface $entity, $update) {
823     // For each entity translation this returns an array of resave flags keyed
824     // by field name, thus we merge them to obtain a list of fields to resave.
825     $resave = [];
826     foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $translation_results) {
827       $resave += array_filter($translation_results);
828     }
829     if ($resave) {
830       $this->doSaveFieldItems($entity, array_keys($resave));
831     }
832   }
833
834   /**
835    * Checks whether the field values changed compared to the original entity.
836    *
837    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
838    *   Field definition of field to compare for changes.
839    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
840    *   Entity to check for field changes.
841    * @param \Drupal\Core\Entity\ContentEntityInterface $original
842    *   Original entity to compare against.
843    *
844    * @return bool
845    *   True if the field value changed from the original entity.
846    */
847   protected function hasFieldValueChanged(FieldDefinitionInterface $field_definition, ContentEntityInterface $entity, ContentEntityInterface $original) {
848     $field_name = $field_definition->getName();
849     $langcodes = array_keys($entity->getTranslationLanguages());
850     if ($langcodes !== array_keys($original->getTranslationLanguages())) {
851       // If the list of langcodes has changed, we need to save.
852       return TRUE;
853     }
854     foreach ($langcodes as $langcode) {
855       $items = $entity->getTranslation($langcode)->get($field_name)->filterEmptyItems();
856       $original_items = $original->getTranslation($langcode)->get($field_name)->filterEmptyItems();
857       // If the field items are not equal, we need to save.
858       if (!$items->equals($original_items)) {
859         return TRUE;
860       }
861     }
862
863     return FALSE;
864   }
865
866   /**
867    * Populates the affected flag for all the revision translations.
868    *
869    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
870    *   An entity object being saved.
871    */
872   protected function populateAffectedRevisionTranslations(ContentEntityInterface $entity) {
873     if ($this->entityType->isTranslatable() && $this->entityType->isRevisionable()) {
874       $languages = $entity->getTranslationLanguages();
875       foreach ($languages as $langcode => $language) {
876         $translation = $entity->getTranslation($langcode);
877         $current_affected = $translation->isRevisionTranslationAffected();
878         if (!isset($current_affected) || ($entity->isNewRevision() && !$translation->isRevisionTranslationAffectedEnforced())) {
879           // When setting the revision translation affected flag we have to
880           // explicitly set it to not be enforced. By default it will be
881           // enforced automatically when being set, which allows us to determine
882           // if the flag has been already set outside the storage in which case
883           // we should not recompute it.
884           // @see \Drupal\Core\Entity\ContentEntityBase::setRevisionTranslationAffected().
885           $new_affected = $translation->hasTranslationChanges() ? TRUE : NULL;
886           $translation->setRevisionTranslationAffected($new_affected);
887           $translation->setRevisionTranslationAffectedEnforced(FALSE);
888         }
889       }
890     }
891   }
892
893   /**
894    * Ensures integer entity key values are valid.
895    *
896    * The identifier sanitization provided by this method has been introduced
897    * as Drupal used to rely on the database to facilitate this, which worked
898    * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
899    *
900    * @param array $ids
901    *   The entity key values to verify.
902    * @param string $entity_key
903    *   (optional) The entity key to sanitise values for. Defaults to 'id'.
904    *
905    * @return array
906    *   The sanitized list of entity key values.
907    */
908   protected function cleanIds(array $ids, $entity_key = 'id') {
909     $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
910     $field_name = $this->entityType->getKey($entity_key);
911     if ($field_name && $definitions[$field_name]->getType() == 'integer') {
912       $ids = array_filter($ids, function ($id) {
913         return is_numeric($id) && $id == (int) $id;
914       });
915       $ids = array_map('intval', $ids);
916     }
917     return $ids;
918   }
919
920   /**
921    * Gets entities from the persistent cache backend.
922    *
923    * @param array|null &$ids
924    *   If not empty, return entities that match these IDs. IDs that were found
925    *   will be removed from the list.
926    *
927    * @return \Drupal\Core\Entity\ContentEntityInterface[]
928    *   Array of entities from the persistent cache.
929    */
930   protected function getFromPersistentCache(array &$ids = NULL) {
931     if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
932       return [];
933     }
934     $entities = [];
935     // Build the list of cache entries to retrieve.
936     $cid_map = [];
937     foreach ($ids as $id) {
938       $cid_map[$id] = $this->buildCacheId($id);
939     }
940     $cids = array_values($cid_map);
941     if ($cache = $this->cacheBackend->getMultiple($cids)) {
942       // Get the entities that were found in the cache.
943       foreach ($ids as $index => $id) {
944         $cid = $cid_map[$id];
945         if (isset($cache[$cid])) {
946           $entities[$id] = $cache[$cid]->data;
947           unset($ids[$index]);
948         }
949       }
950     }
951     return $entities;
952   }
953
954   /**
955    * Stores entities in the persistent cache backend.
956    *
957    * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
958    *   Entities to store in the cache.
959    */
960   protected function setPersistentCache($entities) {
961     if (!$this->entityType->isPersistentlyCacheable()) {
962       return;
963     }
964
965     $cache_tags = [
966       $this->entityTypeId . '_values',
967       'entity_field_info',
968     ];
969     foreach ($entities as $id => $entity) {
970       $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
971     }
972   }
973
974   /**
975    * {@inheritdoc}
976    */
977   public function loadUnchanged($id) {
978     $ids = [$id];
979
980     // The cache invalidation in the parent has the side effect that loading the
981     // same entity again during the save process (for example in
982     // hook_entity_presave()) will load the unchanged entity. Simulate this
983     // by explicitly removing the entity from the static cache.
984     parent::resetCache($ids);
985
986     // The default implementation in the parent class unsets the current cache
987     // and then reloads the entity. That is slow, especially if this is done
988     // repeatedly in the same request, e.g. when validating and then saving
989     // an entity. Optimize this for content entities by trying to load them
990     // directly from the persistent cache again, as in contrast to the static
991     // cache the persistent one will never be changed until the entity is saved.
992     $entities = $this->getFromPersistentCache($ids);
993
994     if (!$entities) {
995       $entities[$id] = $this->load($id);
996     }
997     else {
998       // As the entities are put into the persistent cache before the post load
999       // has been executed we have to execute it if we have retrieved the
1000       // entity directly from the persistent cache.
1001       $this->postLoad($entities);
1002
1003       if ($this->entityType->isStaticallyCacheable()) {
1004         // As we've removed the entity from the static cache already we have to
1005         // put the loaded unchanged entity there to simulate the behavior of the
1006         // parent.
1007         $this->setStaticCache($entities);
1008       }
1009     }
1010
1011     return $entities[$id];
1012   }
1013
1014   /**
1015    * {@inheritdoc}
1016    */
1017   public function resetCache(array $ids = NULL) {
1018     if ($ids) {
1019       parent::resetCache($ids);
1020       if ($this->entityType->isPersistentlyCacheable()) {
1021         $cids = [];
1022         foreach ($ids as $id) {
1023           unset($this->latestRevisionIds[$id]);
1024           $cids[] = $this->buildCacheId($id);
1025         }
1026         $this->cacheBackend->deleteMultiple($cids);
1027       }
1028     }
1029     else {
1030       parent::resetCache();
1031       if ($this->entityType->isPersistentlyCacheable()) {
1032         Cache::invalidateTags([$this->entityTypeId . '_values']);
1033       }
1034       $this->latestRevisionIds = [];
1035     }
1036   }
1037
1038 }