Newer
Older
<?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;
Alex Bronstein
committed
/**
* 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;
catch
committed
* Constructs a ContentEntityStorageBase object.
Alex Pott
committed
* @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) {
Alex Pott
committed
parent::__construct($entity_type);
$this->bundleKey = $this->entityType->getKey('bundle');
$this->entityManager = $entity_manager;
$this->cacheBackend = $cache;
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager'),
$container->get('cache.entity')
);
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
protected function doCreate(array $values) {
// We have to determine the bundle first.
$bundle = FALSE;
if ($this->bundleKey) {
if (!isset($values[$this->bundleKey])) {
catch
committed
throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
}
$bundle = $values[$this->bundleKey];
}
$entity = new $this->entityClass([], $this->entityTypeId, $bundle);
Alex Bronstein
committed
$this->initFieldValues($entity, $values);
return $entity;
}
Lee Rowlands
committed
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* {@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;
}
Alex Bronstein
committed
/**
* 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) {
Alex Bronstein
committed
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;
}
Alex Bronstein
committed
// Make sure modules can alter field initial values.
$this->invokeHook('field_values_init', $entity);
}
/**
* Checks whether any entity revision is translated.
*
Alex Bronstein
committed
* @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()
Alex Bronstein
committed
* @see \Drupal\Core\Entity\ContentEntityStorageBase::isAnyRevisionTranslated()
*/
Alex Bronstein
committed
protected function isAnyStoredRevisionTranslated(TranslatableInterface $entity) {
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/** @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);
}
Alex Bronstein
committed
/**
* {@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);
Alex Bronstein
committed
$values[$this->langcodeKey] = $langcode;
$values[$this->getEntityType()->getKey('default_langcode')] = FALSE;
$this->initFieldValues($translation, $values, $field_names);
$this->invokeHook('translation_create', $translation);
Alex Bronstein
committed
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());
Gabor Hojtsy
committed
// 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)) {
Gabor Hojtsy
committed
$keep_untranslatable_fields = $entity->isDefaultTranslation() && $entity->isDefaultTranslationAffectedOnly();
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
$default_revision = $this->load($entity->id());
catch
committed
$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;
}
Gabor Hojtsy
committed
catch
committed
// 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);
}
}
Gabor Hojtsy
committed
// 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;
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
}
// 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;
}
Alex Bronstein
committed
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
/**
* {@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);
Alex Bronstein
committed
// 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());
}
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
$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();
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
$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);
catch
committed
/**
* 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])) {
Alex Pott
committed
$this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
catch
committed
}
}
}
/**
* 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);
}
catch
committed
/**
* Invokes a method on the Field objects within an entity.
*
* Any argument passed will be forwarded to the invoked method.
*
catch
committed
* @param string $method
* The name of the method to be invoked.
catch
committed
* @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.
catch
committed
*/
protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
$result = [];
$args = array_slice(func_get_args(), 2);
catch
committed
$langcodes = array_keys($entity->getTranslationLanguages());
Alex Pott
committed
// 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);
}
catch
committed
foreach ($langcodes as $langcode) {
catch
committed
$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}();
catch
committed
}
}
catch
committed
// 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));
}
catch
committed
}
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
/**
* 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;
}
catch
committed
/**
* 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);
catch
committed
}
}
}
}
/**
* 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 = [];
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
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);
}
}
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
/**
* {@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);
}