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