EntityNG.php 26.3 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Entity\EntityNG.
6 7 8 9
 */

namespace Drupal\Core\Entity;

10
use Drupal\Core\Language\Language;
11
use Drupal\Core\Session\AccountInterface;
12 13 14 15 16 17 18
use Drupal\Core\TypedData\TypedDataInterface;
use ArrayIterator;
use InvalidArgumentException;

/**
 * Implements Entity Field API specific enhancements to the Entity class.
 *
19 20 21 22
 * Entity(..)NG classes are variants of the Entity(...) classes that implement
 * the next generation (NG) entity field API. They exist during conversion to
 * the new API only and changes will be merged into the respective original
 * classes once the conversion is complete.
23 24 25 26 27 28
 *
 * @todo: Once all entity types have been converted, merge improvements into the
 * Entity class and overhaul the EntityInterface.
 */
class EntityNG extends Entity {

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
  /**
   * Status code indentifying a removed translation.
   */
  const TRANSLATION_REMOVED = 0;

  /**
   * Status code indentifying an existing translation.
   */
  const TRANSLATION_EXISTING = 1;

  /**
   * Status code indentifying a newly created translation.
   */
  const TRANSLATION_CREATED = 2;

44 45 46 47 48 49 50
  /**
   * Local cache holding the value of the bundle field.
   *
   * @var string
   */
  protected $bundle;

51 52 53 54
  /**
   * The plain data values of the contained fields.
   *
   * This always holds the original, unchanged values of the entity. The values
55 56
   * are keyed by language code, whereas Language::LANGCODE_DEFAULT is used for
   * values in default language.
57 58 59 60 61 62
   *
   * @todo: Add methods for getting original fields and for determining
   * changes.
   *
   * @var array
   */
63
  protected $values = array();
64 65 66 67 68 69 70 71 72

  /**
   * The array of fields, each being an instance of FieldInterface.
   *
   * @var array
   */
  protected $fields = array();

  /**
73 74 75 76 77 78
   * An instance of the backward compatibility decorator.
   *
   * @var EntityBCDecorator
   */
  protected $bcEntity;

79 80 81 82 83 84 85
  /**
   * Local cache for the entity language.
   *
   * @var \Drupal\Core\Language\Language
   */
  protected $language;

86 87 88 89 90 91 92
  /**
   * Local cache for the available language objects.
   *
   * @var array
   */
  protected $languages;

93 94
  /**
   * Local cache for field definitions.
95
   *
96
   * @see EntityNG::getPropertyDefinitions()
97 98 99 100 101
   *
   * @var array
   */
  protected $fieldDefinitions;

102 103 104 105 106 107 108
  /**
   * Local cache for URI placeholder substitution values.
   *
   * @var array
   */
  protected $uriPlaceholderReplacements;

109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  /**
   * Language code identifying the entity active language.
   *
   * This is the language field accessors will use to determine which field
   * values manipulate.
   *
   * @var string
   */
  protected $activeLangcode = Language::LANGCODE_DEFAULT;

  /**
   * An array of entity translation metadata.
   *
   * An associative array keyed by translation language code. Every value is an
   * array containg the translation status and the translation object, if it has
   * already been instantiated.
   *
   * @var array
   */
  protected $translations = array();

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

137 138 139
  /**
   * Overrides Entity::__construct().
   */
140
  public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) {
141 142
    $this->entityType = $entity_type;
    $this->bundle = $bundle ? $bundle : $this->entityType;
143 144
    $this->languages = language_list(Language::STATE_ALL);

145
    foreach ($values as $key => $value) {
146 147
      // If the key matches an existing property set the value to the property
      // to ensure non converted properties have the correct value.
148 149
      if (property_exists($this, $key) && isset($value[Language::LANGCODE_DEFAULT])) {
        $this->$key = $value[Language::LANGCODE_DEFAULT];
150
      }
151 152
      $this->values[$key] = $value;
    }
153 154 155 156 157 158 159 160 161 162 163 164 165 166

    // Initialize translations. Ensure we have at least an entry for the entity
    // original language.
    $data = array('status' => static::TRANSLATION_EXISTING);
    $this->translations[Language::LANGCODE_DEFAULT] = $data;
    if ($translations) {
      $default_langcode = $this->language()->id;
      foreach ($translations as $langcode) {
        if ($langcode != $default_langcode && $langcode != Language::LANGCODE_DEFAULT) {
          $this->translations[$langcode] = $data;
        }
      }
    }

167 168 169 170 171
    $this->init();
  }

  /**
   * Gets the typed data type of the entity.
172
   *
173 174 175 176 177 178 179 180
   * @return string
   */
  public function getType() {
    return $this->entityType;
  }

  /**
   * Initialize the object. Invoked upon construction and wake up.
181
   */
182 183 184 185
  protected function init() {
    // We unset all defined properties, so magic getters apply.
    unset($this->langcode);
  }
186

187 188 189 190 191 192 193 194 195
  /**
   * Clear entity translation object cache to remove stale references.
   */
  protected function clearTranslationCache() {
    foreach ($this->translations as &$translation) {
      unset($translation['entity']);
    }
  }

196
  /**
197
   * Magic __wakeup() implementation.
198 199 200
   */
  public function __wakeup() {
    $this->init();
201 202 203 204
    // @todo This should be done before serializing the entity, but we would
    //   need to provide the full list of data to be serialized. See the
    //   dedicated issue at https://drupal.org/node/2027795.
    $this->clearTranslationCache();
205
  }
206 207

  /**
208
   * Implements \Drupal\Core\Entity\EntityInterface::id().
209 210
   */
  public function id() {
211 212 213 214
    return $this->id->value;
  }

  /**
215
   * Implements \Drupal\Core\Entity\EntityInterface::bundle().
216 217 218
   */
  public function bundle() {
    return $this->bundle;
219 220 221 222 223 224 225 226 227
  }

  /**
   * Overrides Entity::uuid().
   */
  public function uuid() {
    return $this->get('uuid')->value;
  }

228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  /**
   * {@inheritdoc}
   */
  public function uri($rel = 'canonical') {
    $entity_info = $this->entityInfo();

    $link_templates = isset($entity_info['links']) ? $entity_info['links'] : array();

    if (isset($link_templates[$rel])) {
      $template = $link_templates[$rel];
      $replacements = $this->uriPlaceholderReplacements();
      $uri['path'] = str_replace(array_keys($replacements), array_values($replacements), $template);

      // @todo Remove this once http://drupal.org/node/1888424 is in and we can
      //   move the BC handling of / vs. no-/ to the generator.
      $uri['path'] = trim($uri['path'], '/');

      // Pass the entity data to url() so that alter functions do not need to
      // look up this entity again.
      $uri['options']['entity_type'] = $this->entityType;
      $uri['options']['entity'] = $this;
      return $uri;
    }

    // For a canonical link (that is, a link to self), look up the stack for
    // default logic. Other relationship types are not supported by parent
    // classes.
    if ($rel == 'canonical') {
      return parent::uri();
    }
  }

  /**
   * Returns an array of placeholders for this entity.
   *
   * 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.
   *
   * @return array
   *   An array of URI placeholders.
   */
  protected function uriPlaceholderReplacements() {
    if (empty($this->uriPlaceholderReplacements)) {
      $this->uriPlaceholderReplacements = array(
        '{entityType}' => $this->entityType(),
        '{bundle}' => $this->bundle(),
        '{id}' => $this->id(),
        '{uuid}' => $this->uuid(),
        '{' . $this->entityType() . '}' => $this->id(),
      );
    }
    return $this->uriPlaceholderReplacements;
  }

283
  /**
284
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
285 286
   */
  public function get($property_name) {
287 288
    if (!isset($this->fields[$property_name][$this->activeLangcode])) {
      return $this->getTranslatedField($property_name, $this->activeLangcode);
289
    }
290
    return $this->fields[$property_name][$this->activeLangcode];
291 292 293 294 295 296 297 298
  }

  /**
   * Gets a translated field.
   *
   * @return \Drupal\Core\Entity\Field\FieldInterface
   */
  protected function getTranslatedField($property_name, $langcode) {
299 300 301 302
    if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
      $message = 'The entity object refers to a removed translation (@langcode) and cannot be manipulated.';
      throw new \InvalidArgumentException(format_string($message, array('@langcode' => $this->activeLangcode)));
    }
303 304
    // Populate $this->fields to speed-up further look-ups and to keep track of
    // fields objects, possibly holding changes to field values.
305 306 307 308 309
    if (!isset($this->fields[$property_name][$langcode])) {
      $definition = $this->getPropertyDefinition($property_name);
      if (!$definition) {
        throw new InvalidArgumentException('Field ' . check_plain($property_name) . ' is unknown.');
      }
310 311 312 313
      // Non-translatable fields are always stored with
      // Language::LANGCODE_DEFAULT as key.
      if ($langcode != Language::LANGCODE_DEFAULT && empty($definition['translatable'])) {
        $this->fields[$property_name][$langcode] = $this->getTranslatedField($property_name, Language::LANGCODE_DEFAULT);
314 315
      }
      else {
316 317 318 319
        $value = NULL;
        if (isset($this->values[$property_name][$langcode])) {
          $value = $this->values[$property_name][$langcode];
        }
320
        // @todo Remove this once the BC decorator is gone.
321
        elseif ($property_name != 'langcode' && $langcode == Language::LANGCODE_DEFAULT) {
322
          $default_langcode = $this->language()->id;
323
          if (isset($this->values[$property_name][$default_langcode])) {
324 325 326
            $value = $this->values[$property_name][$default_langcode];
          }
        }
327
        $this->fields[$property_name][$langcode] = \Drupal::typedData()->getPropertyInstance($this, $property_name, $value);
328 329 330 331 332 333
      }
    }
    return $this->fields[$property_name][$langcode];
  }

  /**
334
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::set().
335
   */
336 337
  public function set($property_name, $value, $notify = TRUE) {
    $this->get($property_name)->setValue($value, FALSE);
338 339 340
  }

  /**
341
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getProperties().
342 343 344 345 346 347 348 349 350 351 352 353
   */
  public function getProperties($include_computed = FALSE) {
    $properties = array();
    foreach ($this->getPropertyDefinitions() as $name => $definition) {
      if ($include_computed || empty($definition['computed'])) {
        $properties[$name] = $this->get($name);
      }
    }
    return $properties;
  }

  /**
354
   * Implements \IteratorAggregate::getIterator().
355 356 357 358 359 360
   */
  public function getIterator() {
    return new ArrayIterator($this->getProperties());
  }

  /**
361
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition().
362 363
   */
  public function getPropertyDefinition($name) {
364 365 366 367 368
    if (!isset($this->fieldDefinitions)) {
      $this->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
      return $this->fieldDefinitions[$name];
369
    }
370 371
    else {
      return FALSE;
372 373 374 375
    }
  }

  /**
376
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
377 378
   */
  public function getPropertyDefinitions() {
379
    if (!isset($this->fieldDefinitions)) {
380 381
      $bundle = $this->bundle != $this->entityType ? $this->bundle : NULL;
      $this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityType, $bundle);
382 383
    }
    return $this->fieldDefinitions;
384 385 386
  }

  /**
387
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues().
388 389 390 391 392 393 394 395 396 397
   */
  public function getPropertyValues() {
    $values = array();
    foreach ($this->getProperties() as $name => $property) {
      $values[$name] = $property->getValue();
    }
    return $values;
  }

  /**
398
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues().
399 400 401 402 403 404 405 406
   */
  public function setPropertyValues($values) {
    foreach ($values as $name => $value) {
      $this->get($name)->setValue($value);
    }
  }

  /**
407
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
408 409 410 411 412 413 414 415 416 417 418 419 420 421
   */
  public function isEmpty() {
    if (!$this->isNew()) {
      return FALSE;
    }
    foreach ($this->getProperties() as $property) {
      if ($property->getValue() !== NULL) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
422 423 424 425 426 427 428 429 430 431
   * {@inheritdoc}
   */
  public function access($operation = 'view', AccountInterface $account = NULL) {
    return \Drupal::entityManager()
      ->getAccessController($this->entityType)
      ->access($this, $operation, $this->activeLangcode, $account);
  }

  /**
   * {@inheritdoc}
432 433
   */
  public function language() {
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
    if ($this->activeLangcode != Language::LANGCODE_DEFAULT) {
      if (!isset($this->languages[$this->activeLangcode])) {
        $this->languages += language_list(Language::STATE_ALL);
      }
      return $this->languages[$this->activeLangcode];
    }
    else {
      return $this->language ?: $this->getDefaultLanguage();
    }
  }

  /**
   * Returns the entity original language.
   *
   * @return \Drupal\Core\Language\Language
   *   A language object.
   */
  protected function getDefaultLanguage() {
452 453 454 455
    // Keep a local cache of the language object and clear it if the langcode
    // gets changed, see EntityNG::onChange().
    if (!isset($this->language)) {
      // Get the language code if the property exists.
456 457
      if ($this->getPropertyDefinition('langcode') && ($item = $this->get('langcode')) && isset($item->language)) {
        $this->language = $item->language;
458 459 460
      }
      if (empty($this->language)) {
        // Make sure we return a proper language object.
461
        $this->language = new Language(array('id' => Language::LANGCODE_NOT_SPECIFIED));
462
      }
463
    }
464 465 466 467 468 469 470 471 472 473 474
    return $this->language;
  }

  /**
   * {@inheritdoc}
   */
  public function onChange($property_name) {
    if ($property_name == 'langcode') {
      // Avoid using unset as this unnecessarily triggers magic methods later
      // on.
      $this->language = NULL;
475
    }
476 477 478
  }

  /**
479
   * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation().
480
   *
481 482 483 484 485 486 487 488 489 490
   * @return \Drupal\Core\Entity\EntityInterface
   */
  public function getTranslation($langcode) {
    // Ensure we always use the default language code when dealing with the
    // original entity language.
    if ($langcode != Language::LANGCODE_DEFAULT) {
      $default_language = $this->language ?: $this->getDefaultLanguage();
      if ($langcode == $default_language->id) {
        $langcode = Language::LANGCODE_DEFAULT;
      }
491
    }
492 493 494 495 496

    // Populate entity translation object cache so it will be available for all
    // translation objects.
    if ($langcode == $this->activeLangcode) {
      $this->translations[$langcode]['entity'] = $this;
497
    }
498 499 500 501 502 503 504 505 506 507

    // 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'];
    }
    else {
      if (isset($this->translations[$langcode])) {
        $translation = $this->initializeTranslation($langcode);
        $this->translations[$langcode]['entity'] = $translation;
508
      }
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
      else {
        // If we were given a valid language and there is no translation for it,
        // we return a new one.
        $languages = language_list(Language::STATE_ALL);
        if (isset($languages[$langcode])) {
          // If the entity or the requested language  is not a configured
          // language, we fall back to the entity itself, since in this case it
          // cannot have translations.
          $translation = empty($this->getDefaultLanguage()->locked) && empty($languages[$langcode]->locked) ? $this->addTranslation($langcode) : $this;
        }
      }
    }

    if (empty($translation)) {
      $message = 'Invalid translation language (@langcode) specified.';
      throw new \InvalidArgumentException(format_string($message, array('@langcode' => $langcode)));
525
    }
526

527 528 529 530
    return $translation;
  }

  /**
531
   * {@inheritdoc}
532
   */
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
  public function getUntranslated() {
    $langcode = Language::LANGCODE_DEFAULT;
    return isset($this->translations[$langcode]['entity']) ? $this->translations[$langcode]['entity'] : $this->getTranslation($langcode);
  }

  /**
   * 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;
    $translation->translationInitialize = FALSE;

    return $translation;
  }

  /**
   * {@inheritdoc}
   */
  public function hasTranslation($langcode) {
    return !empty($this->translations[$langcode]['status']);
  }

  /**
   * {@inheritdoc}
   */
  public function addTranslation($langcode, array $values = array()) {
    $languages = language_list(Language::STATE_ALL);
    if (!isset($languages[$langcode]) || $this->hasTranslation($langcode)) {
      $message = 'Invalid translation language (@langcode) specified.';
      throw new \InvalidArgumentException(format_string($message, array('@langcode' => $langcode)));
    }

    // Instantiate a new empty entity so default values will be populated in the
    // specified language.
    $info = $this->entityInfo();
    $default_values = array($info['entity_keys']['bundle'] => $this->bundle, 'langcode' => $langcode);
    $entity = \Drupal::entityManager()
      ->getStorageController($this->entityType())
      ->create($default_values);

    foreach ($entity as $name => $field) {
      if (!isset($values[$name]) && !$field->isEmpty()) {
        $values[$name] = $field->value;
601
      }
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625
    }

    $this->translations[$langcode]['status'] = static::TRANSLATION_CREATED;
    $translation = $this->getTranslation($langcode);
    $definitions = $translation->getPropertyDefinitions();

    foreach ($values as $name => $value) {
      if (isset($definitions[$name]) && !empty($definitions[$name]['translatable'])) {
        $translation->$name = $value;
      }
    }

    return $translation;
  }

  /**
   * {@inheritdoc}
   */
  public function removeTranslation($langcode) {
    if (isset($this->translations[$langcode]) && $langcode != Language::LANGCODE_DEFAULT && $langcode != $this->getDefaultLanguage()->id) {
      foreach ($this->getPropertyDefinitions() as $name => $definition) {
        if (!empty($definition['translatable'])) {
          unset($this->values[$langcode]);
          unset($this->fields[$langcode]);
626
        }
627
      }
628 629 630 631 632 633 634 635 636 637 638 639 640 641
      $this->translations[$langcode]['status'] = static::TRANSLATION_REMOVED;
    }
    else {
      $message = 'The specified translation (@langcode) cannot be removed.';
      throw new \InvalidArgumentException(format_string($message, array('@langcode' => $langcode)));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function initTranslation($langcode) {
    if ($langcode != Language::LANGCODE_DEFAULT && $langcode != $this->getDefaultLanguage()->id) {
      $this->translations[$langcode]['status'] = static::TRANSLATION_EXISTING;
642
    }
643 644 645 646 647 648 649
  }

  /**
   * {@inheritdoc}
   */
  public function getTranslationLanguages($include_default = TRUE) {
    $translations = array_filter($this->translations, function($translation) { return $translation['status']; });
650
    unset($translations[Language::LANGCODE_DEFAULT]);
651 652

    if ($include_default) {
653 654
      $langcode = $this->getDefaultLanguage()->id;
      $translations[$langcode] = TRUE;
655
    }
656

657
    // Now load language objects based upon translation langcodes.
658
    return array_intersect_key(language_list(Language::STATE_ALL), $translations);
659 660 661
  }

  /**
662
   * Overrides Entity::translations().
663
   *
664
   * @todo: Remove once Entity::translations() gets removed.
665
   */
666 667
  public function translations() {
    return $this->getTranslationLanguages(FALSE);
668 669 670
  }

  /**
671
   * Overrides Entity::getBCEntity().
672
   */
673 674
  public function getBCEntity() {
    if (!isset($this->bcEntity)) {
675 676 677
      // Initialize field definitions so that we can pass them by reference.
      $this->getPropertyDefinitions();
      $this->bcEntity = new EntityBCDecorator($this, $this->fieldDefinitions);
678 679
    }
    return $this->bcEntity;
680 681 682 683 684 685
  }

  /**
   * Updates the original values with the interim changes.
   */
  public function updateOriginalValues() {
686 687 688 689 690 691
    if (!$this->fields) {
      return;
    }
    foreach ($this->getPropertyDefinitions() as $name => $definition) {
      if (empty($definition['computed']) && !empty($this->fields[$name])) {
        foreach ($this->fields[$name] as $langcode => $field) {
692
          $field->filterEmptyValues();
693 694
          $this->values[$name][$langcode] = $field->getValue();
        }
695 696 697 698 699
      }
    }
  }

  /**
700
   * Implements the magic method for setting object properties.
701
   *
702
   * Uses default language always.
703 704 705
   * For compatibility mode to work this must return a reference.
   */
  public function &__get($name) {
706 707
    // 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.
708 709
    if (isset($this->fields[$name][$this->activeLangcode])) {
      return $this->fields[$name][$this->activeLangcode];
710
    }
711 712 713 714 715
    // Inline getPropertyDefinition() to speed up things.
    if (!isset($this->fieldDefinitions)) {
      $this->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
716
      $return = $this->getTranslatedField($name, $this->activeLangcode);
717 718
      return $return;
    }
719 720 721 722 723
    // Allow the EntityBCDecorator to directly access the values and fields.
    // @todo: Remove once the EntityBCDecorator gets removed.
    if ($name == 'values' || $name == 'fields') {
      return $this->$name;
    }
724 725
    // Else directly read/write plain values. That way, non-field entity
    // properties can always be accessed directly.
726 727
    if (!isset($this->values[$name])) {
      $this->values[$name] = NULL;
728
    }
729
    return $this->values[$name];
730 731 732
  }

  /**
733 734 735
   * Implements the magic method for setting object properties.
   *
   * Uses default language always.
736 737 738
   */
  public function __set($name, $value) {
    // Support setting values via property objects.
739
    if ($value instanceof TypedDataInterface && !$value instanceof EntityInterface) {
740 741
      $value = $value->getValue();
    }
742 743
    // 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.
744 745
    if (isset($this->fields[$name][$this->activeLangcode])) {
      $this->fields[$name][$this->activeLangcode]->setValue($value);
746 747
    }
    elseif ($this->getPropertyDefinition($name)) {
748
      $this->getTranslatedField($name, $this->activeLangcode)->setValue($value);
749
    }
750 751
    // Else directly read/write plain values. That way, fields not yet converted
    // to the entity field API can always be directly accessed.
752
    else {
753
      $this->values[$name] = $value;
754 755 756 757
    }
  }

  /**
758
   * Implements the magic method for isset().
759 760
   */
  public function __isset($name) {
761 762
    if ($this->getPropertyDefinition($name)) {
      return $this->get($name)->getValue() !== NULL;
763
    }
764 765
    else {
      return isset($this->values[$name]);
766 767 768 769
    }
  }

  /**
770
   * Implements the magic method for unset.
771 772
   */
  public function __unset($name) {
773 774
    if ($this->getPropertyDefinition($name)) {
      $this->get($name)->setValue(NULL);
775
    }
776 777
    else {
      unset($this->values[$name]);
778 779 780 781 782 783 784
    }
  }

  /**
   * Overrides Entity::createDuplicate().
   */
  public function createDuplicate() {
785 786 787 788 789
    if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
      $message = 'The entity object refers to a removed translation (@langcode) and cannot be manipulated.';
      throw new \InvalidArgumentException(format_string($message, array('@langcode' => $this->activeLangcode)));
    }

790 791
    $duplicate = clone $this;
    $entity_info = $this->entityInfo();
792
    $duplicate->{$entity_info['entity_keys']['id']}->value = NULL;
793 794

    // Check if the entity type supports UUIDs and generate a new one if so.
795
    if (!empty($entity_info['entity_keys']['uuid'])) {
796
      $duplicate->{$entity_info['entity_keys']['uuid']}->applyDefaultValue();
797
    }
798 799 800 801 802 803

    // Check whether the entity type supports revisions and initialize it if so.
    if (!empty($entity_info['entity_keys']['revision'])) {
      $duplicate->{$entity_info['entity_keys']['revision']}->value = NULL;
    }

804 805 806 807
    return $duplicate;
  }

  /**
808
   * Magic method: Implements a deep clone.
809 810
   */
  public function __clone() {
811 812
    $this->bcEntity = NULL;

813 814 815 816 817 818 819 820
    // Avoid deep-cloning when we are initializing a translation object, since
    // it will represent the same entity, only with a different active language.
    if (!$this->translationInitialize) {
      foreach ($this->fields as $name => $properties) {
        foreach ($properties as $langcode => $property) {
          $this->fields[$name][$langcode] = clone $property;
          $this->fields[$name][$langcode]->setContext($name, $this);
        }
821
      }
822
      $this->clearTranslationCache();
823 824
    }
  }
825 826 827 828 829 830 831

  /**
   * Overrides Entity::label() to access the label field with the new API.
   */
  public function label($langcode = NULL) {
    $label = NULL;
    $entity_info = $this->entityInfo();
832 833 834
    if (!isset($langcode)) {
      $langcode = $this->activeLangcode;
    }
835 836 837 838 839 840 841 842
    if (isset($entity_info['label_callback']) && function_exists($entity_info['label_callback'])) {
      $label = $entity_info['label_callback']($this->entityType, $this, $langcode);
    }
    elseif (!empty($entity_info['entity_keys']['label']) && isset($this->{$entity_info['entity_keys']['label']})) {
      $label = $this->{$entity_info['entity_keys']['label']}->value;
    }
    return $label;
  }
843 844 845 846 847 848 849 850

  /**
   * {@inheritdoc}
   */
  public function validate() {
    // @todo: Add the typed data manager as proper dependency.
    return \Drupal::typedData()->getValidator()->validate($this);
  }
851

852
}