ContentEntityBase.php 48.3 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\Core\Entity;

5
use Drupal\Component\Render\FormattableMarkup;
6
use Drupal\Core\Entity\Plugin\DataType\EntityReference;
7
use Drupal\Core\Field\BaseFieldDefinition;
8
use Drupal\Core\Language\Language;
9
use Drupal\Core\Language\LanguageInterface;
10
use Drupal\Core\Session\AccountInterface;
11
use Drupal\Core\StringTranslation\TranslatableMarkup;
12
use Drupal\Core\TypedData\TranslationStatusInterface;
13 14 15 16
use Drupal\Core\TypedData\TypedDataInterface;

/**
 * Implements Entity Field API specific enhancements to the Entity class.
17 18
 *
 * @ingroup entity_api
19
 */
20
abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface {
21

22 23 24 25
  use EntityChangesDetectionTrait {
    getFieldsToSkipFromTranslationChangesCheck as traitGetFieldsToSkipFromTranslationChangesCheck;
  }

26 27 28 29
  /**
   * The plain data values of the contained fields.
   *
   * This always holds the original, unchanged values of the entity. The values
30 31
   * are keyed by language code, whereas LanguageInterface::LANGCODE_DEFAULT
   * is used for values in default language.
32 33 34
   *
   * @todo: Add methods for getting original fields and for determining
   * changes.
35
   * @todo: Provide a better way for defining default values.
36 37 38
   *
   * @var array
   */
39
  protected $values = [];
40 41

  /**
42
   * The array of fields, each being an instance of FieldItemListInterface.
43 44 45
   *
   * @var array
   */
46
  protected $fields = [];
47

48 49
  /**
   * Local cache for field definitions.
50
   *
51
   * @see ContentEntityBase::getFieldDefinitions()
52 53 54 55 56
   *
   * @var array
   */
  protected $fieldDefinitions;

57 58 59
  /**
   * Local cache for the available language objects.
   *
60
   * @var \Drupal\Core\Language\LanguageInterface[]
61 62 63
   */
  protected $languages;

64 65 66 67 68 69 70
  /**
   * The language entity key.
   *
   * @var string
   */
  protected $langcodeKey;

71 72 73 74 75 76 77
  /**
   * The default langcode entity key.
   *
   * @var string
   */
  protected $defaultLangcodeKey;

78 79 80 81 82 83 84 85
  /**
   * Language code identifying the entity active language.
   *
   * This is the language field accessors will use to determine which field
   * values manipulate.
   *
   * @var string
   */
86
  protected $activeLangcode = LanguageInterface::LANGCODE_DEFAULT;
87

88 89 90 91 92 93 94
  /**
   * Local cache for the default language code.
   *
   * @var string
   */
  protected $defaultLangcode;

95 96 97 98
  /**
   * An array of entity translation metadata.
   *
   * An associative array keyed by translation language code. Every value is an
99
   * array containing the translation status and the translation object, if it has
100 101 102 103
   * already been instantiated.
   *
   * @var array
   */
104
  protected $translations = [];
105 106 107 108 109 110 111 112

  /**
   * A flag indicating whether a translation object is being initialized.
   *
   * @var bool
   */
  protected $translationInitialize = FALSE;

113 114 115 116 117 118 119 120 121 122 123 124 125 126
  /**
   * Boolean indicating whether a new revision should be created on save.
   *
   * @var bool
   */
  protected $newRevision = FALSE;

  /**
   * Indicates whether this is the default revision.
   *
   * @var bool
   */
  protected $isDefaultRevision = TRUE;

127
  /**
128
   * Holds untranslatable entity keys such as the ID, bundle, and revision ID.
129 130 131
   *
   * @var array
   */
132
  protected $entityKeys = [];
133

134 135 136 137 138
  /**
   * Holds translatable entity keys such as the label.
   *
   * @var array
   */
139
  protected $translatableEntityKeys = [];
140

141 142 143 144 145 146 147 148 149 150 151 152 153 154
  /**
   * Whether entity validation was performed.
   *
   * @var bool
   */
  protected $validated = FALSE;

  /**
   * Whether entity validation is required before saving the entity.
   *
   * @var bool
   */
  protected $validationRequired = FALSE;

155 156 157 158 159 160 161
  /**
   * The loaded revision ID before the new revision was set.
   *
   * @var int
   */
  protected $loadedRevisionId;

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
  /**
   * The revision translation affected entity key.
   *
   * @var string
   */
  protected $revisionTranslationAffectedKey;

  /**
   * Whether the revision translation affected flag has been enforced.
   *
   * An array, keyed by the translation language code.
   *
   * @var bool[]
   */
  protected $enforceRevisionTranslationAffected = [];

178 179 180 181 182 183 184
  /**
   * Local cache for fields to skip from the checking for translation changes.
   *
   * @var array
   */
  protected static $fieldsToSkipFromTranslationChangesCheck = [];

185
  /**
186
   * {@inheritdoc}
187
   */
188
  public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = []) {
189
    $this->entityTypeId = $entity_type;
190
    $this->entityKeys['bundle'] = $bundle ? $bundle : $this->entityTypeId;
191
    $this->langcodeKey = $this->getEntityType()->getKey('langcode');
192
    $this->defaultLangcodeKey = $this->getEntityType()->getKey('default_langcode');
193
    $this->revisionTranslationAffectedKey = $this->getEntityType()->getKey('revision_translation_affected');
194

195
    foreach ($values as $key => $value) {
196
      // If the key matches an existing property set the value to the property
197 198
      // to set properties like isDefaultRevision.
      // @todo: Should this be converted somehow?
199 200
      if (property_exists($this, $key) && isset($value[LanguageInterface::LANGCODE_DEFAULT])) {
        $this->$key = $value[LanguageInterface::LANGCODE_DEFAULT];
201
      }
202 203 204 205 206
    }

    $this->values = $values;
    foreach ($this->getEntityType()->getKeys() as $key => $field_name) {
      if (isset($this->values[$field_name])) {
207 208 209 210 211 212 213 214 215 216 217 218 219
        if (is_array($this->values[$field_name])) {
          // We store untranslatable fields into an entity key without using a
          // langcode key.
          if (!$this->getFieldDefinition($field_name)->isTranslatable()) {
            if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
              if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
                if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) {
                  $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'];
                }
              }
              else {
                $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
              }
220 221 222
            }
          }
          else {
223 224 225 226 227 228 229 230 231 232 233 234 235 236
            // We save translatable fields such as the publishing status of a node
            // into an entity key array keyed by langcode as a performance
            // optimization, so we don't have to go through TypedData when we
            // need these values.
            foreach ($this->values[$field_name] as $langcode => $field_value) {
              if (is_array($this->values[$field_name][$langcode])) {
                if (isset($this->values[$field_name][$langcode][0]['value'])) {
                  $this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode][0]['value'];
                }
              }
              else {
                $this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode];
              }
            }
237
          }
238 239
        }
      }
240
    }
241

242 243
    // Initialize translations. Ensure we have at least an entry for the default
    // language.
244 245 246 247
    // We determine if the entity is new by checking in the entity values for
    // the presence of the id entity key, as the usage of ::isNew() is not
    // possible in the constructor.
    $data = isset($values[$this->getEntityType()->getKey('id')]) ? ['status' => static::TRANSLATION_EXISTING] : ['status' => static::TRANSLATION_CREATED];
248
    $this->translations[LanguageInterface::LANGCODE_DEFAULT] = $data;
249
    $this->setDefaultLangcode();
250 251
    if ($translations) {
      foreach ($translations as $langcode) {
252
        if ($langcode != $this->defaultLangcode && $langcode != LanguageInterface::LANGCODE_DEFAULT) {
253 254 255 256
          $this->translations[$langcode] = $data;
        }
      }
    }
257 258 259 260 261
    if ($this->getEntityType()->isRevisionable()) {
      // Store the loaded revision ID the entity has been loaded with to
      // keep it safe from changes.
      $this->updateLoadedRevisionId();
    }
262 263
  }

264 265 266 267 268 269
  /**
   * {@inheritdoc}
   */
  protected function getLanguages() {
    if (empty($this->languages)) {
      $this->languages = $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL);
270 271 272 273
      // If the entity references a language that is not or no longer available,
      // we return a mock language object to avoid disrupting the consuming
      // code.
      if (!isset($this->languages[$this->defaultLangcode])) {
274
        $this->languages[$this->defaultLangcode] = new Language(['id' => $this->defaultLangcode]);
275
      }
276 277 278 279
    }
    return $this->languages;
  }

280 281 282
  /**
   * {@inheritdoc}
   */
283
  public function postCreate(EntityStorageInterface $storage) {
284 285 286
    $this->newRevision = TRUE;
  }

287 288 289 290
  /**
   * {@inheritdoc}
   */
  public function setNewRevision($value = TRUE) {
291
    if (!$this->getEntityType()->hasKey('revision')) {
292
      throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support revisions.");
293 294 295 296 297 298
    }

    if ($value && !$this->newRevision) {
      // When saving a new revision, set any existing revision ID to NULL so as
      // to ensure that a new revision will actually be created.
      $this->set($this->getEntityType()->getKey('revision'), NULL);
299 300 301 302 303
    }
    elseif (!$value && $this->newRevision) {
      // If ::setNewRevision(FALSE) is called after ::setNewRevision(TRUE) we
      // have to restore the loaded revision ID.
      $this->set($this->getEntityType()->getKey('revision'), $this->getLoadedRevisionId());
304
    }
305

306 307 308
    $this->newRevision = $value;
  }

309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
  /**
   * {@inheritdoc}
   */
  public function getLoadedRevisionId() {
    return $this->loadedRevisionId;
  }

  /**
   * {@inheritdoc}
   */
  public function updateLoadedRevisionId() {
    $this->loadedRevisionId = $this->getRevisionId() ?: $this->loadedRevisionId;
    return $this;
  }

324 325 326 327
  /**
   * {@inheritdoc}
   */
  public function isNewRevision() {
328
    return $this->newRevision || ($this->getEntityType()->hasKey('revision') && !$this->getRevisionId());
329 330 331 332 333 334 335 336 337 338
  }

  /**
   * {@inheritdoc}
   */
  public function isDefaultRevision($new_value = NULL) {
    $return = $this->isDefaultRevision;
    if (isset($new_value)) {
      $this->isDefaultRevision = (bool) $new_value;
    }
339 340 341
    // New entities should always ensure at least one default revision exists,
    // creating an entity without a default revision is an invalid state.
    return $this->isNew() || $return;
342 343
  }

344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
  /**
   * {@inheritdoc}
   */
  public function wasDefaultRevision() {
    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
    $entity_type = $this->getEntityType();
    if (!$entity_type->isRevisionable()) {
      return TRUE;
    }

    $revision_default_key = $entity_type->getRevisionMetadataKey('revision_default');
    $value = $this->isNew() || $this->get($revision_default_key)->value;
    return $value;
  }

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
  /**
   * {@inheritdoc}
   */
  public function isLatestRevision() {
    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
    $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());

    return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id());
  }

  /**
   * {@inheritdoc}
   */
  public function isLatestTranslationAffectedRevision() {
    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
    $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());

    return $this->getLoadedRevisionId() == $storage->getLatestTranslationAffectedRevisionId($this->id(), $this->language()->getId());
  }

379 380 381 382
  /**
   * {@inheritdoc}
   */
  public function isRevisionTranslationAffected() {
383
    return $this->hasField($this->revisionTranslationAffectedKey) ? $this->get($this->revisionTranslationAffectedKey)->value : TRUE;
384 385 386 387 388 389
  }

  /**
   * {@inheritdoc}
   */
  public function setRevisionTranslationAffected($affected) {
390 391
    if ($this->hasField($this->revisionTranslationAffectedKey)) {
      $this->set($this->revisionTranslationAffectedKey, $affected);
392 393 394 395
    }
    return $this;
  }

396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
  /**
   * {@inheritdoc}
   */
  public function isRevisionTranslationAffectedEnforced() {
    return !empty($this->enforceRevisionTranslationAffected[$this->activeLangcode]);
  }

  /**
   * {@inheritdoc}
   */
  public function setRevisionTranslationAffectedEnforced($enforced) {
    $this->enforceRevisionTranslationAffected[$this->activeLangcode] = $enforced;
    return $this;
  }

411 412 413 414 415 416 417
  /**
   * {@inheritdoc}
   */
  public function isDefaultTranslation() {
    return $this->activeLangcode === LanguageInterface::LANGCODE_DEFAULT;
  }

418 419 420 421
  /**
   * {@inheritdoc}
   */
  public function getRevisionId() {
422
    return $this->getEntityKey('revision');
423 424 425 426 427 428
  }

  /**
   * {@inheritdoc}
   */
  public function isTranslatable() {
429 430
    // Check the bundle is translatable, the entity has a language defined, and
    // the site has more than one language.
431
    $bundles = $this->entityManager()->getBundleInfo($this->entityTypeId);
432
    return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual();
433 434
  }

435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
  /**
   * {@inheritdoc}
   */
  public function preSave(EntityStorageInterface $storage) {
    // An entity requiring validation should not be saved if it has not been
    // actually validated.
    if ($this->validationRequired && !$this->validated) {
      // @todo Make this an assertion in https://www.drupal.org/node/2408013.
      throw new \LogicException('Entity validation was skipped.');
    }
    else {
      $this->validated = FALSE;
    }

    parent::preSave($storage);
  }

452 453 454
  /**
   * {@inheritdoc}
   */
455
  public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
456 457
  }

458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
  /**
   * {@inheritdoc}
   */
  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    parent::postSave($storage, $update);

    // Update the status of all saved translations.
    $removed = [];
    foreach ($this->translations as $langcode => &$data) {
      if ($data['status'] == static::TRANSLATION_REMOVED) {
        $removed[$langcode] = TRUE;
      }
      else {
        $data['status'] = static::TRANSLATION_EXISTING;
      }
    }
    $this->translations = array_diff_key($this->translations, $removed);
475 476 477 478 479 480

    // Reset the new revision flag.
    $this->newRevision = FALSE;

    // Reset the enforcement of the revision translation affected flag.
    $this->enforceRevisionTranslationAffected = [];
481 482
  }

483 484 485 486
  /**
   * {@inheritdoc}
   */
  public function validate() {
487
    $this->validated = TRUE;
488 489
    $violations = $this->getTypedData()->validate();
    return new EntityConstraintViolationList($this, iterator_to_array($violations));
490 491
  }

492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
  /**
   * {@inheritdoc}
   */
  public function isValidationRequired() {
    return (bool) $this->validationRequired;
  }

  /**
   * {@inheritdoc}
   */
  public function setValidationRequired($required) {
    $this->validationRequired = $required;
    return $this;
  }

507 508 509 510 511 512 513 514 515
  /**
   * Clear entity translation object cache to remove stale references.
   */
  protected function clearTranslationCache() {
    foreach ($this->translations as &$translation) {
      unset($translation['entity']);
    }
  }

516 517 518 519 520 521 522 523 524 525
  /**
   * {@inheritdoc}
   */
  public function __sleep() {
    // Get the values of instantiated field objects, only serialize the values.
    foreach ($this->fields as $name => $fields) {
      foreach ($fields as $langcode => $field) {
        $this->values[$name][$langcode] = $field->getValue();
      }
    }
526
    $this->fields = [];
527
    $this->fieldDefinitions = NULL;
528
    $this->languages = NULL;
529
    $this->clearTranslationCache();
530

531
    return parent::__sleep();
532 533
  }

534
  /**
535
   * {@inheritdoc}
536 537
   */
  public function id() {
538
    return $this->getEntityKey('id');
539 540 541
  }

  /**
542
   * {@inheritdoc}
543 544
   */
  public function bundle() {
545
    return $this->getEntityKey('bundle');
546 547 548
  }

  /**
549
   * {@inheritdoc}
550 551
   */
  public function uuid() {
552
    return $this->getEntityKey('uuid');
553 554
  }

555 556 557 558
  /**
   * {@inheritdoc}
   */
  public function hasField($field_name) {
559
    return (bool) $this->getFieldDefinition($field_name);
560 561
  }

562
  /**
563
   * {@inheritdoc}
564
   */
565 566 567
  public function get($field_name) {
    if (!isset($this->fields[$field_name][$this->activeLangcode])) {
      return $this->getTranslatedField($field_name, $this->activeLangcode);
568
    }
569
    return $this->fields[$field_name][$this->activeLangcode];
570 571 572 573 574
  }

  /**
   * Gets a translated field.
   *
575
   * @return \Drupal\Core\Field\FieldItemListInterface
576
   */
577
  protected function getTranslatedField($name, $langcode) {
578
    if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
579
      throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.");
580
    }
581 582
    // Populate $this->fields to speed-up further look-ups and to keep track of
    // fields objects, possibly holding changes to field values.
583
    if (!isset($this->fields[$name][$langcode])) {
584
      $definition = $this->getFieldDefinition($name);
585
      if (!$definition) {
586
        throw new \InvalidArgumentException("Field $name is unknown.");
587
      }
588
      // Non-translatable fields are always stored with
589
      // LanguageInterface::LANGCODE_DEFAULT as key.
590

591
      $default = $langcode == LanguageInterface::LANGCODE_DEFAULT;
592
      if (!$default && !$definition->isTranslatable()) {
593 594
        if (!isset($this->fields[$name][LanguageInterface::LANGCODE_DEFAULT])) {
          $this->fields[$name][LanguageInterface::LANGCODE_DEFAULT] = $this->getTranslatedField($name, LanguageInterface::LANGCODE_DEFAULT);
595
        }
596
        $this->fields[$name][$langcode] = &$this->fields[$name][LanguageInterface::LANGCODE_DEFAULT];
597 598
      }
      else {
599
        $value = NULL;
600 601 602
        if (isset($this->values[$name][$langcode])) {
          $value = $this->values[$name][$langcode];
        }
603
        $field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this->getTranslation($langcode), $name, $value);
604 605 606 607
        if ($default) {
          // $this->defaultLangcode might not be set if we are initializing the
          // default language code cache, in which case there is no valid
          // langcode to assign.
608
          $field_langcode = isset($this->defaultLangcode) ? $this->defaultLangcode : LanguageInterface::LANGCODE_NOT_SPECIFIED;
609
        }
610 611 612 613 614
        else {
          $field_langcode = $langcode;
        }
        $field->setLangcode($field_langcode);
        $this->fields[$name][$langcode] = $field;
615 616
      }
    }
617
    return $this->fields[$name][$langcode];
618 619 620
  }

  /**
621
   * {@inheritdoc}
622
   */
623
  public function set($name, $value, $notify = TRUE) {
624 625 626 627
    // Assign the value on the child and overrule notify such that we get
    // notified to handle changes afterwards. We can ignore notify as there is
    // no parent to notify anyway.
    $this->get($name)->setValue($value, TRUE);
628
    return $this;
629 630 631
  }

  /**
632
   * {@inheritdoc}
633
   */
634
  public function getFields($include_computed = TRUE) {
635
    $fields = [];
636
    foreach ($this->getFieldDefinitions() as $name => $definition) {
637
      if ($include_computed || !$definition->isComputed()) {
638
        $fields[$name] = $this->get($name);
639 640
      }
    }
641
    return $fields;
642 643
  }

644 645 646 647 648 649 650 651 652 653 654 655 656
  /**
   * {@inheritdoc}
   */
  public function getTranslatableFields($include_computed = TRUE) {
    $fields = [];
    foreach ($this->getFieldDefinitions() as $name => $definition) {
      if (($include_computed || !$definition->isComputed()) && $definition->isTranslatable()) {
        $fields[$name] = $this->get($name);
      }
    }
    return $fields;
  }

657
  /**
658
   * {@inheritdoc}
659 660
   */
  public function getIterator() {
661
    return new \ArrayIterator($this->getFields());
662 663 664
  }

  /**
665
   * {@inheritdoc}
666
   */
667
  public function getFieldDefinition($name) {
668
    if (!isset($this->fieldDefinitions)) {
669
      $this->getFieldDefinitions();
670 671 672
    }
    if (isset($this->fieldDefinitions[$name])) {
      return $this->fieldDefinitions[$name];
673 674 675 676
    }
  }

  /**
677
   * {@inheritdoc}
678
   */
679
  public function getFieldDefinitions() {
680
    if (!isset($this->fieldDefinitions)) {
681
      $this->fieldDefinitions = $this->entityManager()->getFieldDefinitions($this->entityTypeId, $this->bundle());
682 683
    }
    return $this->fieldDefinitions;
684 685 686
  }

  /**
687
   * {@inheritdoc}
688
   */
689
  public function toArray() {
690
    $values = [];
691
    foreach ($this->getFields() as $name => $property) {
692 693 694 695 696 697
      $values[$name] = $property->getValue();
    }
    return $values;
  }

  /**
698 699
   * {@inheritdoc}
   */
700
  public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
701
    if ($operation == 'create') {
702
      return $this->entityManager()
703
        ->getAccessControlHandler($this->entityTypeId)
704
        ->createAccess($this->bundle(), $account, [], $return_as_object);
705
    }
706
    return $this->entityManager()
707
      ->getAccessControlHandler($this->entityTypeId)
708
      ->access($this, $operation, $account, $return_as_object);
709 710 711 712
  }

  /**
   * {@inheritdoc}
713 714
   */
  public function language() {
715
    $language = NULL;
716
    if ($this->activeLangcode != LanguageInterface::LANGCODE_DEFAULT) {
717
      if (!isset($this->languages[$this->activeLangcode])) {
718
        $this->getLanguages();
719
      }
720
      $language = $this->languages[$this->activeLangcode];
721 722
    }
    else {
723 724 725
      // @todo Avoid this check by getting the language from the language
      //   manager directly in https://www.drupal.org/node/2303877.
      if (!isset($this->languages[$this->defaultLangcode])) {
726
        $this->getLanguages();
727
      }
728
      $language = $this->languages[$this->defaultLangcode];
729
    }
730
    return $language;
731 732 733
  }

  /**
734 735 736 737
   * Populates the local cache for the default language code.
   */
  protected function setDefaultLangcode() {
    // Get the language code if the property exists.
738 739
    // Try to read the value directly from the list of entity keys which got
    // initialized in __construct(). This avoids creating a field item object.
740 741
    if (isset($this->translatableEntityKeys['langcode'][$this->activeLangcode])) {
      $this->defaultLangcode = $this->translatableEntityKeys['langcode'][$this->activeLangcode];
742 743
    }
    elseif ($this->hasField($this->langcodeKey) && ($item = $this->get($this->langcodeKey)) && isset($item->language)) {
744
      $this->defaultLangcode = $item->language->getId();
745
      $this->translatableEntityKeys['langcode'][$this->activeLangcode] = $this->defaultLangcode;
746
    }
747

748
    if (empty($this->defaultLangcode)) {
749 750
      // Make sure we return a proper language object, if the entity has a
      // langcode field, default to the site's default language.
751
      if ($this->hasField($this->langcodeKey)) {
752 753 754 755 756
        $this->defaultLangcode = $this->languageManager()->getDefaultLanguage()->getId();
      }
      else {
        $this->defaultLangcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
      }
757
    }
758

759 760
    // This needs to be initialized manually as it is skipped when instantiating
    // the language field object to avoid infinite recursion.
761 762
    if (!empty($this->fields[$this->langcodeKey])) {
      $this->fields[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT]->setLangcode($this->defaultLangcode);
763 764 765 766 767
    }
  }

  /**
   * Updates language for already instantiated fields.
768
   */
769 770
  protected function updateFieldLangcodes($langcode) {
    foreach ($this->fields as $name => $items) {
771 772
      if (!empty($items[LanguageInterface::LANGCODE_DEFAULT])) {
        $items[LanguageInterface::LANGCODE_DEFAULT]->setLangcode($langcode);
773
      }
774
    }
775 776 777 778 779
  }

  /**
   * {@inheritdoc}
   */
780
  public function onChange($name) {
781 782 783
    // Check if the changed name is the value of any entity keys and if any of
    // those values are currently cached, if so, reset it. Exclude the bundle
    // from that check, as it ready only and must not change, unsetting it could
784
    // lead to recursions.
785
    foreach (array_keys($this->getEntityType()->getKeys(), $name, TRUE) as $key) {
786 787 788 789 790 791 792
      if ($key != 'bundle') {
        if (isset($this->entityKeys[$key])) {
          unset($this->entityKeys[$key]);
        }
        elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
          unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
        }
793 794 795
        // If the revision identifier field is being populated with the original
        // value, we need to make sure the "new revision" flag is reset
        // accordingly.
796
        if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId() && !$this->isNew()) {
797 798
          $this->newRevision = FALSE;
        }
799 800
      }
    }
801

802 803 804 805 806 807
    switch ($name) {
      case $this->langcodeKey:
        if ($this->isDefaultTranslation()) {
          // Update the default internal language cache.
          $this->setDefaultLangcode();
          if (isset($this->translations[$this->defaultLangcode])) {
808
            $message = new FormattableMarkup('A translation already exists for the specified language (@langcode).', ['@langcode' => $this->defaultLangcode]);
809 810 811 812 813 814 815 816 817 818
            throw new \InvalidArgumentException($message);
          }
          $this->updateFieldLangcodes($this->defaultLangcode);
        }
        else {
          // @todo Allow the translation language to be changed. See
          //   https://www.drupal.org/node/2443989.
          $items = $this->get($this->langcodeKey);
          if ($items->value != $this->activeLangcode) {
            $items->setValue($this->activeLangcode, FALSE);
819
            $message = new FormattableMarkup('The translation language cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
820 821 822 823 824 825 826 827
            throw new \LogicException($message);
          }
        }
        break;

      case $this->defaultLangcodeKey:
        // @todo Use a standard method to make the default_langcode field
        //   read-only. See https://www.drupal.org/node/2443991.
828
        if (isset($this->values[$this->defaultLangcodeKey]) && $this->get($this->defaultLangcodeKey)->value != $this->isDefaultTranslation()) {
829
          $this->get($this->defaultLangcodeKey)->setValue($this->isDefaultTranslation(), FALSE);
830
          $message = new FormattableMarkup('The default translation flag cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
831 832 833
          throw new \LogicException($message);
        }
        break;
834 835 836 837 838 839

      case $this->revisionTranslationAffectedKey:
        // If the revision translation affected flag is being set then enforce
        // its value.
        $this->setRevisionTranslationAffectedEnforced(TRUE);
        break;
840
    }
841 842 843
  }

  /**
844
   * {@inheritdoc}
845 846 847 848
   */
  public function getTranslation($langcode) {
    // Ensure we always use the default language code when dealing with the
    // original entity language.
849 850
    if ($langcode != LanguageInterface::LANGCODE_DEFAULT && $langcode == $this->defaultLangcode) {
      $langcode = LanguageInterface::LANGCODE_DEFAULT;
851
    }
852 853 854

    // Populate entity translation object cache so it will be available for all
    // translation objects.
855 856
    if (!isset($this->translations[$this->activeLangcode]['entity'])) {
      $this->translations[$this->activeLangcode]['entity'] = $this;
857
    }
858 859 860 861 862 863

    // If we already have a translation object for the specified language we can
    // just return it.
    if (isset($this->translations[$langcode]['entity'])) {
      $translation = $this->translations[$langcode]['entity'];
    }
864 865 866 867 868
    // Otherwise if an existing translation language was specified we need to
    // instantiate the related translation.
    elseif (isset($this->translations[$langcode])) {
      $translation = $this->initializeTranslation($langcode);
      $this->translations[$langcode]['entity'] = $translation;
869 870 871
    }

    if (empty($translation)) {
872
      throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
873
    }
874

875 876 877 878
    return $translation;
  }

  /**
879
   * {@inheritdoc}
880
   */
881
  public function getUntranslated() {
882
    return $this->getTranslation(LanguageInterface::LANGCODE_DEFAULT);
883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
  }

  /**
   * Instantiates a translation object for an existing translation.
   *
   * The translated entity will be a clone of the current entity with the
   * specified $langcode. All translations share the same field data structures
   * to ensure that all of them deal with fresh data.
   *
   * @param string $langcode
   *   The language code for the requested translation.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   The translation object. The content properties of the translation object
   *   are stored as references to the main entity.
   */
  protected function initializeTranslation($langcode) {
    // If the requested translation is valid, clone it with the current language
    // as the active language. The $translationInitialize flag triggers a
    // shallow (non-recursive) clone.
    $this->translationInitialize = TRUE;
    $translation = clone $this;
    $this->translationInitialize = FALSE;

    $translation->activeLangcode = $langcode;

    // Ensure that changes to fields, values and translations are propagated
    // to all the translation objects.
    // @todo Consider converting these to ArrayObject.
    $translation->values = &$this->values;
    $translation->fields = &$this->fields;
    $translation->translations = &$this->translations;
915
    $translation->enforceIsNew = &$this->enforceIsNew;
916
    $translation->newRevision = &$this->newRevision;
917 918
    $translation->entityKeys = &$this->entityKeys;
    $translation->translatableEntityKeys = &$this->translatableEntityKeys;
919
    $translation->translationInitialize = FALSE;
920
    $translation->typedData = NULL;
921
    $translation->loadedRevisionId = &$this->loadedRevisionId;
922
    $translation->isDefaultRevision = &$this->isDefaultRevision;
923
    $translation->enforceRevisionTranslationAffected = &$this->enforceRevisionTranslationAffected;
924 925 926 927 928 929 930 931

    return $translation;
  }

  /**
   * {@inheritdoc}
   */
  public function hasTranslation($langcode) {
932
    if ($langcode == $this->defaultLangcode) {
933
      $langcode = LanguageInterface::LANGCODE_DEFAULT;
934
    }
935 936 937
    return !empty($this->translations[$langcode]['status']);
  }

938 939 940 941 942 943 944
  /**
   * {@inheritdoc}
   */
  public function isNewTranslation() {
    return $this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_CREATED;
  }

945 946 947
  /**
   * {@inheritdoc}
   */
948
  public function addTranslation($langcode, array $values = []) {
949 950
    // Make sure we do not attempt to create a translation if an invalid
    // language is specified or the entity cannot be translated.
951
    $this->getLanguages();
952
    if (!isset($this->languages[$langcode]) || $this->hasTranslation($langcode) || $this->languages[$langcode]->isLocked()) {
953
      throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
954
    }
955 956 957
    if ($this->languages[$this->defaultLangcode]->isLocked()) {
      throw new \InvalidArgumentException("The entity cannot be translated since it is language neutral ({$this->defaultLangcode}).");
    }
958

959 960 961
    // Initialize the translation object.
    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
    $storage = $this->entityManager()->getStorage($this->getEntityTypeId());
962
    $this->translations[$langcode]['status'] = !isset($this->translations[$langcode]['status_existed']) ? static::TRANSLATION_CREATED : static::TRANSLATION_EXISTING;
963
    return $storage->createTranslation($this, $langcode, $values);
964 965 966 967 968 969
  }

  /**
   * {@inheritdoc}
   */
  public function removeTranslation($langcode) {
970
    if (isset($this->translations[$langcode]) && $langcode != LanguageInterface::LANGCODE_DEFAULT && $langcode != $this->defaultLangcode) {
971
      foreach ($this->getFieldDefinitions() as $name => $definition) {
972
        if ($definition->isTranslatable()) {
973 974
          unset($this->values[$name][$langcode]);
          unset($this->fields[$name][$langcode]);
975
        }
976
      }
977 978 979 980 981 982 983 984 985 986 987 988
      // If removing a translation which has not been saved yet, then we have
      // to remove it completely so that ::getTranslationStatus returns the
      // proper status.
      if ($this->translations[$langcode]['status'] == static::TRANSLATION_CREATED) {
        unset($this->translations[$langcode]);
      }
      else {
        if ($this->translations[$langcode]['status'] == static::TRANSLATION_EXISTING) {
          $this->translations[$langcode]['status_existed'] = TRUE;
        }
        $this->translations[$langcode]['status'] = static::TRANSLATION_REMOVED;
      }
989 990
    }
    else {
991
      throw new \InvalidArgumentException("The specified translation ($langcode) cannot be removed.");
992 993 994
    }
  }

995 996 997 998 999 1000 1001 1002 1003 1004
  /**
   * {@inheritdoc}
   */
  public function getTranslationStatus($langcode) {
    if ($langcode == $this->defaultLangcode) {
      $langcode = LanguageInterface::LANGCODE_DEFAULT;
    }
    return isset($this->translations[$langcode]) ? $this->translations[$langcode]['status'] : NULL;
  }

1005 1006 1007 1008
  /**
   * {@inheritdoc}
   */
  public function getTranslationLanguages($include_default = TRUE) {
1009
    $translations = array_filter($this->translations, function ($translation) {
1010 1011
      return $translation['status'];
    });
1012
    unset($translations[LanguageInterface::LANGCODE_DEFAULT]);
1013 1014

    if ($include_default) {
1015
      $translations[$this->defaultLangcode] = TRUE;
1016
    }
1017

1018
    // Now load language objects based upon translation langcodes.
1019
    return array_intersect_key($this->getLanguages(), $translations);
1020 1021 1022 1023 1024 1025
  }

  /**
   * Updates the original values with the interim changes.
   */
  public function updateOriginalValues() {
1026 1027 1028
    if (!$this->fields) {
      return;
    }
1029
    foreach ($this->getFieldDefinitions() as $name => $definition) {
1030
      if (!$definition->isComputed() && !empty($this->fields[$name])) {
1031 1032 1033
        foreach ($this->fields[$name] as $langcode => $item) {
          $item->filterEmptyItems();
          $this->values[$name][$langcode] = $item->getValue();
1034
        }
1035 1036 1037 1038 1039
      }
    }
  }

  /**
1040
   * Implements the magic method for getting object properties.
1041
   *
1042 1043
   * @todo: A lot of code still uses non-fields (e.g. $entity->content in view
   *   builders) by reference. Clean that up.
1044 1045
   */
  public function &__get($name) {
1046 1047
    // If this is an entity field, handle it accordingly. We first check whether
    // a field object has been already created. If not, we create one.
1048 1049
    if (isset($this->fields[$name][$this->activeLangcode])) {
      return $this->fields[$name][$this->activeLangcode];
1050
    }
1051
    // Inline getFieldDefinition() to speed things up.
1052
    if (!isset($this->fieldDefinitions)) {
1053
      $this->getFieldDefinitions();
1054 1055
    }
    if (isset($this->fieldDefinitions[$name])) {
1056
      $return = $this->getTranslatedField($name, $this->activeLangcode);
1057 1058
      return $return;
    }
1059 1060
    // Else directly read/write plain values. That way, non-field entity
    // properties can always be accessed directly.
1061 1062
    if (!isset($this->values[$name])) {
      $this->values[$name] = NULL;
1063
    }
1064
    return $this->values[$name];
1065 1066 1067
  }

  /**
1068 1069 1070
   * Implements the magic method for setting object properties.
   *
   * Uses default language always.
1071 1072
   */
  public function __set($name, $value) {
1073 1074 1075
    // Inline getFieldDefinition() to speed things up.
    if (!isset($this->fieldDefinitions)) {
      $this->getFieldDefinitions();
1076
    }
1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
    // Handle Field API fields.
    if (isset($this->fieldDefinitions[$name])) {
      // Support setting values via property objects.
      if ($value instanceof TypedDataInterface) {
        $value = $value->getValue();
      }
      // If a FieldItemList object already exists, set its value.
      if (isset($this->fields[$name][$this->activeLangcode])) {
        $this->fields[$name][$this->activeLangcode]->setValue($value);
      }
      // If not, create one.
      else {
        $this->getTranslatedField($name, $this->activeLangcode)->setValue($value);
      }
1091
    }
1092 1093 1094 1095 1096
    // The translations array is unset when cloning the entity object, we just
    // need to restore it.
    elseif ($name == 'translations') {
      $this->translations = $value;
    }
1097
    // Directly write non-field values.
1098
    else {
1099
      $this->values[$name] = $value;
1100 1101 1102 1103
    }
  }

  /**
1104
   * Implements the magic method for isset().
1105 1106
   */
  public function __isset($name) {
1107 1108 1109
    // "Official" Field API fields are always set. For non-field properties,
    // check the internal values.
    return $this->hasField($name) ? TRUE : isset($this->values[$name]);
1110 1111 1112
  }

  /**
1113
   * Implements the magic method for unset().
1114 1115
   */
  public function __unset($name) {
1116
    // Unsetting a field means emptying it.
1117
    if ($this->hasField($name)) {
1118
      $this->get($name)->setValue([]);
1119
    }
1120
    // For non-field properties, unset the internal value.
1121 1122 1123
    else {
      unset($this->values[$name]);
    }
1124 1125 1126
  }

  /**
1127
   * {@inheritdoc}
1128 1129
   */
  public function createDuplicate() {
1130
    if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
catch's avatar