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