3 namespace Drupal\Core\Entity;
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;
15 * Base class for content entity storage handlers.
17 abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
20 * The entity bundle key.
24 protected $bundleKey = FALSE;
29 * @var \Drupal\Core\Entity\EntityManagerInterface
31 protected $entityManager;
36 * @var \Drupal\Core\Cache\CacheBackendInterface
38 protected $cacheBackend;
41 * Stores the latest revision IDs for entities.
45 protected $latestRevisionIds = [];
48 * Constructs a ContentEntityStorageBase object.
50 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
51 * The entity type definition.
52 * @param \Drupal\Core\Entity\EntityManagerInterface $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.
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;
69 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
72 $container->get('entity.manager'),
73 $container->get('cache.entity'),
74 $container->get('entity.memory_cache')
81 protected function doCreate(array $values) {
82 // We have to determine the bundle first.
84 if ($this->bundleKey) {
85 if (!isset($values[$this->bundleKey])) {
86 throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
88 $bundle = $values[$this->bundleKey];
90 $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
91 $this->initFieldValues($entity, $values);
98 public function createWithSampleValues($bundle = FALSE, array $values = []) {
99 // ID and revision should never have sample values generated for them.
101 $this->entityType->getKey('id'),
103 if ($revision_key = $this->entityType->getKey('revision')) {
104 $forbidden_keys[] = $revision_key;
106 if ($bundle_key = $this->entityType->getKey('bundle')) {
108 throw new EntityStorageException("No entity bundle was specified");
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));
113 $values[$bundle_key] = $bundle;
114 // Bundle is already set
115 $forbidden_keys[] = $bundle_key;
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();
130 * Initializes field values.
132 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
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.
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];
148 elseif (!array_key_exists($name, $values)) {
149 $entity->get($name)->applyDefaultValue();
152 unset($values[$name]);
155 // Set any passed values for non-defined fields also.
156 foreach ($values as $name => $value) {
157 $entity->$name = $value;
160 // Make sure modules can alter field initial values.
161 $this->invokeHook('field_values_init', $entity);
165 * Checks whether any entity revision is translated.
167 * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
168 * The entity object to be checked.
171 * TRUE if the entity has at least one translation in any revision, FALSE
174 * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
175 * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyStoredRevisionTranslated()
177 protected function isAnyRevisionTranslated(TranslatableInterface $entity) {
178 return $entity->getTranslationLanguages(FALSE) || $this->isAnyStoredRevisionTranslated($entity);
182 * Checks whether any stored entity revision is translated.
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
189 * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
190 * The entity object to be checked.
193 * TRUE if the entity has at least one translation in any revision, FALSE
196 * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
197 * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyRevisionTranslated()
199 protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) {
200 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
201 if ($entity->isNew()) {
205 if ($entity instanceof TranslationStatusInterface) {
206 foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
207 if ($entity->getTranslationStatus($langcode) === TranslationStatusInterface::TRANSLATION_EXISTING) {
213 $query = $this->getQuery()
214 ->condition($this->entityType->getKey('id'), $entity->id())
215 ->condition($this->entityType->getKey('default_langcode'), 0)
219 if ($entity->getEntityType()->isRevisionable()) {
220 $query->allRevisions();
223 $result = $query->execute();
224 return !empty($result);
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();
235 $field_names = array_map(function (FieldDefinitionInterface $definition) {
236 return $definition->getName();
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);
248 public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
249 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
250 $new_revision = clone $entity;
252 $original_keep_untranslatable_fields = $keep_untranslatable_fields;
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());
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();
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) {
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);
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(),
288 foreach ($sync_items as $field_name => $items) {
289 $new_revision_translation->set($field_name, $items->getValue());
292 // Make sure the "revision_translation_affected" flag is recalculated.
293 $new_revision_translation->setRevisionTranslationAffected(NULL);
295 // No need to copy untranslatable field values more than once.
296 $keep_untranslatable_fields = TRUE;
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);
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;
315 // Eventually mark the new revision as such.
316 $new_revision->setNewRevision();
317 $new_revision->isDefaultRevision($default);
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);
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);
329 return $new_revision;
333 * Returns an array of field names to skip when merging revision translations.
336 * An array of field names.
338 protected function getRevisionTranslationMergeSkippedFieldNames() {
339 /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
340 $entity_type = $this->getEntityType();
342 // A list of known revision metadata fields which should be skipped from
345 $entity_type->getKey('revision'),
346 $entity_type->getKey('revision_translation_affected'),
348 $field_names = array_merge($field_names, array_values($entity_type->getRevisionMetadataKeys()));
356 public function getLatestRevisionId($entity_id) {
357 if (!$this->entityType->isRevisionable()) {
361 if (!isset($this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT])) {
362 $result = $this->getQuery()
364 ->condition($this->entityType->getKey('id'), $entity_id)
368 $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT] = key($result);
371 return $this->latestRevisionIds[$entity_id][LanguageInterface::LANGCODE_DEFAULT];
377 public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
378 if (!$this->entityType->isRevisionable()) {
382 if (!$this->entityType->isTranslatable()) {
383 return $this->getLatestRevisionId($entity_id);
386 if (!isset($this->latestRevisionIds[$entity_id][$langcode])) {
387 $result = $this->getQuery()
389 ->condition($this->entityType->getKey('id'), $entity_id)
390 ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
392 ->sort($this->entityType->getKey('revision'), 'DESC')
396 $this->latestRevisionIds[$entity_id][$langcode] = key($result);
398 return $this->latestRevisionIds[$entity_id][$langcode];
404 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {}
409 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {}
414 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {}
419 public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) {}
424 public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) {}
429 public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {}
434 public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
435 $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
437 foreach ($items_by_entity as $items) {
439 $this->purgeFieldItems($items->getEntity(), $field_definition);
441 return count($items_by_entity);
445 * Reads values to be purged for a single field.
447 * This method is called during field data purge, on fields for which
448 * onFieldDefinitionDelete() has previously run.
450 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
451 * The field definition.
453 * The maximum number of field data records to purge before returning.
455 * @return \Drupal\Core\Field\FieldItemListInterface[]
456 * An array of field item lists, keyed by entity revision id.
458 abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
461 * Removes field items from storage per entity during purge.
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.
468 abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
473 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {}
478 public function loadRevision($revision_id) {
479 $revisions = $this->loadMultipleRevisions([$revision_id]);
481 return isset($revisions[$revision_id]) ? $revisions[$revision_id] : NULL;
487 public function loadMultipleRevisions(array $revision_ids) {
488 $revisions = $this->doLoadMultipleRevisionsFieldItems($revision_ids);
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
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;
503 // Invoke the entity hooks for each group.
504 foreach ($entity_groups as $entities) {
505 $this->invokeStorageLoadHook($entities);
506 $this->postLoad($entities);
513 * Actually loads revision field item values from the storage.
515 * @param int|string $revision_id
516 * The revision identifier.
518 * @return \Drupal\Core\Entity\EntityInterface|null
519 * The specified entity revision or NULL if not found.
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.
525 * @see https://www.drupal.org/node/2924915
527 abstract protected function doLoadRevisionFieldItems($revision_id);
530 * Actually loads revision field item values from the storage.
532 * @param array $revision_ids
533 * An array of revision identifiers.
535 * @return \Drupal\Core\Entity\EntityInterface[]
536 * The specified entity revisions or an empty array if none are found.
538 protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
540 foreach ($revision_ids as $revision_id) {
541 $revisions[] = $this->doLoadRevisionFieldItems($revision_id);
550 protected function doSave($id, EntityInterface $entity) {
551 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
553 if ($entity->isNew()) {
554 // Ensure the entity is still seen as new after assigning it an id, while
556 $entity->enforceIsNew();
557 if ($this->entityType->isRevisionable()) {
558 $entity->setNewRevision();
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;
568 $this->populateAffectedRevisionTranslations($entity);
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());
578 $this->doSaveFieldItems($entity);
584 * Writes entity field values to the storage.
586 * This method is responsible for allocating entity and revision identifiers
587 * and updating the entity object with their values.
589 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
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.
595 abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
600 protected function doPreSave(EntityInterface $entity) {
601 /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
603 // Sync the changes made in the fields array to the internal values array.
604 $entity->updateOriginalValues();
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();
613 $id = parent::doPreSave($entity);
615 if (!$entity->isNew()) {
616 // If the ID changed then original can't be loaded, throw an exception
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.");
621 // Do not allow changing the revision ID when resaving the current
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.");
634 protected function doPostSave(EntityInterface $entity, $update) {
635 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
637 if ($update && $this->entityType->isTranslatable()) {
638 $this->invokeTranslationHooks($entity);
641 parent::doPostSave($entity, $update);
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);
653 protected function doDelete($entities) {
654 /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
655 foreach ($entities as $entity) {
656 $this->invokeFieldMethod('delete', $entity);
658 $this->doDeleteFieldItems($entities);
662 * Deletes entity field values from the storage.
664 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
665 * An array of entity objects to be deleted.
667 abstract protected function doDeleteFieldItems($entities);
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');
679 $this->invokeFieldMethod('deleteRevision', $revision);
680 $this->doDeleteRevisionFieldItems($revision);
681 $this->invokeHook('revision_delete', $revision);
686 * Deletes field values of an entity revision from the storage.
688 * @param \Drupal\Core\Entity\ContentEntityInterface $revision
689 * An entity revision object to be deleted.
691 abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
694 * Checks translation statuses and invoke the related hooks if needed.
696 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
697 * The entity being saved.
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);
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));
709 elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
710 $this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
716 * Invokes hook_entity_storage_load().
718 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
719 * List of entities, keyed on the entity ID.
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);
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);
739 protected function invokeHook($hook, EntityInterface $entity) {
740 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
744 $this->invokeFieldMethod('preSave', $entity);
748 $this->invokeFieldPostSave($entity, FALSE);
752 $this->invokeFieldPostSave($entity, TRUE);
756 parent::invokeHook($hook, $entity);
760 * Invokes a method on the Field objects within an entity.
762 * Any argument passed will be forwarded to the invoked method.
764 * @param string $method
765 * The name of the method to be invoked.
766 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
770 * A multidimensional associative array of results, keyed by entity
771 * translation language code and field name.
773 protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
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);
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}();
798 // We need to call the delete method for field items of removed
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) {
815 * Invokes the post save method on the Field objects within an entity.
817 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
819 * @param bool $update
820 * Specifies whether the entity is being updated or created.
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.
826 foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $translation_results) {
827 $resave += array_filter($translation_results);
830 $this->doSaveFieldItems($entity, array_keys($resave));
835 * Checks whether the field values changed compared to the original entity.
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.
845 * True if the field value changed from the original entity.
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.
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)) {
867 * Populates the affected flag for all the revision translations.
869 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
870 * An entity object being saved.
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);
894 * Ensures integer entity key values are valid.
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.
901 * The entity key values to verify.
902 * @param string $entity_key
903 * (optional) The entity key to sanitise values for. Defaults to 'id'.
906 * The sanitized list of entity key values.
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;
915 $ids = array_map('intval', $ids);
921 * Gets entities from the persistent cache backend.
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.
927 * @return \Drupal\Core\Entity\ContentEntityInterface[]
928 * Array of entities from the persistent cache.
930 protected function getFromPersistentCache(array &$ids = NULL) {
931 if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
935 // Build the list of cache entries to retrieve.
937 foreach ($ids as $id) {
938 $cid_map[$id] = $this->buildCacheId($id);
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;
955 * Stores entities in the persistent cache backend.
957 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
958 * Entities to store in the cache.
960 protected function setPersistentCache($entities) {
961 if (!$this->entityType->isPersistentlyCacheable()) {
966 $this->entityTypeId . '_values',
969 foreach ($entities as $id => $entity) {
970 $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
977 public function loadUnchanged($id) {
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);
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);
995 $entities[$id] = $this->load($id);
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);
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
1007 $this->setStaticCache($entities);
1011 return $entities[$id];
1017 public function resetCache(array $ids = NULL) {
1019 parent::resetCache($ids);
1020 if ($this->entityType->isPersistentlyCacheable()) {
1022 foreach ($ids as $id) {
1023 unset($this->latestRevisionIds[$id]);
1024 $cids[] = $this->buildCacheId($id);
1026 $this->cacheBackend->deleteMultiple($cids);
1030 parent::resetCache();
1031 if ($this->entityType->isPersistentlyCacheable()) {
1032 Cache::invalidateTags([$this->entityTypeId . '_values']);
1034 $this->latestRevisionIds = [];