1<?php
2
3namespace Drupal\Core\Entity;
4
5use Drupal\Core\Entity\Query\QueryInterface;
6use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
7
8/**
9 * A base entity storage class.
10 */
11abstract class EntityStorageBase extends EntityHandlerBase implements EntityStorageInterface, EntityHandlerInterface {
12
13  /**
14   * Entity type ID for this storage.
15   *
16   * @var string
17   */
18  protected $entityTypeId;
19
20  /**
21   * Information about the entity type.
22   *
23   * The following code returns the same object:
24   * @code
25   * \Drupal::entityTypeManager()->getDefinition($this->entityTypeId)
26   * @endcode
27   *
28   * @var \Drupal\Core\Entity\EntityTypeInterface
29   */
30  protected $entityType;
31
32  /**
33   * Name of the entity's ID field in the entity database table.
34   *
35   * @var string
36   */
37  protected $idKey;
38
39  /**
40   * Name of entity's UUID database table field, if it supports UUIDs.
41   *
42   * Has the value FALSE if this entity does not use UUIDs.
43   *
44   * @var string
45   */
46  protected $uuidKey;
47
48  /**
49   * The name of the entity langcode property.
50   *
51   * @var string
52   */
53  protected $langcodeKey;
54
55  /**
56   * The UUID service.
57   *
58   * @var \Drupal\Component\Uuid\UuidInterface
59   */
60  protected $uuidService;
61
62  /**
63   * Name of the entity class.
64   *
65   * @var string
66   */
67  protected $entityClass;
68
69  /**
70   * The memory cache.
71   *
72   * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
73   */
74  protected $memoryCache;
75
76  /**
77   * The memory cache cache tag.
78   *
79   * @var string
80   */
81  protected $memoryCacheTag;
82
83  /**
84   * Constructs an EntityStorageBase instance.
85   *
86   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
87   *   The entity type definition.
88   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
89   *   The memory cache.
90   */
91  public function __construct(EntityTypeInterface $entity_type, MemoryCacheInterface $memory_cache = NULL) {
92    $this->entityTypeId = $entity_type->id();
93    $this->entityType = $entity_type;
94    $this->idKey = $this->entityType->getKey('id');
95    $this->uuidKey = $this->entityType->getKey('uuid');
96    $this->langcodeKey = $this->entityType->getKey('langcode');
97    $this->entityClass = $this->entityType->getClass();
98
99    if (!isset($memory_cache)) {
100      @trigger_error('The $memory_cache parameter was added in Drupal 8.6.x and will be required in 9.0.0. See https://www.drupal.org/node/2973262', E_USER_DEPRECATED);
101      $memory_cache = \Drupal::service('entity.memory_cache');
102    }
103    $this->memoryCache = $memory_cache;
104    $this->memoryCacheTag = 'entity.memory_cache:' . $this->entityTypeId;
105  }
106
107  /**
108   * {@inheritdoc}
109   */
110  public function getEntityTypeId() {
111    return $this->entityTypeId;
112  }
113
114  /**
115   * {@inheritdoc}
116   */
117  public function getEntityType() {
118    return $this->entityType;
119  }
120
121  /**
122   * Builds the cache ID for the passed in entity ID.
123   *
124   * @param int $id
125   *   Entity ID for which the cache ID should be built.
126   *
127   * @return string
128   *   Cache ID that can be passed to the cache backend.
129   */
130  protected function buildCacheId($id) {
131    return "values:{$this->entityTypeId}:$id";
132  }
133
134  /**
135   * {@inheritdoc}
136   */
137  public function loadUnchanged($id) {
138    $this->resetCache([$id]);
139    return $this->load($id);
140  }
141
142  /**
143   * {@inheritdoc}
144   */
145  public function resetCache(array $ids = NULL) {
146    if ($this->entityType->isStaticallyCacheable() && isset($ids)) {
147      foreach ($ids as $id) {
148        $this->memoryCache->delete($this->buildCacheId($id));
149      }
150    }
151    else {
152      // Call the backend method directly.
153      $this->memoryCache->invalidateTags([$this->memoryCacheTag]);
154    }
155  }
156
157  /**
158   * Gets entities from the static cache.
159   *
160   * @param array $ids
161   *   If not empty, return entities that match these IDs.
162   *
163   * @return \Drupal\Core\Entity\EntityInterface[]
164   *   Array of entities from the entity cache, keyed by entity ID.
165   */
166  protected function getFromStaticCache(array $ids) {
167    $entities = [];
168    // Load any available entities from the internal cache.
169    if ($this->entityType->isStaticallyCacheable()) {
170      foreach ($ids as $id) {
171        if ($cached = $this->memoryCache->get($this->buildCacheId($id))) {
172          $entities[$id] = $cached->data;
173        }
174      }
175    }
176    return $entities;
177  }
178
179  /**
180   * Stores entities in the static entity cache.
181   *
182   * @param \Drupal\Core\Entity\EntityInterface[] $entities
183   *   Entities to store in the cache.
184   */
185  protected function setStaticCache(array $entities) {
186    if ($this->entityType->isStaticallyCacheable()) {
187      foreach ($entities as $id => $entity) {
188        $this->memoryCache->set($this->buildCacheId($entity->id()), $entity, MemoryCacheInterface::CACHE_PERMANENT, [$this->memoryCacheTag]);
189      }
190    }
191  }
192
193  /**
194   * Invokes a hook on behalf of the entity.
195   *
196   * @param string $hook
197   *   One of 'create', 'presave', 'insert', 'update', 'predelete', 'delete', or
198   *   'revision_delete'.
199   * @param \Drupal\Core\Entity\EntityInterface $entity
200   *   The entity object.
201   */
202  protected function invokeHook($hook, EntityInterface $entity) {
203    // Invoke the hook.
204    $this->moduleHandler()->invokeAll($this->entityTypeId . '_' . $hook, [$entity]);
205    // Invoke the respective entity-level hook.
206    $this->moduleHandler()->invokeAll('entity_' . $hook, [$entity]);
207  }
208
209  /**
210   * {@inheritdoc}
211   */
212  public function create(array $values = []) {
213    $entity_class = $this->entityClass;
214    $entity_class::preCreate($this, $values);
215
216    // Assign a new UUID if there is none yet.
217    if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
218      $values[$this->uuidKey] = $this->uuidService->generate();
219    }
220
221    $entity = $this->doCreate($values);
222    $entity->enforceIsNew();
223
224    $entity->postCreate($this);
225
226    // Modules might need to add or change the data initially held by the new
227    // entity object, for instance to fill-in default values.
228    $this->invokeHook('create', $entity);
229
230    return $entity;
231  }
232
233  /**
234   * Performs storage-specific creation of entities.
235   *
236   * @param array $values
237   *   An array of values to set, keyed by property name.
238   *
239   * @return \Drupal\Core\Entity\EntityInterface
240   */
241  protected function doCreate(array $values) {
242    return new $this->entityClass($values, $this->entityTypeId);
243  }
244
245  /**
246   * {@inheritdoc}
247   */
248  public function load($id) {
249    assert(!is_null($id), sprintf('Cannot load the "%s" entity with NULL ID.', $this->entityTypeId));
250    $entities = $this->loadMultiple([$id]);
251    return isset($entities[$id]) ? $entities[$id] : NULL;
252  }
253
254  /**
255   * {@inheritdoc}
256   */
257  public function loadMultiple(array $ids = NULL) {
258    $entities = [];
259    $preloaded_entities = [];
260
261    // Create a new variable which is either a prepared version of the $ids
262    // array for later comparison with the entity cache, or FALSE if no $ids
263    // were passed. The $ids array is reduced as items are loaded from cache,
264    // and we need to know if it is empty for this reason to avoid querying the
265    // database when all requested entities are loaded from cache.
266    $flipped_ids = $ids ? array_flip($ids) : FALSE;
267    // Try to load entities from the static cache, if the entity type supports
268    // static caching.
269    if ($ids) {
270      $entities += $this->getFromStaticCache($ids);
271      // If any entities were loaded, remove them from the IDs still to load.
272      $ids = array_keys(array_diff_key($flipped_ids, $entities));
273    }
274
275    // Try to gather any remaining entities from a 'preload' method. This method
276    // can invoke a hook to be used by modules that need, for example, to swap
277    // the default revision of an entity with a different one. Even though the
278    // base entity storage class does not actually invoke any preload hooks, we
279    // need to call the method here so we can add the pre-loaded entity objects
280    // to the static cache below. If all the entities were fetched from the
281    // static cache, skip this step.
282    if ($ids === NULL || $ids) {
283      $preloaded_entities = $this->preLoad($ids);
284    }
285    if (!empty($preloaded_entities)) {
286      $entities += $preloaded_entities;
287
288      // If any entities were pre-loaded, remove them from the IDs still to
289      // load.
290      $ids = array_keys(array_diff_key($flipped_ids, $entities));
291
292      // Add pre-loaded entities to the cache.
293      $this->setStaticCache($preloaded_entities);
294    }
295
296    // Load any remaining entities from the database. This is the case if $ids
297    // is set to NULL (so we load all entities) or if there are any IDs left to
298    // load.
299    if ($ids === NULL || $ids) {
300      $queried_entities = $this->doLoadMultiple($ids);
301    }
302
303    // Pass all entities loaded from the database through $this->postLoad(),
304    // which attaches fields (if supported by the entity type) and calls the
305    // entity type specific load callback, for example hook_node_load().
306    if (!empty($queried_entities)) {
307      $this->postLoad($queried_entities);
308      $entities += $queried_entities;
309
310      // Add queried entities to the cache.
311      $this->setStaticCache($queried_entities);
312    }
313
314    // Ensure that the returned array is ordered the same as the original
315    // $ids array if this was passed in and remove any invalid IDs.
316    if ($flipped_ids) {
317      // Remove any invalid IDs from the array and preserve the order passed in.
318      $flipped_ids = array_intersect_key($flipped_ids, $entities);
319      $entities = array_replace($flipped_ids, $entities);
320    }
321
322    return $entities;
323  }
324
325  /**
326   * Performs storage-specific loading of entities.
327   *
328   * Override this method to add custom functionality directly after loading.
329   * This is always called, while self::postLoad() is only called when there are
330   * actual results.
331   *
332   * @param array|null $ids
333   *   (optional) An array of entity IDs, or NULL to load all entities.
334   *
335   * @return \Drupal\Core\Entity\EntityInterface[]
336   *   Associative array of entities, keyed on the entity ID.
337   */
338  abstract protected function doLoadMultiple(array $ids = NULL);
339
340  /**
341   * Gathers entities from a 'preload' step.
342   *
343   * @param array|null &$ids
344   *   If not empty, return entities that match these IDs. IDs that were found
345   *   will be removed from the list.
346   *
347   * @return \Drupal\Core\Entity\EntityInterface[]
348   *   Associative array of entities, keyed by the entity ID.
349   */
350  protected function preLoad(array &$ids = NULL) {
351    return [];
352  }
353
354  /**
355   * Attaches data to entities upon loading.
356   *
357   * @param array $entities
358   *   Associative array of query results, keyed on the entity ID.
359   */
360  protected function postLoad(array &$entities) {
361    $entity_class = $this->entityClass;
362    $entity_class::postLoad($this, $entities);
363    // Call hook_entity_load().
364    foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
365      $function = $module . '_entity_load';
366      $function($entities, $this->entityTypeId);
367    }
368    // Call hook_TYPE_load().
369    foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
370      $function = $module . '_' . $this->entityTypeId . '_load';
371      $function($entities);
372    }
373  }
374
375  /**
376   * Maps from storage records to entity objects.
377   *
378   * @param array $records
379   *   Associative array of query results, keyed on the entity ID.
380   *
381   * @return \Drupal\Core\Entity\EntityInterface[]
382   *   An array of entity objects implementing the EntityInterface.
383   */
384  protected function mapFromStorageRecords(array $records) {
385    $entities = [];
386    foreach ($records as $record) {
387      $entity = new $this->entityClass($record, $this->entityTypeId);
388      $entities[$entity->id()] = $entity;
389    }
390    return $entities;
391  }
392
393  /**
394   * Determines if this entity already exists in storage.
395   *
396   * @param int|string $id
397   *   The original entity ID.
398   * @param \Drupal\Core\Entity\EntityInterface $entity
399   *   The entity being saved.
400   *
401   * @return bool
402   */
403  abstract protected function has($id, EntityInterface $entity);
404
405  /**
406   * {@inheritdoc}
407   */
408  public function delete(array $entities) {
409    if (!$entities) {
410      // If no entities were passed, do nothing.
411      return;
412    }
413
414    // Ensure that the entities are keyed by ID.
415    $keyed_entities = [];
416    foreach ($entities as $entity) {
417      $keyed_entities[$entity->id()] = $entity;
418    }
419
420    // Allow code to run before deleting.
421    $entity_class = $this->entityClass;
422    $entity_class::preDelete($this, $keyed_entities);
423    foreach ($keyed_entities as $entity) {
424      $this->invokeHook('predelete', $entity);
425    }
426
427    // Perform the delete and reset the static cache for the deleted entities.
428    $this->doDelete($keyed_entities);
429    $this->resetCache(array_keys($keyed_entities));
430
431    // Allow code to run after deleting.
432    $entity_class::postDelete($this, $keyed_entities);
433    foreach ($keyed_entities as $entity) {
434      $this->invokeHook('delete', $entity);
435    }
436  }
437
438  /**
439   * Performs storage-specific entity deletion.
440   *
441   * @param \Drupal\Core\Entity\EntityInterface[] $entities
442   *   An array of entity objects to delete.
443   */
444  abstract protected function doDelete($entities);
445
446  /**
447   * {@inheritdoc}
448   */
449  public function save(EntityInterface $entity) {
450    // Track if this entity is new.
451    $is_new = $entity->isNew();
452
453    // Execute presave logic and invoke the related hooks.
454    $id = $this->doPreSave($entity);
455
456    // Perform the save and reset the static cache for the changed entity.
457    $return = $this->doSave($id, $entity);
458
459    // Execute post save logic and invoke the related hooks.
460    $this->doPostSave($entity, !$is_new);
461
462    return $return;
463  }
464
465  /**
466   * Performs presave entity processing.
467   *
468   * @param \Drupal\Core\Entity\EntityInterface $entity
469   *   The saved entity.
470   *
471   * @return int|string
472   *   The processed entity identifier.
473   *
474   * @throws \Drupal\Core\Entity\EntityStorageException
475   *   If the entity identifier is invalid.
476   */
477  protected function doPreSave(EntityInterface $entity) {
478    $id = $entity->id();
479
480    // Track the original ID.
481    if ($entity->getOriginalId() !== NULL) {
482      $id = $entity->getOriginalId();
483    }
484
485    // Track if this entity exists already.
486    $id_exists = $this->has($id, $entity);
487
488    // A new entity should not already exist.
489    if ($id_exists && $entity->isNew()) {
490      throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '$id' already exists.");
491    }
492
493    // Load the original entity, if any.
494    if ($id_exists && !isset($entity->original)) {
495      $entity->original = $this->loadUnchanged($id);
496    }
497
498    // Allow code to run before saving.
499    $entity->preSave($this);
500    $this->invokeHook('presave', $entity);
501
502    return $id;
503  }
504
505  /**
506   * Performs storage-specific saving of the entity.
507   *
508   * @param int|string $id
509   *   The original entity ID.
510   * @param \Drupal\Core\Entity\EntityInterface $entity
511   *   The entity to save.
512   *
513   * @return bool|int
514   *   If the record insert or update failed, returns FALSE. If it succeeded,
515   *   returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
516   */
517  abstract protected function doSave($id, EntityInterface $entity);
518
519  /**
520   * Performs post save entity processing.
521   *
522   * @param \Drupal\Core\Entity\EntityInterface $entity
523   *   The saved entity.
524   * @param bool $update
525   *   Specifies whether the entity is being updated or created.
526   */
527  protected function doPostSave(EntityInterface $entity, $update) {
528    $this->resetCache([$entity->id()]);
529
530    // The entity is no longer new.
531    $entity->enforceIsNew(FALSE);
532
533    // Allow code to run after saving.
534    $entity->postSave($this, $update);
535    $this->invokeHook($update ? 'update' : 'insert', $entity);
536
537    // After saving, this is now the "original entity", and subsequent saves
538    // will be updates instead of inserts, and updates must always be able to
539    // correctly identify the original entity.
540    $entity->setOriginalId($entity->id());
541
542    unset($entity->original);
543  }
544
545  /**
546   * {@inheritdoc}
547   */
548  public function restore(EntityInterface $entity) {
549    // The restore process does not invoke any pre or post-save operations.
550    $this->doSave($entity->id(), $entity);
551  }
552
553  /**
554   * Builds an entity query.
555   *
556   * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
557   *   EntityQuery instance.
558   * @param array $values
559   *   An associative array of properties of the entity, where the keys are the
560   *   property names and the values are the values those properties must have.
561   */
562  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
563    foreach ($values as $name => $value) {
564      // Cast scalars to array so we can consistently use an IN condition.
565      $entity_query->condition($name, (array) $value, 'IN');
566    }
567  }
568
569  /**
570   * {@inheritdoc}
571   */
572  public function loadByProperties(array $values = []) {
573    // Build a query to fetch the entity IDs.
574    $entity_query = $this->getQuery();
575    $entity_query->accessCheck(FALSE);
576    $this->buildPropertyQuery($entity_query, $values);
577    $result = $entity_query->execute();
578    return $result ? $this->loadMultiple($result) : [];
579  }
580
581  /**
582   * {@inheritdoc}
583   */
584  public function hasData() {
585    return (bool) $this->getQuery()
586      ->accessCheck(FALSE)
587      ->range(0, 1)
588      ->execute();
589  }
590
591  /**
592   * {@inheritdoc}
593   */
594  public function getQuery($conjunction = 'AND') {
595    // Access the service directly rather than entity.query factory so the
596    // storage's current entity type is used.
597    return \Drupal::service($this->getQueryServiceName())->get($this->entityType, $conjunction);
598  }
599
600  /**
601   * {@inheritdoc}
602   */
603  public function getAggregateQuery($conjunction = 'AND') {
604    // Access the service directly rather than entity.query factory so the
605    // storage's current entity type is used.
606    return \Drupal::service($this->getQueryServiceName())->getAggregate($this->entityType, $conjunction);
607  }
608
609  /**
610   * Gets the name of the service for the query for this entity storage.
611   *
612   * @return string
613   *   The name of the service for the query for this entity storage.
614   */
615  abstract protected function getQueryServiceName();
616
617}
618