3 namespace Drupal\Core\Entity;
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;
13 * Base class for content entity storage handlers.
15 abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
18 * The entity bundle key.
22 protected $bundleKey = FALSE;
27 * @var \Drupal\Core\Entity\EntityManagerInterface
29 protected $entityManager;
34 * @var \Drupal\Core\Cache\CacheBackendInterface
36 protected $cacheBackend;
39 * Constructs a ContentEntityStorageBase object.
41 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
42 * The entity type definition.
43 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
45 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
46 * The cache backend to be used.
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;
58 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
61 $container->get('entity.manager'),
62 $container->get('cache.entity')
69 protected function doCreate(array $values) {
70 // We have to determine the bundle first.
72 if ($this->bundleKey) {
73 if (!isset($values[$this->bundleKey])) {
74 throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
76 $bundle = $values[$this->bundleKey];
78 $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
79 $this->initFieldValues($entity, $values);
86 public function createWithSampleValues($bundle = FALSE, array $values = []) {
87 // ID and revision should never have sample values generated for them.
89 $this->entityType->getKey('id'),
91 if ($revision_key = $this->entityType->getKey('revision')) {
92 $forbidden_keys[] = $revision_key;
94 if ($bundle_key = $this->entityType->getKey('bundle')) {
96 throw new EntityStorageException("No entity bundle was specified");
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));
101 $values[$bundle_key] = $bundle;
102 // Bundle is already set
103 $forbidden_keys[] = $bundle_key;
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();
118 * Initializes field values.
120 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
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.
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];
136 elseif (!array_key_exists($name, $values)) {
137 $entity->get($name)->applyDefaultValue();
140 unset($values[$name]);
143 // Set any passed values for non-defined fields also.
144 foreach ($values as $name => $value) {
145 $entity->$name = $value;
148 // Make sure modules can alter field initial values.
149 $this->invokeHook('field_values_init', $entity);
153 * Checks whether any entity revision is translated.
155 * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
156 * The entity object to be checked.
159 * TRUE if the entity has at least one translation in any revision, FALSE
162 * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
163 * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyStoredRevisionTranslated()
165 protected function isAnyRevisionTranslated(TranslatableInterface $entity) {
166 return $entity->getTranslationLanguages(FALSE) || $this->isAnyStoredRevisionTranslated($entity);
170 * Checks whether any stored entity revision is translated.
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
177 * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
178 * The entity object to be checked.
181 * TRUE if the entity has at least one translation in any revision, FALSE
184 * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
185 * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyRevisionTranslated()
187 protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) {
188 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
189 if ($entity->isNew()) {
193 if ($entity instanceof TranslationStatusInterface) {
194 foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
195 if ($entity->getTranslationStatus($langcode) === TranslationStatusInterface::TRANSLATION_EXISTING) {
201 $query = $this->getQuery()
202 ->condition($this->entityType->getKey('id'), $entity->id())
203 ->condition($this->entityType->getKey('default_langcode'), 0)
207 if ($entity->getEntityType()->isRevisionable()) {
208 $query->allRevisions();
211 $result = $query->execute();
212 return !empty($result);
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();
223 $field_names = array_map(function (FieldDefinitionInterface $definition) {
224 return $definition->getName();
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);
236 public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
237 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
238 $new_revision = clone $entity;
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());
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();
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) {
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);
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(),
274 foreach ($sync_items as $field_name => $items) {
275 $new_revision_translation->set($field_name, $items->getValue());
278 // Make sure the "revision_translation_affected" flag is recalculated.
279 $new_revision_translation->setRevisionTranslationAffected(NULL);
281 // No need to copy untranslatable field values more than once.
282 $keep_untranslatable_fields = TRUE;
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);
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;
301 // Eventually mark the new revision as such.
302 $new_revision->setNewRevision();
303 $new_revision->isDefaultRevision($default);
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);
310 return $new_revision;
314 * Returns an array of field names to skip when merging revision translations.
317 * An array of field names.
319 protected function getRevisionTranslationMergeSkippedFieldNames() {
320 /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
321 $entity_type = $this->getEntityType();
323 // A list of known revision metadata fields which should be skipped from
326 $entity_type->getKey('revision'),
327 $entity_type->getKey('revision_translation_affected'),
329 $field_names = array_merge($field_names, array_values($entity_type->getRevisionMetadataKeys()));
337 public function getLatestRevisionId($entity_id) {
338 if (!$this->entityType->isRevisionable()) {
342 $result = $this->getQuery()
344 ->condition($this->entityType->getKey('id'), $entity_id)
354 public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
355 if (!$this->entityType->isRevisionable()) {
359 if (!$this->entityType->isTranslatable()) {
360 return $this->getLatestRevisionId($entity_id);
363 $result = $this->getQuery()
365 ->condition($this->entityType->getKey('id'), $entity_id)
366 ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
368 ->sort($this->entityType->getKey('revision'), 'DESC')
378 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {}
383 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {}
388 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {}
393 public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) {}
398 public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) {}
403 public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {}
408 public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
409 $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
411 foreach ($items_by_entity as $items) {
413 $this->purgeFieldItems($items->getEntity(), $field_definition);
415 return count($items_by_entity);
419 * Reads values to be purged for a single field.
421 * This method is called during field data purge, on fields for which
422 * onFieldDefinitionDelete() has previously run.
424 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
425 * The field definition.
427 * The maximum number of field data records to purge before returning.
429 * @return \Drupal\Core\Field\FieldItemListInterface[]
430 * An array of field item lists, keyed by entity revision id.
432 abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
435 * Removes field items from storage per entity during purge.
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.
442 abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
447 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {}
452 public function loadRevision($revision_id) {
453 $revisions = $this->loadMultipleRevisions([$revision_id]);
455 return isset($revisions[$revision_id]) ? $revisions[$revision_id] : NULL;
461 public function loadMultipleRevisions(array $revision_ids) {
462 $revisions = $this->doLoadMultipleRevisionsFieldItems($revision_ids);
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
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;
477 // Invoke the entity hooks for each group.
478 foreach ($entity_groups as $entities) {
479 $this->invokeStorageLoadHook($entities);
480 $this->postLoad($entities);
487 * Actually loads revision field item values from the storage.
489 * @param int|string $revision_id
490 * The revision identifier.
492 * @return \Drupal\Core\Entity\EntityInterface|null
493 * The specified entity revision or NULL if not found.
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.
499 * @see https://www.drupal.org/node/2924915
501 abstract protected function doLoadRevisionFieldItems($revision_id);
504 * Actually loads revision field item values from the storage.
506 * @param array $revision_ids
507 * An array of revision identifiers.
509 * @return \Drupal\Core\Entity\EntityInterface[]
510 * The specified entity revisions or an empty array if none are found.
512 protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
514 foreach ($revision_ids as $revision_id) {
515 $revisions[] = $this->doLoadRevisionFieldItems($revision_id);
524 protected function doSave($id, EntityInterface $entity) {
525 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
527 if ($entity->isNew()) {
528 // Ensure the entity is still seen as new after assigning it an id, while
530 $entity->enforceIsNew();
531 if ($this->entityType->isRevisionable()) {
532 $entity->setNewRevision();
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;
542 $this->populateAffectedRevisionTranslations($entity);
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());
552 $this->doSaveFieldItems($entity);
558 * Writes entity field values to the storage.
560 * This method is responsible for allocating entity and revision identifiers
561 * and updating the entity object with their values.
563 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
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.
569 abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
574 protected function doPreSave(EntityInterface $entity) {
575 /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
577 // Sync the changes made in the fields array to the internal values array.
578 $entity->updateOriginalValues();
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();
587 $id = parent::doPreSave($entity);
589 if (!$entity->isNew()) {
590 // If the ID changed then original can't be loaded, throw an exception
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.");
595 // Do not allow changing the revision ID when resaving the current
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.");
608 protected function doPostSave(EntityInterface $entity, $update) {
609 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
611 if ($update && $this->entityType->isTranslatable()) {
612 $this->invokeTranslationHooks($entity);
615 parent::doPostSave($entity, $update);
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);
627 protected function doDelete($entities) {
628 /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
629 foreach ($entities as $entity) {
630 $this->invokeFieldMethod('delete', $entity);
632 $this->doDeleteFieldItems($entities);
636 * Deletes entity field values from the storage.
638 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
639 * An array of entity objects to be deleted.
641 abstract protected function doDeleteFieldItems($entities);
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');
653 $this->invokeFieldMethod('deleteRevision', $revision);
654 $this->doDeleteRevisionFieldItems($revision);
655 $this->invokeHook('revision_delete', $revision);
660 * Deletes field values of an entity revision from the storage.
662 * @param \Drupal\Core\Entity\ContentEntityInterface $revision
663 * An entity revision object to be deleted.
665 abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
668 * Checks translation statuses and invoke the related hooks if needed.
670 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
671 * The entity being saved.
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);
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));
683 elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
684 $this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
690 * Invokes hook_entity_storage_load().
692 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
693 * List of entities, keyed on the entity ID.
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);
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);
713 protected function invokeHook($hook, EntityInterface $entity) {
714 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
718 $this->invokeFieldMethod('preSave', $entity);
722 $this->invokeFieldPostSave($entity, FALSE);
726 $this->invokeFieldPostSave($entity, TRUE);
730 parent::invokeHook($hook, $entity);
734 * Invokes a method on the Field objects within an entity.
736 * Any argument passed will be forwarded to the invoked method.
738 * @param string $method
739 * The name of the method to be invoked.
740 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
744 * A multidimensional associative array of results, keyed by entity
745 * translation language code and field name.
747 protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
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);
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}();
772 // We need to call the delete method for field items of removed
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) {
789 * Invokes the post save method on the Field objects within an entity.
791 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
793 * @param bool $update
794 * Specifies whether the entity is being updated or created.
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.
800 foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $translation_results) {
801 $resave += array_filter($translation_results);
804 $this->doSaveFieldItems($entity, array_keys($resave));
809 * Checks whether the field values changed compared to the original entity.
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.
819 * True if the field value changed from the original entity.
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.
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)) {
841 * Populates the affected flag for all the revision translations.
843 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
844 * An entity object being saved.
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);
868 * Ensures integer entity key values are valid.
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.
875 * The entity key values to verify.
876 * @param string $entity_key
877 * (optional) The entity key to sanitise values for. Defaults to 'id'.
880 * The sanitized list of entity key values.
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;
889 $ids = array_map('intval', $ids);
895 * Gets entities from the persistent cache backend.
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.
901 * @return \Drupal\Core\Entity\ContentEntityInterface[]
902 * Array of entities from the persistent cache.
904 protected function getFromPersistentCache(array &$ids = NULL) {
905 if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
909 // Build the list of cache entries to retrieve.
911 foreach ($ids as $id) {
912 $cid_map[$id] = $this->buildCacheId($id);
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;
929 * Stores entities in the persistent cache backend.
931 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
932 * Entities to store in the cache.
934 protected function setPersistentCache($entities) {
935 if (!$this->entityType->isPersistentlyCacheable()) {
940 $this->entityTypeId . '_values',
943 foreach ($entities as $id => $entity) {
944 $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
951 public function loadUnchanged($id) {
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);
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);
969 $entities[$id] = $this->load($id);
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);
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
981 $this->setStaticCache($entities);
985 return $entities[$id];
991 public function resetCache(array $ids = NULL) {
994 foreach ($ids as $id) {
995 unset($this->entities[$id]);
996 $cids[] = $this->buildCacheId($id);
998 if ($this->entityType->isPersistentlyCacheable()) {
999 $this->cacheBackend->deleteMultiple($cids);
1003 $this->entities = [];
1004 if ($this->entityType->isPersistentlyCacheable()) {
1005 Cache::invalidateTags([$this->entityTypeId . '_values']);
1011 * Builds the cache ID for the passed in entity ID.
1014 * Entity ID for which the cache ID should be built.
1017 * Cache ID that can be passed to the cache backend.
1019 protected function buildCacheId($id) {
1020 return "values:{$this->entityTypeId}:$id";