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 public function hasData() {
69 return (bool) $this->getQuery()
78 protected function doCreate(array $values) {
79 // We have to determine the bundle first.
81 if ($this->bundleKey) {
82 if (!isset($values[$this->bundleKey])) {
83 throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
85 $bundle = $values[$this->bundleKey];
87 $entity = new $this->entityClass([], $this->entityTypeId, $bundle);
88 $this->initFieldValues($entity, $values);
93 * Initializes field values.
95 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
97 * @param array $values
98 * (optional) An associative array of initial field values keyed by field
99 * name. If none is provided default values will be applied.
100 * @param array $field_names
101 * (optional) An associative array of field names to be initialized. If none
102 * is provided all fields will be initialized.
104 protected function initFieldValues(ContentEntityInterface $entity, array $values = [], array $field_names = []) {
105 // Populate field values.
106 foreach ($entity as $name => $field) {
107 if (!$field_names || isset($field_names[$name])) {
108 if (isset($values[$name])) {
109 $entity->$name = $values[$name];
111 elseif (!array_key_exists($name, $values)) {
112 $entity->get($name)->applyDefaultValue();
115 unset($values[$name]);
118 // Set any passed values for non-defined fields also.
119 foreach ($values as $name => $value) {
120 $entity->$name = $value;
123 // Make sure modules can alter field initial values.
124 $this->invokeHook('field_values_init', $entity);
130 public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
131 $translation = $entity->getTranslation($langcode);
132 $definitions = array_filter($translation->getFieldDefinitions(), function(FieldDefinitionInterface $definition) { return $definition->isTranslatable(); });
133 $field_names = array_map(function(FieldDefinitionInterface $definition) { return $definition->getName(); }, $definitions);
134 $values[$this->langcodeKey] = $langcode;
135 $values[$this->getEntityType()->getKey('default_langcode')] = FALSE;
136 $this->initFieldValues($translation, $values, $field_names);
137 $this->invokeHook('translation_create', $translation);
144 public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { }
149 public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { }
154 public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { }
159 public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definition) { }
164 public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original) { }
169 public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) { }
174 public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size) {
175 $items_by_entity = $this->readFieldItemsToPurge($field_definition, $batch_size);
177 foreach ($items_by_entity as $items) {
179 $this->purgeFieldItems($items->getEntity(), $field_definition);
181 return count($items_by_entity);
185 * Reads values to be purged for a single field.
187 * This method is called during field data purge, on fields for which
188 * onFieldDefinitionDelete() has previously run.
190 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
191 * The field definition.
193 * The maximum number of field data records to purge before returning.
195 * @return \Drupal\Core\Field\FieldItemListInterface[]
196 * An array of field item lists, keyed by entity revision id.
198 abstract protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size);
201 * Removes field items from storage per entity during purge.
203 * @param ContentEntityInterface $entity
204 * The entity revision, whose values are being purged.
205 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
206 * The field whose values are bing purged.
208 abstract protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition);
213 public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { }
218 public function loadRevision($revision_id) {
219 $revision = $this->doLoadRevisionFieldItems($revision_id);
222 $entities = [$revision->id() => $revision];
223 $this->invokeStorageLoadHook($entities);
224 $this->postLoad($entities);
231 * Actually loads revision field item values from the storage.
233 * @param int|string $revision_id
234 * The revision identifier.
236 * @return \Drupal\Core\Entity\EntityInterface|null
237 * The specified entity revision or NULL if not found.
239 abstract protected function doLoadRevisionFieldItems($revision_id);
244 protected function doSave($id, EntityInterface $entity) {
245 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
247 if ($entity->isNew()) {
248 // Ensure the entity is still seen as new after assigning it an id, while
250 $entity->enforceIsNew();
251 if ($this->entityType->isRevisionable()) {
252 $entity->setNewRevision();
257 // @todo Consider returning a different value when saving a non-default
258 // entity revision. See https://www.drupal.org/node/2509360.
259 $return = $entity->isDefaultRevision() ? SAVED_UPDATED : FALSE;
262 $this->populateAffectedRevisionTranslations($entity);
263 $this->doSaveFieldItems($entity);
269 * Writes entity field values to the storage.
271 * This method is responsible for allocating entity and revision identifiers
272 * and updating the entity object with their values.
274 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
276 * @param string[] $names
277 * (optional) The name of the fields to be written to the storage. If an
278 * empty value is passed all field values are saved.
280 abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
285 protected function doPreSave(EntityInterface $entity) {
286 /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
288 // Sync the changes made in the fields array to the internal values array.
289 $entity->updateOriginalValues();
291 if ($entity->getEntityType()->isRevisionable() && !$entity->isNew() && empty($entity->getLoadedRevisionId())) {
292 // Update the loaded revision id for rare special cases when no loaded
293 // revision is given when updating an existing entity. This for example
294 // happens when calling save() in hook_entity_insert().
295 $entity->updateLoadedRevisionId();
298 $id = parent::doPreSave($entity);
300 if (!$entity->isNew()) {
301 // If the ID changed then original can't be loaded, throw an exception
303 if (empty($entity->original) || $entity->id() != $entity->original->id()) {
304 throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity while changing the ID is not supported.");
306 // Do not allow changing the revision ID when resaving the current
308 if (!$entity->isNewRevision() && $entity->getRevisionId() != $entity->getLoadedRevisionId()) {
309 throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity revision while changing the revision ID is not supported.");
319 protected function doPostSave(EntityInterface $entity, $update) {
320 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
322 if ($update && $this->entityType->isTranslatable()) {
323 $this->invokeTranslationHooks($entity);
326 parent::doPostSave($entity, $update);
328 // The revision is stored, it should no longer be marked as new now.
329 if ($this->entityType->isRevisionable()) {
330 $entity->updateLoadedRevisionId();
331 $entity->setNewRevision(FALSE);
338 protected function doDelete($entities) {
339 /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
340 foreach ($entities as $entity) {
341 $this->invokeFieldMethod('delete', $entity);
343 $this->doDeleteFieldItems($entities);
347 * Deletes entity field values from the storage.
349 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
350 * An array of entity objects to be deleted.
352 abstract protected function doDeleteFieldItems($entities);
357 public function deleteRevision($revision_id) {
358 /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
359 if ($revision = $this->loadRevision($revision_id)) {
360 // Prevent deletion if this is the default revision.
361 if ($revision->isDefaultRevision()) {
362 throw new EntityStorageException('Default revision can not be deleted');
364 $this->invokeFieldMethod('deleteRevision', $revision);
365 $this->doDeleteRevisionFieldItems($revision);
366 $this->invokeHook('revision_delete', $revision);
371 * Deletes field values of an entity revision from the storage.
373 * @param \Drupal\Core\Entity\ContentEntityInterface $revision
374 * An entity revision object to be deleted.
376 abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
379 * Checks translation statuses and invoke the related hooks if needed.
381 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
382 * The entity being saved.
384 protected function invokeTranslationHooks(ContentEntityInterface $entity) {
385 $translations = $entity->getTranslationLanguages(FALSE);
386 $original_translations = $entity->original->getTranslationLanguages(FALSE);
387 $all_translations = array_keys($translations + $original_translations);
389 // Notify modules of translation insertion/deletion.
390 foreach ($all_translations as $langcode) {
391 if (isset($translations[$langcode]) && !isset($original_translations[$langcode])) {
392 $this->invokeHook('translation_insert', $entity->getTranslation($langcode));
394 elseif (!isset($translations[$langcode]) && isset($original_translations[$langcode])) {
395 $this->invokeHook('translation_delete', $entity->original->getTranslation($langcode));
401 * Invokes hook_entity_storage_load().
403 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
404 * List of entities, keyed on the entity ID.
406 protected function invokeStorageLoadHook(array &$entities) {
407 if (!empty($entities)) {
408 // Call hook_entity_storage_load().
409 foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
410 $function = $module . '_entity_storage_load';
411 $function($entities, $this->entityTypeId);
413 // Call hook_TYPE_storage_load().
414 foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
415 $function = $module . '_' . $this->entityTypeId . '_storage_load';
416 $function($entities);
424 protected function invokeHook($hook, EntityInterface $entity) {
425 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
429 $this->invokeFieldMethod('preSave', $entity);
433 $this->invokeFieldPostSave($entity, FALSE);
437 $this->invokeFieldPostSave($entity, TRUE);
441 parent::invokeHook($hook, $entity);
445 * Invokes a method on the Field objects within an entity.
447 * Any argument passed will be forwarded to the invoked method.
449 * @param string $method
450 * The name of the method to be invoked.
451 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
455 * A multidimensional associative array of results, keyed by entity
456 * translation language code and field name.
458 protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
460 $args = array_slice(func_get_args(), 2);
461 $langcodes = array_keys($entity->getTranslationLanguages());
462 // Ensure that the field method is invoked as first on the current entity
463 // translation and then on all other translations.
464 $current_entity_langcode = $entity->language()->getId();
465 if (reset($langcodes) != $current_entity_langcode) {
466 $langcodes = array_diff($langcodes, [$current_entity_langcode]);
467 array_unshift($langcodes, $current_entity_langcode);
469 foreach ($langcodes as $langcode) {
470 $translation = $entity->getTranslation($langcode);
471 // For non translatable fields, there is only one field object instance
472 // across all translations and it has as parent entity the entity in the
473 // default entity translation. Therefore field methods on non translatable
474 // fields should be invoked only on the default entity translation.
475 $fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields();
476 foreach ($fields as $name => $items) {
477 // call_user_func_array() is way slower than a direct call so we avoid
478 // using it if have no parameters.
479 $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}();
483 // We need to call the delete method for field items of removed
485 if ($method == 'postSave' && !empty($entity->original)) {
486 $original_langcodes = array_keys($entity->original->getTranslationLanguages());
487 foreach (array_diff($original_langcodes, $langcodes) as $removed_langcode) {
488 $translation = $entity->original->getTranslation($removed_langcode);
489 $fields = $translation->getTranslatableFields();
490 foreach ($fields as $name => $items) {
500 * Invokes the post save method on the Field objects within an entity.
502 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
504 * @param bool $update
505 * Specifies whether the entity is being updated or created.
507 protected function invokeFieldPostSave(ContentEntityInterface $entity, $update) {
508 // For each entity translation this returns an array of resave flags keyed
509 // by field name, thus we merge them to obtain a list of fields to resave.
511 foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $translation_results) {
512 $resave += array_filter($translation_results);
515 $this->doSaveFieldItems($entity, array_keys($resave));
520 * Checks whether the field values changed compared to the original entity.
522 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
523 * Field definition of field to compare for changes.
524 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
525 * Entity to check for field changes.
526 * @param \Drupal\Core\Entity\ContentEntityInterface $original
527 * Original entity to compare against.
530 * True if the field value changed from the original entity.
532 protected function hasFieldValueChanged(FieldDefinitionInterface $field_definition, ContentEntityInterface $entity, ContentEntityInterface $original) {
533 $field_name = $field_definition->getName();
534 $langcodes = array_keys($entity->getTranslationLanguages());
535 if ($langcodes !== array_keys($original->getTranslationLanguages())) {
536 // If the list of langcodes has changed, we need to save.
539 foreach ($langcodes as $langcode) {
540 $items = $entity->getTranslation($langcode)->get($field_name)->filterEmptyItems();
541 $original_items = $original->getTranslation($langcode)->get($field_name)->filterEmptyItems();
542 // If the field items are not equal, we need to save.
543 if (!$items->equals($original_items)) {
552 * Populates the affected flag for all the revision translations.
554 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
555 * An entity object being saved.
557 protected function populateAffectedRevisionTranslations(ContentEntityInterface $entity) {
558 if ($this->entityType->isTranslatable() && $this->entityType->isRevisionable()) {
559 $languages = $entity->getTranslationLanguages();
560 foreach ($languages as $langcode => $language) {
561 $translation = $entity->getTranslation($langcode);
562 // Avoid populating the value if it was already manually set.
563 $affected = $translation->isRevisionTranslationAffected();
564 if (!isset($affected) && $translation->hasTranslationChanges()) {
565 $translation->setRevisionTranslationAffected(TRUE);
572 * Ensures integer entity IDs are valid.
574 * The identifier sanitization provided by this method has been introduced
575 * as Drupal used to rely on the database to facilitate this, which worked
576 * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
579 * The entity IDs to verify.
582 * The sanitized list of entity IDs.
584 protected function cleanIds(array $ids) {
585 $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
586 $id_definition = $definitions[$this->entityType->getKey('id')];
587 if ($id_definition->getType() == 'integer') {
588 $ids = array_filter($ids, function ($id) {
589 return is_numeric($id) && $id == (int) $id;
591 $ids = array_map('intval', $ids);
597 * Gets entities from the persistent cache backend.
599 * @param array|null &$ids
600 * If not empty, return entities that match these IDs. IDs that were found
601 * will be removed from the list.
603 * @return \Drupal\Core\Entity\ContentEntityInterface[]
604 * Array of entities from the persistent cache.
606 protected function getFromPersistentCache(array &$ids = NULL) {
607 if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
611 // Build the list of cache entries to retrieve.
613 foreach ($ids as $id) {
614 $cid_map[$id] = $this->buildCacheId($id);
616 $cids = array_values($cid_map);
617 if ($cache = $this->cacheBackend->getMultiple($cids)) {
618 // Get the entities that were found in the cache.
619 foreach ($ids as $index => $id) {
620 $cid = $cid_map[$id];
621 if (isset($cache[$cid])) {
622 $entities[$id] = $cache[$cid]->data;
631 * Stores entities in the persistent cache backend.
633 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
634 * Entities to store in the cache.
636 protected function setPersistentCache($entities) {
637 if (!$this->entityType->isPersistentlyCacheable()) {
642 $this->entityTypeId . '_values',
645 foreach ($entities as $id => $entity) {
646 $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
653 public function loadUnchanged($id) {
656 // The cache invalidation in the parent has the side effect that loading the
657 // same entity again during the save process (for example in
658 // hook_entity_presave()) will load the unchanged entity. Simulate this
659 // by explicitly removing the entity from the static cache.
660 parent::resetCache($ids);
662 // The default implementation in the parent class unsets the current cache
663 // and then reloads the entity. That is slow, especially if this is done
664 // repeatedly in the same request, e.g. when validating and then saving
665 // an entity. Optimize this for content entities by trying to load them
666 // directly from the persistent cache again, as in contrast to the static
667 // cache the persistent one will never be changed until the entity is saved.
668 $entities = $this->getFromPersistentCache($ids);
671 $entities[$id] = $this->load($id);
674 // As the entities are put into the persistent cache before the post load
675 // has been executed we have to execute it if we have retrieved the
676 // entity directly from the persistent cache.
677 $this->postLoad($entities);
679 if ($this->entityType->isStaticallyCacheable()) {
680 // As we've removed the entity from the static cache already we have to
681 // put the loaded unchanged entity there to simulate the behavior of the
683 $this->setStaticCache($entities);
687 return $entities[$id];
693 public function resetCache(array $ids = NULL) {
696 foreach ($ids as $id) {
697 unset($this->entities[$id]);
698 $cids[] = $this->buildCacheId($id);
700 if ($this->entityType->isPersistentlyCacheable()) {
701 $this->cacheBackend->deleteMultiple($cids);
705 $this->entities = [];
706 if ($this->entityType->isPersistentlyCacheable()) {
707 Cache::invalidateTags([$this->entityTypeId . '_values']);
713 * Builds the cache ID for the passed in entity ID.
716 * Entity ID for which the cache ID should be built.
719 * Cache ID that can be passed to the cache backend.
721 protected function buildCacheId($id) {
722 return "values:{$this->entityTypeId}:$id";