123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023 |
- <?php
- namespace Drupal\Core\Entity;
- use Drupal\Core\Cache\Cache;
- use Drupal\Core\Cache\CacheBackendInterface;
- use Drupal\Core\Field\FieldDefinitionInterface;
- use Drupal\Core\Field\FieldStorageDefinitionInterface;
- use Drupal\Core\TypedData\TranslationStatusInterface;
- use Symfony\Component\DependencyInjection\ContainerInterface;
- /**
- * Base class for content entity storage handlers.
- */
- abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
- /**
- * The entity bundle key.
- *
- * @var string|bool
- */
- protected $bundleKey = FALSE;
- /**
- * The entity manager.
- *
- * @var \Drupal\Core\Entity\EntityManagerInterface
- */
- protected $entityManager;
- /**
- * Cache backend.
- *
- * @var \Drupal\Core\Cache\CacheBackendInterface
- */
- protected $cacheBackend;
- /**
- * Constructs a ContentEntityStorageBase object.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * The entity type definition.
- * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
- * The entity manager.
- * @param \Drupal\Core\Cache\CacheBackendInterface $cache
- * The cache backend to be used.
- */
- public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
- parent::__construct($entity_type);
- $this->bundleKey = $this->entityType->getKey('bundle');
- $this->entityManager = $entity_manager;
- $this->cacheBackend = $cache;
- }
- /**
- * {@inheritdoc}
- */
- public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
- return new static(
- $entity_type,
- $container->get('entity.manager'),
- $container->get('cache.entity')
- );
- }
- /**
- * {@inheritdoc}
- */
- protected function doCreate(array $values) {
- // We have to determine the bundle first.
- $bundle = FALSE;
- if ($this->bundleKey) {
- if (!isset($values[$this->bundleKey])) {
- throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
- }
- $bundle = $values[$this->bundleKey];
- }
- $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
- $this->initFieldValues($entity, $values);
- return $entity;
- }
- /**
- * {@inheritdoc}
- */
- public function createWithSampleValues($bundle = FALSE, array $values = []) {
- // ID and revision should never have sample values generated for them.
- $forbidden_keys = [
- $this->entityType->getKey('id'),
- ];
- if ($revision_key = $this->entityType->getKey('revision')) {
- $forbidden_keys[] = $revision_key;
- }
- if ($bundle_key = $this->entityType->getKey('bundle')) {
- if (!$bundle) {
- throw new EntityStorageException("No entity bundle was specified");
- }
- if (!array_key_exists($bundle, $this->entityManager->getBundleInfo($this->entityTypeId))) {
- throw new EntityStorageException(sprintf("Missing entity bundle. The \"%s\" bundle does not exist", $bundle));
- }
- $values[$bundle_key] = $bundle;
- // Bundle is already set
- $forbidden_keys[] = $bundle_key;
- }
- // Forbid sample generation on any keys whose values were submitted.
- $forbidden_keys = array_merge($forbidden_keys, array_keys($values));
- /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
- $entity = $this->create($values);
- foreach ($entity as $field_name => $value) {
- if (!in_array($field_name, $forbidden_keys, TRUE)) {
- $entity->get($field_name)->generateSampleItems();
- }
- }
- return $entity;
- }
- /**
- * Initializes field values.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * An entity object.
- * @param array $values
- * (optional) An associative array of initial field values keyed by field
- * name. If none is provided default values will be applied.
- * @param array $field_names
- * (optional) An associative array of field names to be initialized. If none
- * is provided all fields will be initialized.
- */
- protected function initFieldValues(ContentEntityInterface $entity, array $values = [], array $field_names = []) {
- // Populate field values.
- foreach ($entity as $name => $field) {
- if (!$field_names || isset($field_names[$name])) {
- if (isset($values[$name])) {
- $entity->$name = $values[$name];
- }
- elseif (!array_key_exists($name, $values)) {
- $entity->get($name)->applyDefaultValue();
- }
- }
- unset($values[$name]);
- }
- // Set any passed values for non-defined fields also.
- foreach ($values as $name => $value) {
- $entity->$name = $value;
- }
- // Make sure modules can alter field initial values.
- $this->invokeHook('field_values_init', $entity);
- }
- /**
- * Checks whether any entity revision is translated.
- *
- * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
- * The entity object to be checked.
- *
- * @return bool
- * TRUE if the entity has at least one translation in any revision, FALSE
- * otherwise.
- *
- * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
- * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyStoredRevisionTranslated()
- */
- protected function isAnyRevisionTranslated(TranslatableInterface $entity) {
- return $entity->getTranslationLanguages(FALSE) || $this->isAnyStoredRevisionTranslated($entity);
- }
- /**
- * Checks whether any stored entity revision is translated.
- *
- * A revisionable entity can have translations in a pending revision, hence
- * the default revision may appear as not translated. This determines whether
- * the entity has any translation in the storage and thus should be considered
- * as multilingual.
- *
- * @param \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\TranslatableInterface $entity
- * The entity object to be checked.
- *
- * @return bool
- * TRUE if the entity has at least one translation in any revision, FALSE
- * otherwise.
- *
- * @see \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages()
- * @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyRevisionTranslated()
- */
- protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) {
- /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
- if ($entity->isNew()) {
- return FALSE;
- }
- if ($entity instanceof TranslationStatusInterface) {
- foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
- if ($entity->getTranslationStatus($langcode) === TranslationStatusInterface::TRANSLATION_EXISTING) {
- return TRUE;
- }
- }
- }
- $query = $this->getQuery()
- ->condition($this->entityType->getKey('id'), $entity->id())
- ->condition($this->entityType->getKey('default_langcode'), 0)
- ->accessCheck(FALSE)
- ->range(0, 1);
- if ($entity->getEntityType()->isRevisionable()) {
- $query->allRevisions();
- }
- $result = $query->execute();
- return !empty($result);
- }
- /**
- * {@inheritdoc}
- */
- public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
- $translation = $entity->getTranslation($langcode);
- $definitions = array_filter($translation->getFieldDefinitions(), function (FieldDefinitionInterface $definition) {
- return $definition->isTranslatable();
- });
- $field_names = array_map(function (FieldDefinitionInterface $definition) {
- return $definition->getName();
- }, $definitions);
- $values[$this->langcodeKey] = $langcode;
- $values[$this->getEntityType()->getKey('default_langcode')] = FALSE;
- $this->initFieldValues($translation, $values, $field_names);
- $this->invokeHook('translation_create', $translation);
- return $translation;
- }
- /**
- * {@inheritdoc}
- */
- public function createRevision(RevisionableInterface $entity, $default = TRUE, $keep_untranslatable_fields = NULL) {
- /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
- $new_revision = clone $entity;
- // For translatable entities, create a merged revision of the active
- // translation and the other translations in the default revision. This
- // permits the creation of pending revisions that can always be saved as the
- // new default revision without reverting changes in other languages.
- if (!$entity->isNew() && !$entity->isDefaultRevision() && $entity->isTranslatable() && $this->isAnyRevisionTranslated($entity)) {
- $active_langcode = $entity->language()->getId();
- $skipped_field_names = array_flip($this->getRevisionTranslationMergeSkippedFieldNames());
- // By default we copy untranslatable field values from the default
- // revision, unless they are configured to affect only the default
- // translation. This way we can ensure we always have only one affected
- // translation in pending revisions. This constraint is enforced by
- // EntityUntranslatableFieldsConstraintValidator.
- if (!isset($keep_untranslatable_fields)) {
- $keep_untranslatable_fields = $entity->isDefaultTranslation() && $entity->isDefaultTranslationAffectedOnly();
- }
- /** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
- $default_revision = $this->load($entity->id());
- $translation_languages = $default_revision->getTranslationLanguages();
- foreach ($translation_languages as $langcode => $language) {
- if ($langcode == $active_langcode) {
- continue;
- }
- $default_revision_translation = $default_revision->getTranslation($langcode);
- $new_revision_translation = $new_revision->hasTranslation($langcode) ?
- $new_revision->getTranslation($langcode) : $new_revision->addTranslation($langcode);
- /** @var \Drupal\Core\Field\FieldItemListInterface[] $sync_items */
- $sync_items = array_diff_key(
- $keep_untranslatable_fields ? $default_revision_translation->getTranslatableFields() : $default_revision_translation->getFields(),
- $skipped_field_names
- );
- foreach ($sync_items as $field_name => $items) {
- $new_revision_translation->set($field_name, $items->getValue());
- }
- // Make sure the "revision_translation_affected" flag is recalculated.
- $new_revision_translation->setRevisionTranslationAffected(NULL);
- // No need to copy untranslatable field values more than once.
- $keep_untranslatable_fields = TRUE;
- }
- // Make sure we do not inadvertently recreate removed translations.
- foreach (array_diff_key($new_revision->getTranslationLanguages(), $translation_languages) as $langcode => $language) {
- // Allow a new revision to be created for the active language.
- if ($langcode !== $active_langcode) {
- $new_revision->removeTranslation($langcode);
- }
- }
- // The "original" property is used in various places to detect changes in
- // field values with respect to the stored ones. If the property is not
- // defined, the stored version is loaded explicitly. Since the merged
- // revision generated here is not stored anywhere, we need to populate the
- // "original" property manually, so that changes can be properly detected.
- $new_revision->original = clone $new_revision;
- }
- // Eventually mark the new revision as such.
- $new_revision->setNewRevision();
- $new_revision->isDefaultRevision($default);
- // Actually make sure the current translation is marked as affected, even if
- // there are no explicit changes, to be sure this revision can be related
- // to the correct translation.
- $new_revision->setRevisionTranslationAffected(TRUE);
- return $new_revision;
- }
- /**
- * Returns an array of field names to skip when merging revision translations.
- *
- * @return array
- * An array of field names.
- */
- protected function getRevisionTranslationMergeSkippedFieldNames() {
- /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
- $entity_type = $this->getEntityType();
- // A list of known revision metadata fields which should be skipped from
- // the comparision.
- $field_names = [
- $entity_type->getKey('revision'),
- $entity_type->getKey('revision_translation_affected'),
- ];
- $field_names = array_merge($field_names, array_values($entity_type->getRevisionMetadataKeys()));
- return $field_names;
- }
- /**
- * {@inheritdoc}
- */
- public function getLatestRevisionId($entity_id) {
- if (!$this->entityType->isRevisionable()) {
- return NULL;
- }
- $result = $this->getQuery()
- ->latestRevision()
- ->condition($this->entityType->getKey('id'), $entity_id)
- ->accessCheck(FALSE)
- ->execute();
- return key($result);
- }
- /**
- * {@inheritdoc}
- */
- public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
- if (!$this->entityType->isRevisionable()) {
- return NULL;
- }
- if (!$this->entityType->isTranslatable()) {
- return $this->getLatestRevisionId($entity_id);
- }
- $result = $this->getQuery()
- ->allRevisions()
- ->condition($this->entityType->getKey('id'), $entity_id)
- ->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
- ->range(0, 1)
- ->sort($this->entityType->getKey('revision'), 'DESC')
- ->accessCheck(FALSE)
- ->execute();
- return key($result);
- }
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {}
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {}
- /**
- * {@inheritdoc}
- */
- public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {}
- /**
- * {@inheritdoc}
- */
- public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) {}
- /**
- * {@inheritdoc}
- */
- public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) {}
- /**
- * {@inheritdoc}
- */
- public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {}
- /**
- * {@inheritdoc}
- */
- public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
- $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
- foreach ($items_by_entity as $items) {
- $items->delete();
- $this->purgeFieldItems($items->getEntity(), $field_definition);
- }
- return count($items_by_entity);
- }
- /**
- * Reads values to be purged for a single field.
- *
- * This method is called during field data purge, on fields for which
- * onFieldDefinitionDelete() has previously run.
- *
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- * The field definition.
- * @param $batch_size
- * The maximum number of field data records to purge before returning.
- *
- * @return \Drupal\Core\Field\FieldItemListInterface[]
- * An array of field item lists, keyed by entity revision id.
- */
- abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
- /**
- * Removes field items from storage per entity during purge.
- *
- * @param ContentEntityInterface $entity
- * The entity revision, whose values are being purged.
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- * The field whose values are bing purged.
- */
- abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
- /**
- * {@inheritdoc}
- */
- public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {}
- /**
- * {@inheritdoc}
- */
- public function loadRevision($revision_id) {
- $revisions = $this->loadMultipleRevisions([$revision_id]);
- return isset($revisions[$revision_id]) ? $revisions[$revision_id] : NULL;
- }
- /**
- * {@inheritdoc}
- */
- public function loadMultipleRevisions(array $revision_ids) {
- $revisions = $this->doLoadMultipleRevisionsFieldItems($revision_ids);
- // The hooks are executed with an array of entities keyed by the entity ID.
- // As we could load multiple revisions for the same entity ID at once we
- // have to build groups of entities where the same entity ID is present only
- // once.
- $entity_groups = [];
- $entity_group_mapping = [];
- foreach ($revisions as $revision) {
- $entity_id = $revision->id();
- $entity_group_key = isset($entity_group_mapping[$entity_id]) ? $entity_group_mapping[$entity_id] + 1 : 0;
- $entity_group_mapping[$entity_id] = $entity_group_key;
- $entity_groups[$entity_group_key][$entity_id] = $revision;
- }
- // Invoke the entity hooks for each group.
- foreach ($entity_groups as $entities) {
- $this->invokeStorageLoadHook($entities);
- $this->postLoad($entities);
- }
- return $revisions;
- }
- /**
- * Actually loads revision field item values from the storage.
- *
- * @param int|string $revision_id
- * The revision identifier.
- *
- * @return \Drupal\Core\Entity\EntityInterface|null
- * The specified entity revision or NULL if not found.
- *
- * @deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0.
- * \Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()
- * should be implemented instead.
- *
- * @see https://www.drupal.org/node/2924915
- */
- abstract protected function doLoadRevisionFieldItems($revision_id);
- /**
- * Actually loads revision field item values from the storage.
- *
- * @param array $revision_ids
- * An array of revision identifiers.
- *
- * @return \Drupal\Core\Entity\EntityInterface[]
- * The specified entity revisions or an empty array if none are found.
- */
- protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
- $revisions = [];
- foreach ($revision_ids as $revision_id) {
- $revisions[] = $this->doLoadRevisionFieldItems($revision_id);
- }
- return $revisions;
- }
- /**
- * {@inheritdoc}
- */
- protected function doSave($id, EntityInterface $entity) {
- /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
- if ($entity->isNew()) {
- // Ensure the entity is still seen as new after assigning it an id, while
- // storing its data.
- $entity->enforceIsNew();
- if ($this->entityType->isRevisionable()) {
- $entity->setNewRevision();
- }
- $return = SAVED_NEW;
- }
- else {
- // @todo Consider returning a different value when saving a non-default
- // entity revision. See https://www.drupal.org/node/2509360.
- $return = $entity->isDefaultRevision() ? SAVED_UPDATED : FALSE;
- }
- $this->populateAffectedRevisionTranslations($entity);
- // Populate the "revision_default" flag. We skip this when we are resaving
- // the revision because this is only allowed for default revisions, and
- // these cannot be made non-default.
- if ($this->entityType->isRevisionable() && $entity->isNewRevision()) {
- $revision_default_key = $this->entityType->getRevisionMetadataKey('revision_default');
- $entity->set($revision_default_key, $entity->isDefaultRevision());
- }
- $this->doSaveFieldItems($entity);
- return $return;
- }
- /**
- * Writes entity field values to the storage.
- *
- * This method is responsible for allocating entity and revision identifiers
- * and updating the entity object with their values.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity object.
- * @param string[] $names
- * (optional) The name of the fields to be written to the storage. If an
- * empty value is passed all field values are saved.
- */
- abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
- /**
- * {@inheritdoc}
- */
- protected function doPreSave(EntityInterface $entity) {
- /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
- // Sync the changes made in the fields array to the internal values array.
- $entity->updateOriginalValues();
- if ($entity->getEntityType()->isRevisionable() && !$entity->isNew() && empty($entity->getLoadedRevisionId())) {
- // Update the loaded revision id for rare special cases when no loaded
- // revision is given when updating an existing entity. This for example
- // happens when calling save() in hook_entity_insert().
- $entity->updateLoadedRevisionId();
- }
- $id = parent::doPreSave($entity);
- if (!$entity->isNew()) {
- // If the ID changed then original can't be loaded, throw an exception
- // in that case.
- if (empty($entity->original) || $entity->id() != $entity->original->id()) {
- throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity while changing the ID is not supported.");
- }
- // Do not allow changing the revision ID when resaving the current
- // revision.
- if (!$entity->isNewRevision() && $entity->getRevisionId() != $entity->getLoadedRevisionId()) {
- throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity revision while changing the revision ID is not supported.");
- }
- }
- return $id;
- }
- /**
- * {@inheritdoc}
- */
- protected function doPostSave(EntityInterface $entity, $update) {
- /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
- if ($update && $this->entityType->isTranslatable()) {
- $this->invokeTranslationHooks($entity);
- }
- parent::doPostSave($entity, $update);
- // The revision is stored, it should no longer be marked as new now.
- if ($this->entityType->isRevisionable()) {
- $entity->updateLoadedRevisionId();
- $entity->setNewRevision(FALSE);
- }
- }
- /**
- * {@inheritdoc}
- */
- protected function doDelete($entities) {
- /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
- foreach ($entities as $entity) {
- $this->invokeFieldMethod('delete', $entity);
- }
- $this->doDeleteFieldItems($entities);
- }
- /**
- * Deletes entity field values from the storage.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
- * An array of entity objects to be deleted.
- */
- abstract protected function doDeleteFieldItems($entities);
- /**
- * {@inheritdoc}
- */
- public function deleteRevision($revision_id) {
- /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
- if ($revision = $this->loadRevision($revision_id)) {
- // Prevent deletion if this is the default revision.
- if ($revision->isDefaultRevision()) {
- throw new EntityStorageException('Default revision can not be deleted');
- }
- $this->invokeFieldMethod('deleteRevision', $revision);
- $this->doDeleteRevisionFieldItems($revision);
- $this->invokeHook('revision_delete', $revision);
- }
- }
- /**
- * Deletes field values of an entity revision from the storage.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $revision
- * An entity revision object to be deleted.
- */
- abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
- /**
- * Checks translation statuses and invoke the related hooks if needed.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity being saved.
- */
- protected function invokeTranslationHooks(ContentEntityInterface $entity) {
- $translations = $entity->getTranslationLanguages(FALSE);
- $original_translations = $entity->original->getTranslationLanguages(FALSE);
- $all_translations = array_keys($translations + $original_translations);
- // Notify modules of translation insertion/deletion.
- foreach ($all_translations as $langcode) {
- if (isset($translations[$langcode]) && !isset($original_translations[$langcode])) {
- $this->invokeHook('translation_insert', $entity->getTranslation($langcode));
- }
- elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
- $this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
- }
- }
- }
- /**
- * Invokes hook_entity_storage_load().
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
- * List of entities, keyed on the entity ID.
- */
- protected function invokeStorageLoadHook(array &$entities) {
- if (!empty($entities)) {
- // Call hook_entity_storage_load().
- foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
- $function = $module . '_entity_storage_load';
- $function($entities, $this->entityTypeId);
- }
- // Call hook_TYPE_storage_load().
- foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
- $function = $module . '_' . $this->entityTypeId . '_storage_load';
- $function($entities);
- }
- }
- }
- /**
- * {@inheritdoc}
- */
- protected function invokeHook($hook, EntityInterface $entity) {
- /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
- switch ($hook) {
- case 'presave':
- $this->invokeFieldMethod('preSave', $entity);
- break;
- case 'insert':
- $this->invokeFieldPostSave($entity, FALSE);
- break;
- case 'update':
- $this->invokeFieldPostSave($entity, TRUE);
- break;
- }
- parent::invokeHook($hook, $entity);
- }
- /**
- * Invokes a method on the Field objects within an entity.
- *
- * Any argument passed will be forwarded to the invoked method.
- *
- * @param string $method
- * The name of the method to be invoked.
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity object.
- *
- * @return array
- * A multidimensional associative array of results, keyed by entity
- * translation language code and field name.
- */
- protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
- $result = [];
- $args = array_slice(func_get_args(), 2);
- $langcodes = array_keys($entity->getTranslationLanguages());
- // Ensure that the field method is invoked as first on the current entity
- // translation and then on all other translations.
- $current_entity_langcode = $entity->language()->getId();
- if (reset($langcodes) != $current_entity_langcode) {
- $langcodes = array_diff($langcodes, [$current_entity_langcode]);
- array_unshift($langcodes, $current_entity_langcode);
- }
- foreach ($langcodes as $langcode) {
- $translation = $entity->getTranslation($langcode);
- // For non translatable fields, there is only one field object instance
- // across all translations and it has as parent entity the entity in the
- // default entity translation. Therefore field methods on non translatable
- // fields should be invoked only on the default entity translation.
- $fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields();
- foreach ($fields as $name => $items) {
- // call_user_func_array() is way slower than a direct call so we avoid
- // using it if have no parameters.
- $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}();
- }
- }
- // We need to call the delete method for field items of removed
- // translations.
- if ($method == 'postSave' && !empty($entity->original)) {
- $original_langcodes = array_keys($entity->original->getTranslationLanguages());
- foreach (array_diff($original_langcodes, $langcodes) as $removed_langcode) {
- $translation = $entity->original->getTranslation($removed_langcode);
- $fields = $translation->getTranslatableFields();
- foreach ($fields as $name => $items) {
- $items->delete();
- }
- }
- }
- return $result;
- }
- /**
- * Invokes the post save method on the Field objects within an entity.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity object.
- * @param bool $update
- * Specifies whether the entity is being updated or created.
- */
- protected function invokeFieldPostSave(ContentEntityInterface $entity, $update) {
- // For each entity translation this returns an array of resave flags keyed
- // by field name, thus we merge them to obtain a list of fields to resave.
- $resave = [];
- foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $translation_results) {
- $resave += array_filter($translation_results);
- }
- if ($resave) {
- $this->doSaveFieldItems($entity, array_keys($resave));
- }
- }
- /**
- * Checks whether the field values changed compared to the original entity.
- *
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- * Field definition of field to compare for changes.
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * Entity to check for field changes.
- * @param \Drupal\Core\Entity\ContentEntityInterface $original
- * Original entity to compare against.
- *
- * @return bool
- * True if the field value changed from the original entity.
- */
- protected function hasFieldValueChanged(FieldDefinitionInterface $field_definition, ContentEntityInterface $entity, ContentEntityInterface $original) {
- $field_name = $field_definition->getName();
- $langcodes = array_keys($entity->getTranslationLanguages());
- if ($langcodes !== array_keys($original->getTranslationLanguages())) {
- // If the list of langcodes has changed, we need to save.
- return TRUE;
- }
- foreach ($langcodes as $langcode) {
- $items = $entity->getTranslation($langcode)->get($field_name)->filterEmptyItems();
- $original_items = $original->getTranslation($langcode)->get($field_name)->filterEmptyItems();
- // If the field items are not equal, we need to save.
- if (!$items->equals($original_items)) {
- return TRUE;
- }
- }
- return FALSE;
- }
- /**
- * Populates the affected flag for all the revision translations.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * An entity object being saved.
- */
- protected function populateAffectedRevisionTranslations(ContentEntityInterface $entity) {
- if ($this->entityType->isTranslatable() && $this->entityType->isRevisionable()) {
- $languages = $entity->getTranslationLanguages();
- foreach ($languages as $langcode => $language) {
- $translation = $entity->getTranslation($langcode);
- $current_affected = $translation->isRevisionTranslationAffected();
- if (!isset($current_affected) || ($entity->isNewRevision() && !$translation->isRevisionTranslationAffectedEnforced())) {
- // When setting the revision translation affected flag we have to
- // explicitly set it to not be enforced. By default it will be
- // enforced automatically when being set, which allows us to determine
- // if the flag has been already set outside the storage in which case
- // we should not recompute it.
- // @see \Drupal\Core\Entity\ContentEntityBase::setRevisionTranslationAffected().
- $new_affected = $translation->hasTranslationChanges() ? TRUE : NULL;
- $translation->setRevisionTranslationAffected($new_affected);
- $translation->setRevisionTranslationAffectedEnforced(FALSE);
- }
- }
- }
- }
- /**
- * Ensures integer entity key values are valid.
- *
- * The identifier sanitization provided by this method has been introduced
- * as Drupal used to rely on the database to facilitate this, which worked
- * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
- *
- * @param array $ids
- * The entity key values to verify.
- * @param string $entity_key
- * (optional) The entity key to sanitise values for. Defaults to 'id'.
- *
- * @return array
- * The sanitized list of entity key values.
- */
- protected function cleanIds(array $ids, $entity_key = 'id') {
- $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
- $field_name = $this->entityType->getKey($entity_key);
- if ($field_name && $definitions[$field_name]->getType() == 'integer') {
- $ids = array_filter($ids, function ($id) {
- return is_numeric($id) && $id == (int) $id;
- });
- $ids = array_map('intval', $ids);
- }
- return $ids;
- }
- /**
- * Gets entities from the persistent cache backend.
- *
- * @param array|null &$ids
- * If not empty, return entities that match these IDs. IDs that were found
- * will be removed from the list.
- *
- * @return \Drupal\Core\Entity\ContentEntityInterface[]
- * Array of entities from the persistent cache.
- */
- protected function getFromPersistentCache(array &$ids = NULL) {
- if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
- return [];
- }
- $entities = [];
- // Build the list of cache entries to retrieve.
- $cid_map = [];
- foreach ($ids as $id) {
- $cid_map[$id] = $this->buildCacheId($id);
- }
- $cids = array_values($cid_map);
- if ($cache = $this->cacheBackend->getMultiple($cids)) {
- // Get the entities that were found in the cache.
- foreach ($ids as $index => $id) {
- $cid = $cid_map[$id];
- if (isset($cache[$cid])) {
- $entities[$id] = $cache[$cid]->data;
- unset($ids[$index]);
- }
- }
- }
- return $entities;
- }
- /**
- * Stores entities in the persistent cache backend.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
- * Entities to store in the cache.
- */
- protected function setPersistentCache($entities) {
- if (!$this->entityType->isPersistentlyCacheable()) {
- return;
- }
- $cache_tags = [
- $this->entityTypeId . '_values',
- 'entity_field_info',
- ];
- foreach ($entities as $id => $entity) {
- $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
- }
- }
- /**
- * {@inheritdoc}
- */
- public function loadUnchanged($id) {
- $ids = [$id];
- // The cache invalidation in the parent has the side effect that loading the
- // same entity again during the save process (for example in
- // hook_entity_presave()) will load the unchanged entity. Simulate this
- // by explicitly removing the entity from the static cache.
- parent::resetCache($ids);
- // The default implementation in the parent class unsets the current cache
- // and then reloads the entity. That is slow, especially if this is done
- // repeatedly in the same request, e.g. when validating and then saving
- // an entity. Optimize this for content entities by trying to load them
- // directly from the persistent cache again, as in contrast to the static
- // cache the persistent one will never be changed until the entity is saved.
- $entities = $this->getFromPersistentCache($ids);
- if (!$entities) {
- $entities[$id] = $this->load($id);
- }
- else {
- // As the entities are put into the persistent cache before the post load
- // has been executed we have to execute it if we have retrieved the
- // entity directly from the persistent cache.
- $this->postLoad($entities);
- if ($this->entityType->isStaticallyCacheable()) {
- // As we've removed the entity from the static cache already we have to
- // put the loaded unchanged entity there to simulate the behavior of the
- // parent.
- $this->setStaticCache($entities);
- }
- }
- return $entities[$id];
- }
- /**
- * {@inheritdoc}
- */
- public function resetCache(array $ids = NULL) {
- if ($ids) {
- $cids = [];
- foreach ($ids as $id) {
- unset($this->entities[$id]);
- $cids[] = $this->buildCacheId($id);
- }
- if ($this->entityType->isPersistentlyCacheable()) {
- $this->cacheBackend->deleteMultiple($cids);
- }
- }
- else {
- $this->entities = [];
- if ($this->entityType->isPersistentlyCacheable()) {
- Cache::invalidateTags([$this->entityTypeId . '_values']);
- }
- }
- }
- /**
- * Builds the cache ID for the passed in entity ID.
- *
- * @param int $id
- * Entity ID for which the cache ID should be built.
- *
- * @return string
- * Cache ID that can be passed to the cache backend.
- */
- protected function buildCacheId($id) {
- return "values:{$this->entityTypeId}:$id";
- }
- }
|