3 namespace Drupal\Core\Entity;
5 use Drupal\Core\Entity\Query\QueryInterface;
6 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
9 * A base entity storage class.
11 abstract class EntityStorageBase extends EntityHandlerBase implements EntityStorageInterface, EntityHandlerInterface {
14 * Entity type ID for this storage.
18 protected $entityTypeId;
21 * Information about the entity type.
23 * The following code returns the same object:
25 * \Drupal::entityManager()->getDefinition($this->entityTypeId)
28 * @var \Drupal\Core\Entity\EntityTypeInterface
30 protected $entityType;
33 * Name of the entity's ID field in the entity database table.
40 * Name of entity's UUID database table field, if it supports UUIDs.
42 * Has the value FALSE if this entity does not use UUIDs.
49 * The name of the entity langcode property.
53 protected $langcodeKey;
58 * @var \Drupal\Component\Uuid\UuidInterface
60 protected $uuidService;
63 * Name of the entity class.
67 protected $entityClass;
72 * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
74 protected $memoryCache;
77 * The memory cache cache tag.
81 protected $memoryCacheTag;
84 * Constructs an EntityStorageBase instance.
86 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
87 * The entity type definition.
88 * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
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();
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');
103 $this->memoryCache = $memory_cache;
104 $this->memoryCacheTag = 'entity.memory_cache:' . $this->entityTypeId;
110 public function getEntityTypeId() {
111 return $this->entityTypeId;
117 public function getEntityType() {
118 return $this->entityType;
122 * Builds the cache ID for the passed in entity ID.
125 * Entity ID for which the cache ID should be built.
128 * Cache ID that can be passed to the cache backend.
130 protected function buildCacheId($id) {
131 return "values:{$this->entityTypeId}:$id";
137 public function loadUnchanged($id) {
138 $this->resetCache([$id]);
139 return $this->load($id);
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));
152 // Call the backend method directly.
153 $this->memoryCache->invalidateTags([$this->memoryCacheTag]);
158 * Gets entities from the static cache.
161 * If not empty, return entities that match these IDs.
163 * @return \Drupal\Core\Entity\EntityInterface[]
164 * Array of entities from the entity cache.
166 protected function getFromStaticCache(array $ids) {
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;
180 * Stores entities in the static entity cache.
182 * @param \Drupal\Core\Entity\EntityInterface[] $entities
183 * Entities to store in the cache.
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]);
194 * Invokes a hook on behalf of the entity.
196 * @param string $hook
197 * One of 'create', 'presave', 'insert', 'update', 'predelete', 'delete', or
199 * @param \Drupal\Core\Entity\EntityInterface $entity
202 protected function invokeHook($hook, EntityInterface $entity) {
204 $this->moduleHandler()->invokeAll($this->entityTypeId . '_' . $hook, [$entity]);
205 // Invoke the respective entity-level hook.
206 $this->moduleHandler()->invokeAll('entity_' . $hook, [$entity]);
212 public function create(array $values = []) {
213 $entity_class = $this->entityClass;
214 $entity_class::preCreate($this, $values);
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();
221 $entity = $this->doCreate($values);
222 $entity->enforceIsNew();
224 $entity->postCreate($this);
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);
234 * Performs storage-specific creation of entities.
236 * @param array $values
237 * An array of values to set, keyed by property name.
239 * @return \Drupal\Core\Entity\EntityInterface
241 protected function doCreate(array $values) {
242 return new $this->entityClass($values, $this->entityTypeId);
248 public function load($id) {
249 $entities = $this->loadMultiple([$id]);
250 return isset($entities[$id]) ? $entities[$id] : NULL;
256 public function loadMultiple(array $ids = NULL) {
259 // Create a new variable which is either a prepared version of the $ids
260 // array for later comparison with the entity cache, or FALSE if no $ids
261 // were passed. The $ids array is reduced as items are loaded from cache,
262 // and we need to know if it's empty for this reason to avoid querying the
263 // database when all requested entities are loaded from cache.
264 $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
265 // Try to load entities from the static cache, if the entity type supports
267 if ($this->entityType->isStaticallyCacheable() && $ids) {
268 $entities += $this->getFromStaticCache($ids);
269 // If any entities were loaded, remove them from the ids still to load.
271 $ids = array_keys(array_diff_key($passed_ids, $entities));
275 // Load any remaining entities from the database. This is the case if $ids
276 // is set to NULL (so we load all entities) or if there are any ids left to
278 if ($ids === NULL || $ids) {
279 $queried_entities = $this->doLoadMultiple($ids);
282 // Pass all entities loaded from the database through $this->postLoad(),
283 // which attaches fields (if supported by the entity type) and calls the
284 // entity type specific load callback, for example hook_node_load().
285 if (!empty($queried_entities)) {
286 $this->postLoad($queried_entities);
287 $entities += $queried_entities;
290 if ($this->entityType->isStaticallyCacheable()) {
291 // Add entities to the cache.
292 if (!empty($queried_entities)) {
293 $this->setStaticCache($queried_entities);
297 // Ensure that the returned array is ordered the same as the original
298 // $ids array if this was passed in and remove any invalid ids.
300 // Remove any invalid ids from the array.
301 $passed_ids = array_intersect_key($passed_ids, $entities);
302 foreach ($entities as $entity) {
303 $passed_ids[$entity->id()] = $entity;
305 $entities = $passed_ids;
312 * Performs storage-specific loading of entities.
314 * Override this method to add custom functionality directly after loading.
315 * This is always called, while self::postLoad() is only called when there are
318 * @param array|null $ids
319 * (optional) An array of entity IDs, or NULL to load all entities.
321 * @return \Drupal\Core\Entity\EntityInterface[]
322 * Associative array of entities, keyed on the entity ID.
324 abstract protected function doLoadMultiple(array $ids = NULL);
327 * Attaches data to entities upon loading.
329 * @param array $entities
330 * Associative array of query results, keyed on the entity ID.
332 protected function postLoad(array &$entities) {
333 $entity_class = $this->entityClass;
334 $entity_class::postLoad($this, $entities);
335 // Call hook_entity_load().
336 foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
337 $function = $module . '_entity_load';
338 $function($entities, $this->entityTypeId);
340 // Call hook_TYPE_load().
341 foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
342 $function = $module . '_' . $this->entityTypeId . '_load';
343 $function($entities);
348 * Maps from storage records to entity objects.
350 * @param array $records
351 * Associative array of query results, keyed on the entity ID.
353 * @return \Drupal\Core\Entity\EntityInterface[]
354 * An array of entity objects implementing the EntityInterface.
356 protected function mapFromStorageRecords(array $records) {
358 foreach ($records as $record) {
359 $entity = new $this->entityClass($record, $this->entityTypeId);
360 $entities[$entity->id()] = $entity;
366 * Determines if this entity already exists in storage.
368 * @param int|string $id
369 * The original entity ID.
370 * @param \Drupal\Core\Entity\EntityInterface $entity
371 * The entity being saved.
375 abstract protected function has($id, EntityInterface $entity);
380 public function delete(array $entities) {
382 // If no entities were passed, do nothing.
386 // Ensure that the entities are keyed by ID.
387 $keyed_entities = [];
388 foreach ($entities as $entity) {
389 $keyed_entities[$entity->id()] = $entity;
392 // Allow code to run before deleting.
393 $entity_class = $this->entityClass;
394 $entity_class::preDelete($this, $keyed_entities);
395 foreach ($keyed_entities as $entity) {
396 $this->invokeHook('predelete', $entity);
399 // Perform the delete and reset the static cache for the deleted entities.
400 $this->doDelete($keyed_entities);
401 $this->resetCache(array_keys($keyed_entities));
403 // Allow code to run after deleting.
404 $entity_class::postDelete($this, $keyed_entities);
405 foreach ($keyed_entities as $entity) {
406 $this->invokeHook('delete', $entity);
411 * Performs storage-specific entity deletion.
413 * @param \Drupal\Core\Entity\EntityInterface[] $entities
414 * An array of entity objects to delete.
416 abstract protected function doDelete($entities);
421 public function save(EntityInterface $entity) {
422 // Track if this entity is new.
423 $is_new = $entity->isNew();
425 // Execute presave logic and invoke the related hooks.
426 $id = $this->doPreSave($entity);
428 // Perform the save and reset the static cache for the changed entity.
429 $return = $this->doSave($id, $entity);
431 // Execute post save logic and invoke the related hooks.
432 $this->doPostSave($entity, !$is_new);
438 * Performs presave entity processing.
440 * @param \Drupal\Core\Entity\EntityInterface $entity
444 * The processed entity identifier.
446 * @throws \Drupal\Core\Entity\EntityStorageException
447 * If the entity identifier is invalid.
449 protected function doPreSave(EntityInterface $entity) {
452 // Track the original ID.
453 if ($entity->getOriginalId() !== NULL) {
454 $id = $entity->getOriginalId();
457 // Track if this entity exists already.
458 $id_exists = $this->has($id, $entity);
460 // A new entity should not already exist.
461 if ($id_exists && $entity->isNew()) {
462 throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '$id' already exists.");
465 // Load the original entity, if any.
466 if ($id_exists && !isset($entity->original)) {
467 $entity->original = $this->loadUnchanged($id);
470 // Allow code to run before saving.
471 $entity->preSave($this);
472 $this->invokeHook('presave', $entity);
478 * Performs storage-specific saving of the entity.
480 * @param int|string $id
481 * The original entity ID.
482 * @param \Drupal\Core\Entity\EntityInterface $entity
483 * The entity to save.
486 * If the record insert or update failed, returns FALSE. If it succeeded,
487 * returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
489 abstract protected function doSave($id, EntityInterface $entity);
492 * Performs post save entity processing.
494 * @param \Drupal\Core\Entity\EntityInterface $entity
496 * @param bool $update
497 * Specifies whether the entity is being updated or created.
499 protected function doPostSave(EntityInterface $entity, $update) {
500 $this->resetCache([$entity->id()]);
502 // The entity is no longer new.
503 $entity->enforceIsNew(FALSE);
505 // Allow code to run after saving.
506 $entity->postSave($this, $update);
507 $this->invokeHook($update ? 'update' : 'insert', $entity);
509 // After saving, this is now the "original entity", and subsequent saves
510 // will be updates instead of inserts, and updates must always be able to
511 // correctly identify the original entity.
512 $entity->setOriginalId($entity->id());
514 unset($entity->original);
518 * Builds an entity query.
520 * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
521 * EntityQuery instance.
522 * @param array $values
523 * An associative array of properties of the entity, where the keys are the
524 * property names and the values are the values those properties must have.
526 protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
527 foreach ($values as $name => $value) {
528 // Cast scalars to array so we can consistently use an IN condition.
529 $entity_query->condition($name, (array) $value, 'IN');
536 public function loadByProperties(array $values = []) {
537 // Build a query to fetch the entity IDs.
538 $entity_query = $this->getQuery();
539 $entity_query->accessCheck(FALSE);
540 $this->buildPropertyQuery($entity_query, $values);
541 $result = $entity_query->execute();
542 return $result ? $this->loadMultiple($result) : [];
548 public function hasData() {
549 return (bool) $this->getQuery()
558 public function getQuery($conjunction = 'AND') {
559 // Access the service directly rather than entity.query factory so the
560 // storage's current entity type is used.
561 return \Drupal::service($this->getQueryServiceName())->get($this->entityType, $conjunction);
567 public function getAggregateQuery($conjunction = 'AND') {
568 // Access the service directly rather than entity.query factory so the
569 // storage's current entity type is used.
570 return \Drupal::service($this->getQueryServiceName())->getAggregate($this->entityType, $conjunction);
574 * Gets the name of the service for the query for this entity storage.
577 * The name of the service for the query for this entity storage.
579 abstract protected function getQueryServiceName();