Entity.php 18.4 KB
Newer Older
1 2
<?php

3
namespace Drupal\Core\Entity;
4

5
use Drupal\Core\Cache\Cache;
6
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
7
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
9
use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
10
use Drupal\Core\Language\Language;
11
use Drupal\Core\Language\LanguageInterface;
12
use Drupal\Core\Link;
13
use Drupal\Core\Session\AccountInterface;
14
use Drupal\Core\Url;
15
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
16
use Symfony\Component\Routing\Exception\RouteNotFoundException;
17

18 19 20
/**
 * Defines a base entity class.
 */
21
abstract class Entity implements EntityInterface {
22

23 24
  use RefinableCacheableDependencyTrait;

25 26 27
  use DependencySerializationTrait {
    __sleep as traitSleep;
  }
28 29 30 31 32 33

  /**
   * The entity type.
   *
   * @var string
   */
34
  protected $entityTypeId;
35 36 37 38 39 40 41 42

  /**
   * Boolean indicating whether the entity should be forced to be new.
   *
   * @var bool
   */
  protected $enforceIsNew;

43 44 45 46 47 48 49
  /**
   * A typed data object wrapping this entity.
   *
   * @var \Drupal\Core\TypedData\ComplexDataInterface
   */
  protected $typedData;

50
  /**
51 52 53 54 55 56 57
   * Constructs an Entity object.
   *
   * @param array $values
   *   An array of values to set, keyed by property name. If the entity type
   *   has bundles, the bundle key has to be specified.
   * @param string $entity_type
   *   The type of the entity to create.
58
   */
59
  public function __construct(array $values, $entity_type) {
60
    $this->entityTypeId = $entity_type;
61 62 63 64 65 66
    // Set initial values.
    foreach ($values as $key => $value) {
      $this->$key = $value;
    }
  }

67
  /**
68
   * Gets the entity manager.
69 70
   *
   * @return \Drupal\Core\Entity\EntityManagerInterface
71 72 73 74 75 76
   *
   * @deprecated in Drupal 8.0.0 and will be removed before Drupal 9.0.0.
   *   Use \Drupal::entityTypeManager() instead in most cases. If the needed
   *   method is not on \Drupal\Core\Entity\EntityTypeManagerInterface, see the
   *   deprecated \Drupal\Core\Entity\EntityManager to find the
   *   correct interface or service.
77 78 79 80 81
   */
  protected function entityManager() {
    return \Drupal::entityManager();
  }

82 83 84 85 86 87 88 89 90
  /**
   * Gets the entity type manager.
   *
   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected function entityTypeManager() {
    return \Drupal::entityTypeManager();
  }

91 92 93 94 95 96 97 98 99
  /**
   * Gets the entity type bundle info service.
   *
   * @return \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected function entityTypeBundleInfo() {
    return \Drupal::service('entity_type.bundle.info');
  }

100
  /**
101
   * Gets the language manager.
102 103 104 105 106 107 108 109
   *
   * @return \Drupal\Core\Language\LanguageManagerInterface
   */
  protected function languageManager() {
    return \Drupal::languageManager();
  }

  /**
110
   * Gets the UUID generator.
111 112 113 114 115 116 117
   *
   * @return \Drupal\Component\Uuid\UuidInterface
   */
  protected function uuidGenerator() {
    return \Drupal::service('uuid');
  }

118
  /**
119
   * {@inheritdoc}
120 121 122 123 124 125
   */
  public function id() {
    return isset($this->id) ? $this->id : NULL;
  }

  /**
126
   * {@inheritdoc}
127 128 129 130 131 132
   */
  public function uuid() {
    return isset($this->uuid) ? $this->uuid : NULL;
  }

  /**
133
   * {@inheritdoc}
134 135
   */
  public function isNew() {
136
    return !empty($this->enforceIsNew) || !$this->id();
137 138
  }

139
  /**
140
   * {@inheritdoc}
141 142 143
   */
  public function enforceIsNew($value = TRUE) {
    $this->enforceIsNew = $value;
144 145

    return $this;
146 147
  }

148
  /**
149
   * {@inheritdoc}
150
   */
151 152
  public function getEntityTypeId() {
    return $this->entityTypeId;
153 154 155
  }

  /**
156
   * {@inheritdoc}
157 158
   */
  public function bundle() {
159
    return $this->entityTypeId;
160 161 162
  }

  /**
163
   * {@inheritdoc}
164
   */
165
  public function label() {
166
    $label = NULL;
167
    $entity_type = $this->getEntityType();
168 169
    if (($label_callback = $entity_type->getLabelCallback()) && is_callable($label_callback)) {
      $label = call_user_func($label_callback, $this);
170
    }
171
    elseif (($label_key = $entity_type->getKey('label')) && isset($this->{$label_key})) {
172
      $label = $this->{$label_key};
173 174 175 176 177
    }
    return $label;
  }

  /**
178
   * {@inheritdoc}
179
   */
180
  public function urlInfo($rel = 'canonical', array $options = []) {
181 182 183 184 185 186 187
    return $this->toUrl($rel, $options);
  }

  /**
   * {@inheritdoc}
   */
  public function toUrl($rel = 'canonical', array $options = []) {
188
    if ($this->id() === NULL) {
189
      throw new EntityMalformedException(sprintf('The "%s" entity cannot have a URI as it does not have an ID', $this->getEntityTypeId()));
190
    }
191 192

    // The links array might contain URI templates set in annotations.
193
    $link_templates = $this->linkTemplates();
194

195 196 197 198 199 200
    // Links pointing to the current revision point to the actual entity. So
    // instead of using the 'revision' link, use the 'canonical' link.
    if ($rel === 'revision' && $this instanceof RevisionableInterface && $this->isDefaultRevision()) {
      $rel = 'canonical';
    }

201
    if (isset($link_templates[$rel])) {
202
      $route_parameters = $this->urlRouteParameters($rel);
203
      $route_name = "entity.{$this->entityTypeId}." . str_replace(['-', 'drupal:'], ['_', ''], $rel);
204
      $uri = new Url($route_name, $route_parameters);
205 206 207 208 209
    }
    else {
      $bundle = $this->bundle();
      // A bundle-specific callback takes precedence over the generic one for
      // the entity type.
210
      $bundles = $this->entityTypeBundleInfo()->getBundleInfo($this->getEntityTypeId());
211 212
      if (isset($bundles[$bundle]['uri_callback'])) {
        $uri_callback = $bundles[$bundle]['uri_callback'];
213
      }
214 215
      elseif ($entity_uri_callback = $this->getEntityType()->getUriCallback()) {
        $uri_callback = $entity_uri_callback;
216
      }
217

218 219
      // Invoke the callback to get the URI. If there is no callback, use the
      // default URI format.
220 221
      if (isset($uri_callback) && is_callable($uri_callback)) {
        $uri = call_user_func($uri_callback, $this);
222 223
      }
      else {
224
        throw new UndefinedLinkTemplateException("No link template '$rel' found for the '{$this->getEntityTypeId()}' entity type");
225
      }
226 227
    }

228 229
    // Pass the entity data through as options, so that alter functions do not
    // need to look up this entity again.
230 231 232
    $uri
      ->setOption('entity_type', $this->getEntityTypeId())
      ->setOption('entity', $this);
233 234

    // Display links by default based on the current language.
235 236 237
    // Link relations that do not require an existing entity should not be
    // affected by this entity's language, however.
    if (!in_array($rel, ['collection', 'add-page', 'add-form'], TRUE)) {
238 239 240
      $options += ['language' => $this->language()];
    }

241 242
    $uri_options = $uri->getOptions();
    $uri_options += $options;
243

244
    return $uri->setOptions($uri_options);
245 246
  }

247 248 249 250 251 252 253 254
  /**
   * {@inheritdoc}
   */
  public function hasLinkTemplate($rel) {
    $link_templates = $this->linkTemplates();
    return isset($link_templates[$rel]);
  }

255
  /**
256
   * Gets an array link templates.
257 258
   *
   * @return array
259
   *   An array of link templates containing paths.
260 261
   */
  protected function linkTemplates() {
262
    return $this->getEntityType()->getLinkTemplates();
263 264
  }

265 266 267 268
  /**
   * {@inheritdoc}
   */
  public function link($text = NULL, $rel = 'canonical', array $options = []) {
269 270 271 272 273 274 275 276
    return $this->toLink($text, $rel, $options)->toString();
  }

  /**
   * {@inheritdoc}
   */
  public function toLink($text = NULL, $rel = 'canonical', array $options = []) {
    if (!isset($text)) {
277 278
      $text = $this->label();
    }
279
    $url = $this->toUrl($rel);
280 281
    $options += $url->getOptions();
    $url->setOptions($options);
282
    return new Link($text, $url);
283 284
  }

285 286 287
  /**
   * {@inheritdoc}
   */
288
  public function url($rel = 'canonical', $options = []) {
289
    // While self::toUrl() will throw an exception if the entity has no id,
290
    // the expected result for a URL is always a string.
291
    if ($this->id() === NULL || !$this->hasLinkTemplate($rel)) {
292 293 294
      return '';
    }

295
    $uri = $this->toUrl($rel);
296 297 298
    $options += $uri->getOptions();
    $uri->setOptions($options);
    return $uri->toString();
299 300
  }

301
  /**
302
   * Gets an array of placeholders for this entity.
303 304 305 306 307
   *
   * Individual entity classes may override this method to add additional
   * placeholders if desired. If so, they should be sure to replicate the
   * property caching logic.
   *
308 309 310
   * @param string $rel
   *   The link relationship type, for example: canonical or edit-form.
   *
311 312 313
   * @return array
   *   An array of URI placeholders.
   */
314
  protected function urlRouteParameters($rel) {
315
    $uri_route_parameters = [];
316

317
    if (!in_array($rel, ['collection', 'add-page', 'add-form'], TRUE)) {
318 319 320
      // The entity ID is needed as a route parameter.
      $uri_route_parameters[$this->getEntityTypeId()] = $this->id();
    }
321 322 323 324
    if ($rel === 'add-form' && ($this->getEntityType()->hasKey('bundle'))) {
      $parameter_name = $this->getEntityType()->getBundleEntityType() ?: $this->getEntityType()->getKey('bundle');
      $uri_route_parameters[$parameter_name] = $this->bundle();
    }
325
    if ($rel === 'revision' && $this instanceof RevisionableInterface) {
326 327 328
      $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
    }

329
    return $uri_route_parameters;
330 331
  }

332 333 334 335
  /**
   * {@inheritdoc}
   */
  public function uriRelationships() {
336 337 338 339 340 341 342 343 344 345 346
    return array_filter(array_keys($this->linkTemplates()), function ($link_relation_type) {
      // It's not guaranteed that every link relation type also has a
      // corresponding route. For some, additional modules or configuration may
      // be necessary. The interface demands that we only return supported URI
      // relationships.
      try {
        $this->toUrl($link_relation_type)->toString(TRUE)->getGeneratedUrl();
      }
      catch (RouteNotFoundException $e) {
        return FALSE;
      }
347 348 349
      catch (MissingMandatoryParametersException $e) {
        return FALSE;
      }
350 351
      return TRUE;
    });
352 353
  }

354
  /**
355
   * {@inheritdoc}
356
   */
357
  public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
358
    if ($operation == 'create') {
359
      return $this->entityTypeManager()
360
        ->getAccessControlHandler($this->entityTypeId)
361
        ->createAccess($this->bundle(), $account, [], $return_as_object);
362
    }
363
    return $this->entityTypeManager()
364
      ->getAccessControlHandler($this->entityTypeId)
365
      ->access($this, $operation, $account, $return_as_object);
366 367 368
  }

  /**
369
   * {@inheritdoc}
370 371
   */
  public function language() {
372 373 374 375 376 377
    if ($key = $this->getEntityType()->getKey('langcode')) {
      $langcode = $this->$key;
      $language = $this->languageManager()->getLanguage($langcode);
      if ($language) {
        return $language;
      }
378
    }
379 380
    // Make sure we return a proper language object.
    $langcode = !empty($this->langcode) ? $this->langcode : LanguageInterface::LANGCODE_NOT_SPECIFIED;
381
    $language = new Language(['id' => $langcode]);
382
    return $language;
383 384 385
  }

  /**
386
   * {@inheritdoc}
387 388
   */
  public function save() {
389 390
    $storage = $this->entityTypeManager()->getStorage($this->entityTypeId);
    return $storage->save($this);
391 392 393
  }

  /**
394
   * {@inheritdoc}
395 396 397
   */
  public function delete() {
    if (!$this->isNew()) {
398
      $this->entityTypeManager()->getStorage($this->entityTypeId)->delete([$this->id() => $this]);
399 400 401 402
    }
  }

  /**
403
   * {@inheritdoc}
404 405 406
   */
  public function createDuplicate() {
    $duplicate = clone $this;
407
    $entity_type = $this->getEntityType();
408
    // Reset the entity ID and indicate that this is a new entity.
409
    $duplicate->{$entity_type->getKey('id')} = NULL;
410
    $duplicate->enforceIsNew();
411 412

    // Check if the entity type supports UUIDs and generate a new one if so.
413
    if ($entity_type->hasKey('uuid')) {
414
      $duplicate->{$entity_type->getKey('uuid')} = $this->uuidGenerator()->generate();
415 416 417 418 419
    }
    return $duplicate;
  }

  /**
420
   * {@inheritdoc}
421
   */
422
  public function getEntityType() {
423
    return $this->entityTypeManager()->getDefinition($this->getEntityTypeId());
424 425
  }

426 427 428
  /**
   * {@inheritdoc}
   */
429
  public function preSave(EntityStorageInterface $storage) {
430 431 432
    // Check if this is an entity bundle.
    if ($this->getEntityType()->getBundleOf()) {
      // Throw an exception if the bundle ID is longer than 32 characters.
433
      if (mb_strlen($this->id()) > EntityTypeInterface::BUNDLE_MAX_LENGTH) {
434
        throw new ConfigEntityIdLengthException("Attempt to create a bundle with an ID longer than " . EntityTypeInterface::BUNDLE_MAX_LENGTH . " characters: $this->id().");
435 436
      }
    }
437 438 439 440 441
  }

  /**
   * {@inheritdoc}
   */
442
  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
443
    $this->invalidateTagsOnSave($update);
444 445 446 447 448
  }

  /**
   * {@inheritdoc}
   */
449
  public static function preCreate(EntityStorageInterface $storage, array &$values) {
450 451 452 453 454
  }

  /**
   * {@inheritdoc}
   */
455
  public function postCreate(EntityStorageInterface $storage) {
456 457 458 459 460
  }

  /**
   * {@inheritdoc}
   */
461
  public static function preDelete(EntityStorageInterface $storage, array $entities) {
462 463 464 465 466
  }

  /**
   * {@inheritdoc}
   */
467
  public static function postDelete(EntityStorageInterface $storage, array $entities) {
468
    static::invalidateTagsOnDelete($storage->getEntityType(), $entities);
469 470 471 472 473
  }

  /**
   * {@inheritdoc}
   */
474
  public static function postLoad(EntityStorageInterface $storage, array &$entities) {
475 476
  }

477 478 479 480
  /**
   * {@inheritdoc}
   */
  public function referencedEntities() {
481
    return [];
482 483
  }

484 485 486 487
  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
488
    return $this->cacheContexts;
489 490
  }

491 492 493
  /**
   * {@inheritdoc}
   */
494
  public function getCacheTagsToInvalidate() {
495 496
    // @todo Add bundle-specific listing cache tag?
    //   https://www.drupal.org/node/2145751
497 498 499
    if ($this->isNew()) {
      return [];
    }
500
    return [$this->entityTypeId . ':' . $this->id()];
501 502
  }

503 504 505 506 507 508 509 510 511 512
  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    if ($this->cacheTags) {
      return Cache::mergeTags($this->getCacheTagsToInvalidate(), $this->cacheTags);
    }
    return $this->getCacheTagsToInvalidate();
  }

513 514 515 516
  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
517
    return $this->cacheMaxAge;
518 519
  }

520 521 522 523
  /**
   * {@inheritdoc}
   */
  public static function load($id) {
524 525 526 527
    $entity_type_repository = \Drupal::service('entity_type.repository');
    $entity_type_manager = \Drupal::entityTypeManager();
    $storage = $entity_type_manager->getStorage($entity_type_repository->getEntityTypeFromClass(get_called_class()));
    return $storage->load($id);
528 529 530 531 532 533
  }

  /**
   * {@inheritdoc}
   */
  public static function loadMultiple(array $ids = NULL) {
534 535 536 537
    $entity_type_repository = \Drupal::service('entity_type.repository');
    $entity_type_manager = \Drupal::entityTypeManager();
    $storage = $entity_type_manager->getStorage($entity_type_repository->getEntityTypeFromClass(get_called_class()));
    return $storage->loadMultiple($ids);
538 539
  }

540 541 542
  /**
   * {@inheritdoc}
   */
543
  public static function create(array $values = []) {
544 545 546 547
    $entity_type_repository = \Drupal::service('entity_type.repository');
    $entity_type_manager = \Drupal::entityTypeManager();
    $storage = $entity_type_manager->getStorage($entity_type_repository->getEntityTypeFromClass(get_called_class()));
    return $storage->create($values);
548 549
  }

550 551 552 553 554 555 556 557 558 559 560
  /**
   * Invalidates an entity's cache tags upon save.
   *
   * @param bool $update
   *   TRUE if the entity has been updated, or FALSE if it has been inserted.
   */
  protected function invalidateTagsOnSave($update) {
    // An entity was created or updated: invalidate its list cache tags. (An
    // updated entity may start to appear in a listing because it now meets that
    // listing's filtering requirements. A newly created entity may start to
    // appear in listings because it did not exist before.)
561
    $tags = $this->getEntityType()->getListCacheTags();
562 563 564 565
    if ($this->hasLinkTemplate('canonical')) {
      // Creating or updating an entity may change a cached 403 or 404 response.
      $tags = Cache::mergeTags($tags, ['4xx-response']);
    }
566 567
    if ($update) {
      // An existing entity was updated, also invalidate its unique cache tag.
568
      $tags = Cache::mergeTags($tags, $this->getCacheTagsToInvalidate());
569 570 571 572 573 574 575
    }
    Cache::invalidateTags($tags);
  }

  /**
   * Invalidates an entity's cache tags upon delete.
   *
576 577
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
578 579 580
   * @param \Drupal\Core\Entity\EntityInterface[] $entities
   *   An array of entities.
   */
581 582
  protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_type, array $entities) {
    $tags = $entity_type->getListCacheTags();
583 584 585 586 587 588
    foreach ($entities as $entity) {
      // An entity was deleted: invalidate its own cache tag, but also its list
      // cache tags. (A deleted entity may cause changes in a paged list on
      // other pages than the one it's on. The one it's on is handled by its own
      // cache tag, but subsequent list pages would not be invalidated, hence we
      // must invalidate its list cache tags as well.)
589
      $tags = Cache::mergeTags($tags, $entity->getCacheTagsToInvalidate());
590 591 592 593
    }
    Cache::invalidateTags($tags);
  }

594 595 596 597 598 599 600 601 602 603 604 605 606
  /**
   * {@inheritdoc}
   */
  public function getOriginalId() {
    // By default, entities do not support renames and do not have original IDs.
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function setOriginalId($id) {
    // By default, entities do not support renames and do not have original IDs.
607 608 609 610 611 612
    // If the specified ID is anything except NULL, this should mark this entity
    // as no longer new.
    if ($id !== NULL) {
      $this->enforceIsNew(FALSE);
    }

613 614 615
    return $this;
  }

616 617 618 619
  /**
   * {@inheritdoc}
   */
  public function toArray() {
620
    return [];
621 622
  }

623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
  /**
   * {@inheritdoc}
   */
  public function getTypedData() {
    if (!isset($this->typedData)) {
      $class = \Drupal::typedDataManager()->getDefinition('entity')['class'];
      $this->typedData = $class::createFromEntity($this);
    }
    return $this->typedData;
  }

  /**
   * {@inheritdoc}
   */
  public function __sleep() {
    $this->typedData = NULL;
    return $this->traitSleep();
  }

642 643 644 645 646 647 648
  /**
   * {@inheritdoc}
   */
  public function getConfigDependencyKey() {
    return $this->getEntityType()->getConfigDependencyKey();
  }

649 650 651 652 653 654 655
  /**
   * {@inheritdoc}
   */
  public function getConfigDependencyName() {
    return $this->getEntityTypeId() . ':' . $this->bundle() . ':' . $this->uuid();
  }

656 657 658 659 660 661 662 663 664
  /**
   * {@inheritdoc}
   */
  public function getConfigTarget() {
    // For content entities, use the UUID for the config target identifier.
    // This ensures that references to the target can be deployed reliably.
    return $this->uuid();
  }

665
}