3 namespace Drupal\Core\Entity;
5 use Drupal\Core\Entity\Query\QueryInterface;
8 * A base entity storage class.
10 abstract class EntityStorageBase extends EntityHandlerBase implements EntityStorageInterface, EntityHandlerInterface {
13 * Static cache of entities, keyed by entity ID.
17 protected $entities = [];
20 * Entity type ID for this storage.
24 protected $entityTypeId;
27 * Information about the entity type.
29 * The following code returns the same object:
31 * \Drupal::entityManager()->getDefinition($this->entityTypeId)
34 * @var \Drupal\Core\Entity\EntityTypeInterface
36 protected $entityType;
39 * Name of the entity's ID field in the entity database table.
46 * Name of entity's UUID database table field, if it supports UUIDs.
48 * Has the value FALSE if this entity does not use UUIDs.
55 * The name of the entity langcode property.
59 protected $langcodeKey;
64 * @var \Drupal\Component\Uuid\UuidInterface
66 protected $uuidService;
69 * Name of the entity class.
73 protected $entityClass;
76 * Constructs an EntityStorageBase instance.
78 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
79 * The entity type definition.
81 public function __construct(EntityTypeInterface $entity_type) {
82 $this->entityTypeId = $entity_type->id();
83 $this->entityType = $entity_type;
84 $this->idKey = $this->entityType->getKey('id');
85 $this->uuidKey = $this->entityType->getKey('uuid');
86 $this->langcodeKey = $this->entityType->getKey('langcode');
87 $this->entityClass = $this->entityType->getClass();
93 public function getEntityTypeId() {
94 return $this->entityTypeId;
100 public function getEntityType() {
101 return $this->entityType;
107 public function loadUnchanged($id) {
108 $this->resetCache([$id]);
109 return $this->load($id);
115 public function resetCache(array $ids = NULL) {
116 if ($this->entityType->isStaticallyCacheable() && isset($ids)) {
117 foreach ($ids as $id) {
118 unset($this->entities[$id]);
122 $this->entities = [];
127 * Gets entities from the static cache.
130 * If not empty, return entities that match these IDs.
132 * @return \Drupal\Core\Entity\EntityInterface[]
133 * Array of entities from the entity cache.
135 protected function getFromStaticCache(array $ids) {
137 // Load any available entities from the internal cache.
138 if ($this->entityType->isStaticallyCacheable() && !empty($this->entities)) {
139 $entities += array_intersect_key($this->entities, array_flip($ids));
145 * Stores entities in the static entity cache.
147 * @param \Drupal\Core\Entity\EntityInterface[] $entities
148 * Entities to store in the cache.
150 protected function setStaticCache(array $entities) {
151 if ($this->entityType->isStaticallyCacheable()) {
152 $this->entities += $entities;
157 * Invokes a hook on behalf of the entity.
159 * @param string $hook
160 * One of 'presave', 'insert', 'update', 'predelete', 'delete', or
162 * @param \Drupal\Core\Entity\EntityInterface $entity
165 protected function invokeHook($hook, EntityInterface $entity) {
167 $this->moduleHandler()->invokeAll($this->entityTypeId . '_' . $hook, [$entity]);
168 // Invoke the respective entity-level hook.
169 $this->moduleHandler()->invokeAll('entity_' . $hook, [$entity]);
175 public function create(array $values = []) {
176 $entity_class = $this->entityClass;
177 $entity_class::preCreate($this, $values);
179 // Assign a new UUID if there is none yet.
180 if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
181 $values[$this->uuidKey] = $this->uuidService->generate();
184 $entity = $this->doCreate($values);
185 $entity->enforceIsNew();
187 $entity->postCreate($this);
189 // Modules might need to add or change the data initially held by the new
190 // entity object, for instance to fill-in default values.
191 $this->invokeHook('create', $entity);
197 * Performs storage-specific creation of entities.
199 * @param array $values
200 * An array of values to set, keyed by property name.
202 * @return \Drupal\Core\Entity\EntityInterface
204 protected function doCreate(array $values) {
205 return new $this->entityClass($values, $this->entityTypeId);
211 public function load($id) {
212 $entities = $this->loadMultiple([$id]);
213 return isset($entities[$id]) ? $entities[$id] : NULL;
219 public function loadMultiple(array $ids = NULL) {
222 // Create a new variable which is either a prepared version of the $ids
223 // array for later comparison with the entity cache, or FALSE if no $ids
224 // were passed. The $ids array is reduced as items are loaded from cache,
225 // and we need to know if it's empty for this reason to avoid querying the
226 // database when all requested entities are loaded from cache.
227 $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
228 // Try to load entities from the static cache, if the entity type supports
230 if ($this->entityType->isStaticallyCacheable() && $ids) {
231 $entities += $this->getFromStaticCache($ids);
232 // If any entities were loaded, remove them from the ids still to load.
234 $ids = array_keys(array_diff_key($passed_ids, $entities));
238 // Load any remaining entities from the database. This is the case if $ids
239 // is set to NULL (so we load all entities) or if there are any ids left to
241 if ($ids === NULL || $ids) {
242 $queried_entities = $this->doLoadMultiple($ids);
245 // Pass all entities loaded from the database through $this->postLoad(),
246 // which attaches fields (if supported by the entity type) and calls the
247 // entity type specific load callback, for example hook_node_load().
248 if (!empty($queried_entities)) {
249 $this->postLoad($queried_entities);
250 $entities += $queried_entities;
253 if ($this->entityType->isStaticallyCacheable()) {
254 // Add entities to the cache.
255 if (!empty($queried_entities)) {
256 $this->setStaticCache($queried_entities);
260 // Ensure that the returned array is ordered the same as the original
261 // $ids array if this was passed in and remove any invalid ids.
263 // Remove any invalid ids from the array.
264 $passed_ids = array_intersect_key($passed_ids, $entities);
265 foreach ($entities as $entity) {
266 $passed_ids[$entity->id()] = $entity;
268 $entities = $passed_ids;
275 * Performs storage-specific loading of entities.
277 * Override this method to add custom functionality directly after loading.
278 * This is always called, while self::postLoad() is only called when there are
281 * @param array|null $ids
282 * (optional) An array of entity IDs, or NULL to load all entities.
284 * @return \Drupal\Core\Entity\EntityInterface[]
285 * Associative array of entities, keyed on the entity ID.
287 abstract protected function doLoadMultiple(array $ids = NULL);
290 * Attaches data to entities upon loading.
292 * @param array $entities
293 * Associative array of query results, keyed on the entity ID.
295 protected function postLoad(array &$entities) {
296 $entity_class = $this->entityClass;
297 $entity_class::postLoad($this, $entities);
298 // Call hook_entity_load().
299 foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) {
300 $function = $module . '_entity_load';
301 $function($entities, $this->entityTypeId);
303 // Call hook_TYPE_load().
304 foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) {
305 $function = $module . '_' . $this->entityTypeId . '_load';
306 $function($entities);
311 * Maps from storage records to entity objects.
313 * @param array $records
314 * Associative array of query results, keyed on the entity ID.
316 * @return \Drupal\Core\Entity\EntityInterface[]
317 * An array of entity objects implementing the EntityInterface.
319 protected function mapFromStorageRecords(array $records) {
321 foreach ($records as $record) {
322 $entity = new $this->entityClass($record, $this->entityTypeId);
323 $entities[$entity->id()] = $entity;
329 * Determines if this entity already exists in storage.
331 * @param int|string $id
332 * The original entity ID.
333 * @param \Drupal\Core\Entity\EntityInterface $entity
334 * The entity being saved.
338 abstract protected function has($id, EntityInterface $entity);
343 public function delete(array $entities) {
345 // If no entities were passed, do nothing.
349 // Ensure that the entities are keyed by ID.
350 $keyed_entities = [];
351 foreach ($entities as $entity) {
352 $keyed_entities[$entity->id()] = $entity;
355 // Allow code to run before deleting.
356 $entity_class = $this->entityClass;
357 $entity_class::preDelete($this, $keyed_entities);
358 foreach ($keyed_entities as $entity) {
359 $this->invokeHook('predelete', $entity);
362 // Perform the delete and reset the static cache for the deleted entities.
363 $this->doDelete($keyed_entities);
364 $this->resetCache(array_keys($keyed_entities));
366 // Allow code to run after deleting.
367 $entity_class::postDelete($this, $keyed_entities);
368 foreach ($keyed_entities as $entity) {
369 $this->invokeHook('delete', $entity);
374 * Performs storage-specific entity deletion.
376 * @param \Drupal\Core\Entity\EntityInterface[] $entities
377 * An array of entity objects to delete.
379 abstract protected function doDelete($entities);
384 public function save(EntityInterface $entity) {
385 // Track if this entity is new.
386 $is_new = $entity->isNew();
388 // Execute presave logic and invoke the related hooks.
389 $id = $this->doPreSave($entity);
391 // Perform the save and reset the static cache for the changed entity.
392 $return = $this->doSave($id, $entity);
394 // Execute post save logic and invoke the related hooks.
395 $this->doPostSave($entity, !$is_new);
401 * Performs presave entity processing.
403 * @param \Drupal\Core\Entity\EntityInterface $entity
407 * The processed entity identifier.
409 * @throws \Drupal\Core\Entity\EntityStorageException
410 * If the entity identifier is invalid.
412 protected function doPreSave(EntityInterface $entity) {
415 // Track the original ID.
416 if ($entity->getOriginalId() !== NULL) {
417 $id = $entity->getOriginalId();
420 // Track if this entity exists already.
421 $id_exists = $this->has($id, $entity);
423 // A new entity should not already exist.
424 if ($id_exists && $entity->isNew()) {
425 throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '$id' already exists.");
428 // Load the original entity, if any.
429 if ($id_exists && !isset($entity->original)) {
430 $entity->original = $this->loadUnchanged($id);
433 // Allow code to run before saving.
434 $entity->preSave($this);
435 $this->invokeHook('presave', $entity);
441 * Performs storage-specific saving of the entity.
443 * @param int|string $id
444 * The original entity ID.
445 * @param \Drupal\Core\Entity\EntityInterface $entity
446 * The entity to save.
449 * If the record insert or update failed, returns FALSE. If it succeeded,
450 * returns SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
452 abstract protected function doSave($id, EntityInterface $entity);
455 * Performs post save entity processing.
457 * @param \Drupal\Core\Entity\EntityInterface $entity
459 * @param bool $update
460 * Specifies whether the entity is being updated or created.
462 protected function doPostSave(EntityInterface $entity, $update) {
463 $this->resetCache([$entity->id()]);
465 // The entity is no longer new.
466 $entity->enforceIsNew(FALSE);
468 // Allow code to run after saving.
469 $entity->postSave($this, $update);
470 $this->invokeHook($update ? 'update' : 'insert', $entity);
472 // After saving, this is now the "original entity", and subsequent saves
473 // will be updates instead of inserts, and updates must always be able to
474 // correctly identify the original entity.
475 $entity->setOriginalId($entity->id());
477 unset($entity->original);
481 * Builds an entity query.
483 * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
484 * EntityQuery instance.
485 * @param array $values
486 * An associative array of properties of the entity, where the keys are the
487 * property names and the values are the values those properties must have.
489 protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
490 foreach ($values as $name => $value) {
491 // Cast scalars to array so we can consistently use an IN condition.
492 $entity_query->condition($name, (array) $value, 'IN');
499 public function loadByProperties(array $values = []) {
500 // Build a query to fetch the entity IDs.
501 $entity_query = $this->getQuery();
502 $this->buildPropertyQuery($entity_query, $values);
503 $result = $entity_query->execute();
504 return $result ? $this->loadMultiple($result) : [];
510 public function getQuery($conjunction = 'AND') {
511 // Access the service directly rather than entity.query factory so the
512 // storage's current entity type is used.
513 return \Drupal::service($this->getQueryServiceName())->get($this->entityType, $conjunction);
519 public function getAggregateQuery($conjunction = 'AND') {
520 // Access the service directly rather than entity.query factory so the
521 // storage's current entity type is used.
522 return \Drupal::service($this->getQueryServiceName())->getAggregate($this->entityType, $conjunction);
526 * Gets the name of the service for the query for this entity storage.
529 * The name of the service for the query for this entity storage.
531 abstract protected function getQueryServiceName();