EntityStorageBase.php 17.4 KB
Newer Older
1
2
3
<?php

namespace Drupal\Core\Entity;
4

5
use Drupal\Core\Entity\Query\QueryInterface;
6
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
7
8

/**
9
 * A base entity storage class.
10
 */
11
abstract class EntityStorageBase extends EntityHandlerBase implements EntityStorageInterface, EntityHandlerInterface {
12
13

  /**
14
   * Entity type ID for this storage.
15
16
17
   *
   * @var string
   */
18
  protected $entityTypeId;
19
20

  /**
21
   * Information about the entity type.
22
   *
23
24
   * The following code returns the same object:
   * @code
25
   * \Drupal::entityTypeManager()->getDefinition($this->entityTypeId)
26
27
   * @endcode
   *
28
   * @var \Drupal\Core\Entity\EntityTypeInterface
29
   */
30
  protected $entityType;
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

  /**
   * Name of the entity's ID field in the entity database table.
   *
   * @var string
   */
  protected $idKey;

  /**
   * Name of entity's UUID database table field, if it supports UUIDs.
   *
   * Has the value FALSE if this entity does not use UUIDs.
   *
   * @var string
   */
  protected $uuidKey;

48
49
50
51
52
53
54
  /**
   * The name of the entity langcode property.
   *
   * @var string
   */
  protected $langcodeKey;

55
56
57
58
59
60
61
62
63
64
65
66
67
68
  /**
   * The UUID service.
   *
   * @var \Drupal\Component\Uuid\UuidInterface
   */
  protected $uuidService;

  /**
   * Name of the entity class.
   *
   * @var string
   */
  protected $entityClass;

69
70
71
72
73
74
75
76
77
78
79
80
81
82
  /**
   * The memory cache.
   *
   * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
   */
  protected $memoryCache;

  /**
   * The memory cache cache tag.
   *
   * @var string
   */
  protected $memoryCacheTag;

83
  /**
84
   * Constructs an EntityStorageBase instance.
85
   *
86
87
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
88
   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
89
   *   The memory cache.
90
   */
91
  public function __construct(EntityTypeInterface $entity_type, MemoryCacheInterface $memory_cache) {
92
93
    $this->entityTypeId = $entity_type->id();
    $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
    $this->memoryCache = $memory_cache;
    $this->memoryCacheTag = 'entity.memory_cache:' . $this->entityTypeId;
100
101
  }

102
103
104
  /**
   * {@inheritdoc}
   */
105
106
  public function getEntityTypeId() {
    return $this->entityTypeId;
107
108
109
110
111
  }

  /**
   * {@inheritdoc}
   */
112
113
  public function getEntityType() {
    return $this->entityType;
114
115
  }

116
117
118
119
120
121
122
123
124
125
126
127
128
  /**
   * Builds the cache ID for the passed in entity ID.
   *
   * @param int $id
   *   Entity ID for which the cache ID should be built.
   *
   * @return string
   *   Cache ID that can be passed to the cache backend.
   */
  protected function buildCacheId($id) {
    return "values:{$this->entityTypeId}:$id";
  }

129
130
131
132
  /**
   * {@inheritdoc}
   */
  public function loadUnchanged($id) {
133
    $this->resetCache([$id]);
134
    return $this->load($id);
135
136
137
138
139
140
  }

  /**
   * {@inheritdoc}
   */
  public function resetCache(array $ids = NULL) {
141
    if ($this->entityType->isStaticallyCacheable() && isset($ids)) {
142
      foreach ($ids as $id) {
143
        $this->memoryCache->delete($this->buildCacheId($id));
144
145
146
      }
    }
    else {
147
148
      // Call the backend method directly.
      $this->memoryCache->invalidateTags([$this->memoryCacheTag]);
149
150
151
152
153
154
    }
  }

  /**
   * Gets entities from the static cache.
   *
155
   * @param array $ids
156
157
   *   If not empty, return entities that match these IDs.
   *
158
   * @return \Drupal\Core\Entity\EntityInterface[]
159
   *   Array of entities from the entity cache, keyed by entity ID.
160
   */
161
  protected function getFromStaticCache(array $ids) {
162
    $entities = [];
163
    // Load any available entities from the internal cache.
164
165
166
167
168
169
    if ($this->entityType->isStaticallyCacheable()) {
      foreach ($ids as $id) {
        if ($cached = $this->memoryCache->get($this->buildCacheId($id))) {
          $entities[$id] = $cached->data;
        }
      }
170
171
172
173
174
175
176
    }
    return $entities;
  }

  /**
   * Stores entities in the static entity cache.
   *
177
   * @param \Drupal\Core\Entity\EntityInterface[] $entities
178
179
   *   Entities to store in the cache.
   */
180
181
  protected function setStaticCache(array $entities) {
    if ($this->entityType->isStaticallyCacheable()) {
182
183
184
      foreach ($entities as $id => $entity) {
        $this->memoryCache->set($this->buildCacheId($entity->id()), $entity, MemoryCacheInterface::CACHE_PERMANENT, [$this->memoryCacheTag]);
      }
185
186
187
    }
  }

188
189
190
191
  /**
   * Invokes a hook on behalf of the entity.
   *
   * @param string $hook
192
   *   One of 'create', 'presave', 'insert', 'update', 'predelete', 'delete', or
193
   *   'revision_delete'.
194
   * @param \Drupal\Core\Entity\EntityInterface $entity
195
196
197
198
   *   The entity object.
   */
  protected function invokeHook($hook, EntityInterface $entity) {
    // Invoke the hook.
199
    $this->moduleHandler()->invokeAll($this->entityTypeId . '_' . $hook, [$entity]);
200
    // Invoke the respective entity-level hook.
201
    $this->moduleHandler()->invokeAll('entity_' . $hook, [$entity]);
202
203
  }

204
205
206
  /**
   * {@inheritdoc}
   */
207
  public function create(array $values = []) {
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
    $entity_class = $this->entityClass;
    $entity_class::preCreate($this, $values);

    // Assign a new UUID if there is none yet.
    if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
      $values[$this->uuidKey] = $this->uuidService->generate();
    }

    $entity = $this->doCreate($values);
    $entity->enforceIsNew();

    $entity->postCreate($this);

    // Modules might need to add or change the data initially held by the new
    // entity object, for instance to fill-in default values.
    $this->invokeHook('create', $entity);

    return $entity;
  }

  /**
   * Performs storage-specific creation of entities.
   *
   * @param array $values
   *   An array of values to set, keyed by property name.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   */
  protected function doCreate(array $values) {
    return new $this->entityClass($values, $this->entityTypeId);
  }

  /**
   * {@inheritdoc}
   */
  public function load($id) {
244
    assert(!is_null($id), sprintf('Cannot load the "%s" entity with NULL ID.', $this->entityTypeId));
245
    $entities = $this->loadMultiple([$id]);
246
247
248
249
250
251
252
    return isset($entities[$id]) ? $entities[$id] : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function loadMultiple(array $ids = NULL) {
253
    $entities = [];
254
    $preloaded_entities = [];
255
256
257
258

    // Create a new variable which is either a prepared version of the $ids
    // array for later comparison with the entity cache, or FALSE if no $ids
    // were passed. The $ids array is reduced as items are loaded from cache,
259
    // and we need to know if it is empty for this reason to avoid querying the
260
    // database when all requested entities are loaded from cache.
261
    $flipped_ids = $ids ? array_flip($ids) : FALSE;
262
263
    // Try to load entities from the static cache, if the entity type supports
    // static caching.
264
    if ($ids) {
265
      $entities += $this->getFromStaticCache($ids);
266
267
      // If any entities were loaded, remove them from the IDs still to load.
      $ids = array_keys(array_diff_key($flipped_ids, $entities));
268
269
    }

270
271
272
273
274
275
276
277
278
279
    // Try to gather any remaining entities from a 'preload' method. This method
    // can invoke a hook to be used by modules that need, for example, to swap
    // the default revision of an entity with a different one. Even though the
    // base entity storage class does not actually invoke any preload hooks, we
    // need to call the method here so we can add the pre-loaded entity objects
    // to the static cache below. If all the entities were fetched from the
    // static cache, skip this step.
    if ($ids === NULL || $ids) {
      $preloaded_entities = $this->preLoad($ids);
    }
280
281
    if (!empty($preloaded_entities)) {
      $entities += $preloaded_entities;
282

283
284
285
286
      // If any entities were pre-loaded, remove them from the IDs still to
      // load.
      $ids = array_keys(array_diff_key($flipped_ids, $entities));

287
288
      // Add pre-loaded entities to the cache.
      $this->setStaticCache($preloaded_entities);
289
290
    }

291
    // Load any remaining entities from the database. This is the case if $ids
292
    // is set to NULL (so we load all entities) or if there are any IDs left to
293
294
295
296
297
298
299
300
301
302
303
304
    // load.
    if ($ids === NULL || $ids) {
      $queried_entities = $this->doLoadMultiple($ids);
    }

    // Pass all entities loaded from the database through $this->postLoad(),
    // which attaches fields (if supported by the entity type) and calls the
    // entity type specific load callback, for example hook_node_load().
    if (!empty($queried_entities)) {
      $this->postLoad($queried_entities);
      $entities += $queried_entities;

305
      // Add queried entities to the cache.
306
      $this->setStaticCache($queried_entities);
307
308
309
    }

    // Ensure that the returned array is ordered the same as the original
310
311
312
313
314
    // $ids array if this was passed in and remove any invalid IDs.
    if ($flipped_ids) {
      // Remove any invalid IDs from the array and preserve the order passed in.
      $flipped_ids = array_intersect_key($flipped_ids, $entities);
      $entities = array_replace($flipped_ids, $entities);
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
    }

    return $entities;
  }

  /**
   * Performs storage-specific loading of entities.
   *
   * Override this method to add custom functionality directly after loading.
   * This is always called, while self::postLoad() is only called when there are
   * actual results.
   *
   * @param array|null $ids
   *   (optional) An array of entity IDs, or NULL to load all entities.
   *
   * @return \Drupal\Core\Entity\EntityInterface[]
   *   Associative array of entities, keyed on the entity ID.
   */
  abstract protected function doLoadMultiple(array $ids = NULL);

335
336
337
338
339
340
341
342
343
344
345
346
347
348
  /**
   * Gathers entities from a 'preload' step.
   *
   * @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\EntityInterface[]
   *   Associative array of entities, keyed by the entity ID.
   */
  protected function preLoad(array &$ids = NULL) {
    return [];
  }

349
350
351
  /**
   * Attaches data to entities upon loading.
   *
352
   * @param array $entities
353
354
   *   Associative array of query results, keyed on the entity ID.
   */
355
356
357
  protected function postLoad(array &$entities) {
    $entity_class = $this->entityClass;
    $entity_class::postLoad($this, $entities);
358
    // Call hook_entity_load().
359
    foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
360
      $function = $module . '_entity_load';
361
      $function($entities, $this->entityTypeId);
362
363
    }
    // Call hook_TYPE_load().
364
365
    foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
      $function = $module . '_' . $this->entityTypeId . '_load';
366
      $function($entities);
367
368
369
    }
  }

370
371
372
373
374
375
376
377
378
379
  /**
   * Maps from storage records to entity objects.
   *
   * @param array $records
   *   Associative array of query results, keyed on the entity ID.
   *
   * @return \Drupal\Core\Entity\EntityInterface[]
   *   An array of entity objects implementing the EntityInterface.
   */
  protected function mapFromStorageRecords(array $records) {
380
    $entities = [];
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
    foreach ($records as $record) {
      $entity = new $this->entityClass($record, $this->entityTypeId);
      $entities[$entity->id()] = $entity;
    }
    return $entities;
  }

  /**
   * Determines if this entity already exists in storage.
   *
   * @param int|string $id
   *   The original entity ID.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being saved.
   *
   * @return bool
   */
  abstract protected function has($id, EntityInterface $entity);

  /**
   * {@inheritdoc}
   */
  public function delete(array $entities) {
    if (!$entities) {
405
      // If no entities were passed, do nothing.
406
407
408
      return;
    }

409
410
411
412
413
414
    // Ensure that the entities are keyed by ID.
    $keyed_entities = [];
    foreach ($entities as $entity) {
      $keyed_entities[$entity->id()] = $entity;
    }

415
    // Allow code to run before deleting.
416
    $entity_class = $this->entityClass;
417
418
    $entity_class::preDelete($this, $keyed_entities);
    foreach ($keyed_entities as $entity) {
419
420
421
      $this->invokeHook('predelete', $entity);
    }

422
    // Perform the delete and reset the static cache for the deleted entities.
423
424
    $this->doDelete($keyed_entities);
    $this->resetCache(array_keys($keyed_entities));
425

426
    // Allow code to run after deleting.
427
428
    $entity_class::postDelete($this, $keyed_entities);
    foreach ($keyed_entities as $entity) {
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
      $this->invokeHook('delete', $entity);
    }
  }

  /**
   * Performs storage-specific entity deletion.
   *
   * @param \Drupal\Core\Entity\EntityInterface[] $entities
   *   An array of entity objects to delete.
   */
  abstract protected function doDelete($entities);

  /**
   * {@inheritdoc}
   */
  public function save(EntityInterface $entity) {
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
    // Track if this entity is new.
    $is_new = $entity->isNew();

    // Execute presave logic and invoke the related hooks.
    $id = $this->doPreSave($entity);

    // Perform the save and reset the static cache for the changed entity.
    $return = $this->doSave($id, $entity);

    // Execute post save logic and invoke the related hooks.
    $this->doPostSave($entity, !$is_new);

    return $return;
  }

  /**
   * Performs presave entity processing.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The saved entity.
   *
   * @return int|string
   *   The processed entity identifier.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If the entity identifier is invalid.
   */
  protected function doPreSave(EntityInterface $entity) {
473
474
475
476
477
478
479
480
481
482
483
    $id = $entity->id();

    // Track the original ID.
    if ($entity->getOriginalId() !== NULL) {
      $id = $entity->getOriginalId();
    }

    // Track if this entity exists already.
    $id_exists = $this->has($id, $entity);

    // A new entity should not already exist.
484
    if ($id_exists && $entity->isNew()) {
485
      throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '$id' already exists.");
486
487
488
489
490
491
492
493
494
495
496
    }

    // Load the original entity, if any.
    if ($id_exists && !isset($entity->original)) {
      $entity->original = $this->loadUnchanged($id);
    }

    // Allow code to run before saving.
    $entity->preSave($this);
    $this->invokeHook('presave', $entity);

497
    return $id;
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
  }

  /**
   * Performs storage-specific saving of the entity.
   *
   * @param int|string $id
   *   The original entity ID.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to save.
   *
   * @return bool|int
   *   If the record insert or update failed, returns FALSE. If it succeeded,
   *   returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
   */
  abstract protected function doSave($id, EntityInterface $entity);

514
515
516
517
518
519
520
521
522
  /**
   * Performs post save entity processing.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The saved entity.
   * @param bool $update
   *   Specifies whether the entity is being updated or created.
   */
  protected function doPostSave(EntityInterface $entity, $update) {
523
    $this->resetCache([$entity->id()]);
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539

    // The entity is no longer new.
    $entity->enforceIsNew(FALSE);

    // Allow code to run after saving.
    $entity->postSave($this, $update);
    $this->invokeHook($update ? 'update' : 'insert', $entity);

    // After saving, this is now the "original entity", and subsequent saves
    // will be updates instead of inserts, and updates must always be able to
    // correctly identify the original entity.
    $entity->setOriginalId($entity->id());

    unset($entity->original);
  }

540
541
542
543
  /**
   * {@inheritdoc}
   */
  public function restore(EntityInterface $entity) {
544
    // The restore process does not invoke any pre or post-save operations.
545
546
547
    $this->doSave($entity->id(), $entity);
  }

548
549
550
551
552
553
554
555
556
557
558
  /**
   * Builds an entity query.
   *
   * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
   *   EntityQuery instance.
   * @param array $values
   *   An associative array of properties of the entity, where the keys are the
   *   property names and the values are the values those properties must have.
   */
  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
    foreach ($values as $name => $value) {
559
560
      // Cast scalars to array so we can consistently use an IN condition.
      $entity_query->condition($name, (array) $value, 'IN');
561
562
563
564
565
566
    }
  }

  /**
   * {@inheritdoc}
   */
567
  public function loadByProperties(array $values = []) {
568
    // Build a query to fetch the entity IDs.
569
    $entity_query = $this->getQuery();
570
    $entity_query->accessCheck(FALSE);
571
572
    $this->buildPropertyQuery($entity_query, $values);
    $result = $entity_query->execute();
573
    return $result ? $this->loadMultiple($result) : [];
574
575
  }

576
577
578
579
580
581
582
583
584
585
  /**
   * {@inheritdoc}
   */
  public function hasData() {
    return (bool) $this->getQuery()
      ->accessCheck(FALSE)
      ->range(0, 1)
      ->execute();
  }

586
587
588
589
  /**
   * {@inheritdoc}
   */
  public function getQuery($conjunction = 'AND') {
590
    return \Drupal::service($this->getQueryServiceName())->get($this->entityType, $conjunction);
591
592
  }

593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
  /**
   * {@inheritdoc}
   */
  public function getAggregateQuery($conjunction = 'AND') {
    return \Drupal::service($this->getQueryServiceName())->getAggregate($this->entityType, $conjunction);
  }

  /**
   * Gets the name of the service for the query for this entity storage.
   *
   * @return string
   *   The name of the service for the query for this entity storage.
   */
  abstract protected function getQueryServiceName();

608
}