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 Symfony\Component\DependencyInjection\ContainerInterface;
12 * Base class for content entity storage handlers.
14 abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
17 * The entity bundle key.
21 protected $bundleKey = FALSE;
26 * @var \Drupal\Core\Entity\EntityManagerInterface
28 protected $entityManager;
33 * @var \Drupal\Core\Cache\CacheBackendInterface
35 protected $cacheBackend;
38 * Constructs a ContentEntityStorageBase object.
40 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
41 * The entity type definition.
42 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
44 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
45 * The cache backend to be used.
47 public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
48 parent::__construct($entity_type);
49 $this->bundleKey = $this->entityType->getKey('bundle');
50 $this->entityManager = $entity_manager;
51 $this->cacheBackend = $cache;
57 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
60 $container->get('entity.manager'),
61 $container->get('cache.entity')
68 protected function doCreate(array $values) {
69 // We have to determine the bundle first.
71 if ($this->bundleKey) {
72 if (!isset($values[$this->bundleKey])) {
73 throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
75 $bundle = $values[$this->bundleKey];
77 $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
78 $this->initFieldValues($entity, $values);
83 * Initializes field values.
85 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
87 * @param array $values
88 * (optional) An associative array of initial field values keyed by field
89 * name. If none is provided default values will be applied.
90 * @param array $field_names
91 * (optional) An associative array of field names to be initialized. If none
92 * is provided all fields will be initialized.
94 protected function initFieldValues(ContentEntityInterface $entity, array $values = [], array $field_names = []) {
95 // Populate field values.
96 foreach ($entity as $name => $field) {
97 if (!$field_names || isset($field_names[$name])) {
98 if (isset($values[$name])) {
99 $entity->$name = $values[$name];
101 elseif (!array_key_exists($name, $values)) {
102 $entity->get($name)->applyDefaultValue();
105 unset($values[$name]);
108 // Set any passed values for non-defined fields also.
109 foreach ($values as $name => $value) {
110 $entity->$name = $value;
113 // Make sure modules can alter field initial values.
114 $this->invokeHook('field_values_init', $entity);
120 public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
121 $translation = $entity->getTranslation($langcode);
122 $definitions = array_filter($translation->getFieldDefinitions(), function (FieldDefinitionInterface $definition) {
123 return $definition->isTranslatable();
125 $field_names = array_map(function (FieldDefinitionInterface $definition) {
126 return $definition->getName();
128 $values[$this->langcodeKey] = $langcode;
129 $values[$this->getEntityType()->getKey('default_langcode')] = FALSE;
130 $this->initFieldValues($translation, $values, $field_names);
131 $this->invokeHook('translation_create', $translation);
138 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {}
143 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {}
148 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {}
153 public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) {}
158 public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) {}
163 public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {}
168 public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
169 $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
171 foreach ($items_by_entity as $items) {
173 $this->purgeFieldItems($items->getEntity(), $field_definition);
175 return count($items_by_entity);
179 * Reads values to be purged for a single field.
181 * This method is called during field data purge, on fields for which
182 * onFieldDefinitionDelete() has previously run.
184 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
185 * The field definition.
187 * The maximum number of field data records to purge before returning.
189 * @return \Drupal\Core\Field\FieldItemListInterface[]
190 * An array of field item lists, keyed by entity revision id.
192 abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
195 * Removes field items from storage per entity during purge.
197 * @param ContentEntityInterface $entity
198 * The entity revision, whose values are being purged.
199 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
200 * The field whose values are bing purged.
202 abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
207 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {}
212 public function loadRevision($revision_id) {
213 $revision = $this->doLoadRevisionFieldItems($revision_id);
216 $entities = [$revision->id() => $revision];
217 $this->invokeStorageLoadHook($entities);
218 $this->postLoad($entities);
225 * Actually loads revision field item values from the storage.
227 * @param int|string $revision_id
228 * The revision identifier.
230 * @return \Drupal\Core\Entity\EntityInterface|null
231 * The specified entity revision or NULL if not found.
233 abstract protected function doLoadRevisionFieldItems($revision_id);
238 protected function doSave($id, EntityInterface $entity) {
239 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
241 if ($entity->isNew()) {
242 // Ensure the entity is still seen as new after assigning it an id, while
244 $entity->enforceIsNew();
245 if ($this->entityType->isRevisionable()) {
246 $entity->setNewRevision();
251 // @todo Consider returning a different value when saving a non-default
252 // entity revision. See https://www.drupal.org/node/2509360.
253 $return = $entity->isDefaultRevision() ? SAVED_UPDATED : FALSE;
256 $this->populateAffectedRevisionTranslations($entity);
257 $this->doSaveFieldItems($entity);
263 * Writes entity field values to the storage.
265 * This method is responsible for allocating entity and revision identifiers
266 * and updating the entity object with their values.
268 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
270 * @param string[] $names
271 * (optional) The name of the fields to be written to the storage. If an
272 * empty value is passed all field values are saved.
274 abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
279 protected function doPreSave(EntityInterface $entity) {
280 /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
282 // Sync the changes made in the fields array to the internal values array.
283 $entity->updateOriginalValues();
285 if ($entity->getEntityType()->isRevisionable() && !$entity->isNew() && empty($entity->getLoadedRevisionId())) {
286 // Update the loaded revision id for rare special cases when no loaded
287 // revision is given when updating an existing entity. This for example
288 // happens when calling save() in hook_entity_insert().
289 $entity->updateLoadedRevisionId();
292 $id = parent::doPreSave($entity);
294 if (!$entity->isNew()) {
295 // If the ID changed then original can't be loaded, throw an exception
297 if (empty($entity->original) || $entity->id() != $entity->original->id()) {
298 throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity while changing the ID is not supported.");
300 // Do not allow changing the revision ID when resaving the current
302 if (!$entity->isNewRevision() && $entity->getRevisionId() != $entity->getLoadedRevisionId()) {
303 throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity revision while changing the revision ID is not supported.");
313 protected function doPostSave(EntityInterface $entity, $update) {
314 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
316 if ($update && $this->entityType->isTranslatable()) {
317 $this->invokeTranslationHooks($entity);
320 parent::doPostSave($entity, $update);
322 // The revision is stored, it should no longer be marked as new now.
323 if ($this->entityType->isRevisionable()) {
324 $entity->updateLoadedRevisionId();
325 $entity->setNewRevision(FALSE);
332 protected function doDelete($entities) {
333 /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
334 foreach ($entities as $entity) {
335 $this->invokeFieldMethod('delete', $entity);
337 $this->doDeleteFieldItems($entities);
341 * Deletes entity field values from the storage.
343 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
344 * An array of entity objects to be deleted.
346 abstract protected function doDeleteFieldItems($entities);
351 public function deleteRevision($revision_id) {
352 /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
353 if ($revision = $this->loadRevision($revision_id)) {
354 // Prevent deletion if this is the default revision.
355 if ($revision->isDefaultRevision()) {
356 throw new EntityStorageException('Default revision can not be deleted');
358 $this->invokeFieldMethod('deleteRevision', $revision);
359 $this->doDeleteRevisionFieldItems($revision);
360 $this->invokeHook('revision_delete', $revision);
365 * Deletes field values of an entity revision from the storage.
367 * @param \Drupal\Core\Entity\ContentEntityInterface $revision
368 * An entity revision object to be deleted.
370 abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
373 * Checks translation statuses and invoke the related hooks if needed.
375 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
376 * The entity being saved.
378 protected function invokeTranslationHooks(ContentEntityInterface $entity) {
379 $translations = $entity->getTranslationLanguages(FALSE);
380 $original_translations = $entity->original->getTranslationLanguages(FALSE);
381 $all_translations = array_keys($translations + $original_translations);
383 // Notify modules of translation insertion/deletion.
384 foreach ($all_translations as $langcode) {
385 if (isset($translations[$langcode]) && !isset($original_translations[$langcode])) {
386 $this->invokeHook('translation_insert', $entity->getTranslation($langcode));
388 elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
389 $this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
395 * Invokes hook_entity_storage_load().
397 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
398 * List of entities, keyed on the entity ID.
400 protected function invokeStorageLoadHook(array &$entities) {
401 if (!empty($entities)) {
402 // Call hook_entity_storage_load().
403 foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
404 $function = $module . '_entity_storage_load';
405 $function($entities, $this->entityTypeId);
407 // Call hook_TYPE_storage_load().
408 foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
409 $function = $module . '_' . $this->entityTypeId . '_storage_load';
410 $function($entities);
418 protected function invokeHook($hook, EntityInterface $entity) {
419 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
423 $this->invokeFieldMethod('preSave', $entity);
427 $this->invokeFieldPostSave($entity, FALSE);
431 $this->invokeFieldPostSave($entity, TRUE);
435 parent::invokeHook($hook, $entity);
439 * Invokes a method on the Field objects within an entity.
441 * Any argument passed will be forwarded to the invoked method.
443 * @param string $method
444 * The name of the method to be invoked.
445 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
449 * A multidimensional associative array of results, keyed by entity
450 * translation language code and field name.
452 protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
454 $args = array_slice(func_get_args(), 2);
455 $langcodes = array_keys($entity->getTranslationLanguages());
456 // Ensure that the field method is invoked as first on the current entity
457 // translation and then on all other translations.
458 $current_entity_langcode = $entity->language()->getId();
459 if (reset($langcodes) != $current_entity_langcode) {
460 $langcodes = array_diff($langcodes, [$current_entity_langcode]);
461 array_unshift($langcodes, $current_entity_langcode);
463 foreach ($langcodes as $langcode) {
464 $translation = $entity->getTranslation($langcode);
465 // For non translatable fields, there is only one field object instance
466 // across all translations and it has as parent entity the entity in the
467 // default entity translation. Therefore field methods on non translatable
468 // fields should be invoked only on the default entity translation.
469 $fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields();
470 foreach ($fields as $name => $items) {
471 // call_user_func_array() is way slower than a direct call so we avoid
472 // using it if have no parameters.
473 $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}();
477 // We need to call the delete method for field items of removed
479 if ($method == 'postSave' && !empty($entity->original)) {
480 $original_langcodes = array_keys($entity->original->getTranslationLanguages());
481 foreach (array_diff($original_langcodes, $langcodes) as $removed_langcode) {
482 $translation = $entity->original->getTranslation($removed_langcode);
483 $fields = $translation->getTranslatableFields();
484 foreach ($fields as $name => $items) {
494 * Invokes the post save method on the Field objects within an entity.
496 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
498 * @param bool $update
499 * Specifies whether the entity is being updated or created.
501 protected function invokeFieldPostSave(ContentEntityInterface $entity, $update) {
502 // For each entity translation this returns an array of resave flags keyed
503 // by field name, thus we merge them to obtain a list of fields to resave.
505 foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $translation_results) {
506 $resave += array_filter($translation_results);
509 $this->doSaveFieldItems($entity, array_keys($resave));
514 * Checks whether the field values changed compared to the original entity.
516 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
517 * Field definition of field to compare for changes.
518 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
519 * Entity to check for field changes.
520 * @param \Drupal\Core\Entity\ContentEntityInterface $original
521 * Original entity to compare against.
524 * True if the field value changed from the original entity.
526 protected function hasFieldValueChanged(FieldDefinitionInterface $field_definition, ContentEntityInterface $entity, ContentEntityInterface $original) {
527 $field_name = $field_definition->getName();
528 $langcodes = array_keys($entity->getTranslationLanguages());
529 if ($langcodes !== array_keys($original->getTranslationLanguages())) {
530 // If the list of langcodes has changed, we need to save.
533 foreach ($langcodes as $langcode) {
534 $items = $entity->getTranslation($langcode)->get($field_name)->filterEmptyItems();
535 $original_items = $original->getTranslation($langcode)->get($field_name)->filterEmptyItems();
536 // If the field items are not equal, we need to save.
537 if (!$items->equals($original_items)) {
546 * Populates the affected flag for all the revision translations.
548 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
549 * An entity object being saved.
551 protected function populateAffectedRevisionTranslations(ContentEntityInterface $entity) {
552 if ($this->entityType->isTranslatable() && $this->entityType->isRevisionable()) {
553 $languages = $entity->getTranslationLanguages();
554 foreach ($languages as $langcode => $language) {
555 $translation = $entity->getTranslation($langcode);
556 // Avoid populating the value if it was already manually set.
557 $affected = $translation->isRevisionTranslationAffected();
558 if (!isset($affected) && $translation->hasTranslationChanges()) {
559 $translation->setRevisionTranslationAffected(TRUE);
566 * Ensures integer entity IDs are valid.
568 * The identifier sanitization provided by this method has been introduced
569 * as Drupal used to rely on the database to facilitate this, which worked
570 * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
573 * The entity IDs to verify.
576 * The sanitized list of entity IDs.
578 protected function cleanIds(array $ids) {
579 $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
580 $id_definition = $definitions[$this->entityType->getKey('id')];
581 if ($id_definition->getType() == 'integer') {
582 $ids = array_filter($ids, function ($id) {
583 return is_numeric($id) && $id == (int) $id;
585 $ids = array_map('intval', $ids);
591 * Gets entities from the persistent cache backend.
593 * @param array|null &$ids
594 * If not empty, return entities that match these IDs. IDs that were found
595 * will be removed from the list.
597 * @return \Drupal\Core\Entity\ContentEntityInterface[]
598 * Array of entities from the persistent cache.
600 protected function getFromPersistentCache(array &$ids = NULL) {
601 if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
605 // Build the list of cache entries to retrieve.
607 foreach ($ids as $id) {
608 $cid_map[$id] = $this->buildCacheId($id);
610 $cids = array_values($cid_map);
611 if ($cache = $this->cacheBackend->getMultiple($cids)) {
612 // Get the entities that were found in the cache.
613 foreach ($ids as $index => $id) {
614 $cid = $cid_map[$id];
615 if (isset($cache[$cid])) {
616 $entities[$id] = $cache[$cid]->data;
625 * Stores entities in the persistent cache backend.
627 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
628 * Entities to store in the cache.
630 protected function setPersistentCache($entities) {
631 if (!$this->entityType->isPersistentlyCacheable()) {
636 $this->entityTypeId . '_values',
639 foreach ($entities as $id => $entity) {
640 $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
647 public function loadUnchanged($id) {
650 // The cache invalidation in the parent has the side effect that loading the
651 // same entity again during the save process (for example in
652 // hook_entity_presave()) will load the unchanged entity. Simulate this
653 // by explicitly removing the entity from the static cache.
654 parent::resetCache($ids);
656 // The default implementation in the parent class unsets the current cache
657 // and then reloads the entity. That is slow, especially if this is done
658 // repeatedly in the same request, e.g. when validating and then saving
659 // an entity. Optimize this for content entities by trying to load them
660 // directly from the persistent cache again, as in contrast to the static
661 // cache the persistent one will never be changed until the entity is saved.
662 $entities = $this->getFromPersistentCache($ids);
665 $entities[$id] = $this->load($id);
668 // As the entities are put into the persistent cache before the post load
669 // has been executed we have to execute it if we have retrieved the
670 // entity directly from the persistent cache.
671 $this->postLoad($entities);
673 if ($this->entityType->isStaticallyCacheable()) {
674 // As we've removed the entity from the static cache already we have to
675 // put the loaded unchanged entity there to simulate the behavior of the
677 $this->setStaticCache($entities);
681 return $entities[$id];
687 public function resetCache(array $ids = NULL) {
690 foreach ($ids as $id) {
691 unset($this->entities[$id]);
692 $cids[] = $this->buildCacheId($id);
694 if ($this->entityType->isPersistentlyCacheable()) {
695 $this->cacheBackend->deleteMultiple($cids);
699 $this->entities = [];
700 if ($this->entityType->isPersistentlyCacheable()) {
701 Cache::invalidateTags([$this->entityTypeId . '_values']);
707 * Builds the cache ID for the passed in entity ID.
710 * Entity ID for which the cache ID should be built.
713 * Cache ID that can be passed to the cache backend.
715 protected function buildCacheId($id) {
716 return "values:{$this->entityTypeId}:$id";