EntityNG.php 19.2 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 12 13 14 15 16 17
use Drupal\Core\TypedData\TypedDataInterface;
use ArrayIterator;
use InvalidArgumentException;

/**
 * Implements Entity Field API specific enhancements to the Entity class.
 *
18 19 20 21
 * 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.
22 23 24 25 26 27
 *
 * @todo: Once all entity types have been converted, merge improvements into the
 * Entity class and overhaul the EntityInterface.
 */
class EntityNG extends Entity {

28 29 30 31 32 33 34
  /**
   * Local cache holding the value of the bundle field.
   *
   * @var string
   */
  protected $bundle;

35 36 37 38
  /**
   * The plain data values of the contained fields.
   *
   * This always holds the original, unchanged values of the entity. The values
39 40
   * are keyed by language code, whereas Language::LANGCODE_DEFAULT is used for
   * values in default language.
41 42 43 44 45 46
   *
   * @todo: Add methods for getting original fields and for determining
   * changes.
   *
   * @var array
   */
47
  protected $values = array();
48 49 50 51 52 53 54 55 56

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

  /**
57 58 59 60 61 62
   * An instance of the backward compatibility decorator.
   *
   * @var EntityBCDecorator
   */
  protected $bcEntity;

63 64 65 66 67 68 69
  /**
   * Local cache for the entity language.
   *
   * @var \Drupal\Core\Language\Language
   */
  protected $language;

70 71
  /**
   * Local cache for field definitions.
72
   *
73
   * @see EntityNG::getPropertyDefinitions()
74 75 76 77 78
   *
   * @var array
   */
  protected $fieldDefinitions;

79 80 81 82 83 84 85
  /**
   * Local cache for URI placeholder substitution values.
   *
   * @var array
   */
  protected $uriPlaceholderReplacements;

86 87 88 89 90 91 92
  /**
   * Overrides Entity::__construct().
   */
  public function __construct(array $values, $entity_type, $bundle = FALSE) {
    $this->entityType = $entity_type;
    $this->bundle = $bundle ? $bundle : $this->entityType;
    foreach ($values as $key => $value) {
93 94
      // If the key matches an existing property set the value to the property
      // to ensure non converted properties have the correct value.
95 96
      if (property_exists($this, $key) && isset($value[Language::LANGCODE_DEFAULT])) {
        $this->$key = $value[Language::LANGCODE_DEFAULT];
97
      }
98 99 100 101 102 103 104
      $this->values[$key] = $value;
    }
    $this->init();
  }

  /**
   * Gets the typed data type of the entity.
105
   *
106 107 108 109 110 111 112 113
   * @return string
   */
  public function getType() {
    return $this->entityType;
  }

  /**
   * Initialize the object. Invoked upon construction and wake up.
114
   */
115 116 117 118
  protected function init() {
    // We unset all defined properties, so magic getters apply.
    unset($this->langcode);
  }
119

120
  /**
121
   * Magic __wakeup() implementation.
122 123 124 125
   */
  public function __wakeup() {
    $this->init();
  }
126 127

  /**
128
   * Implements \Drupal\Core\Entity\EntityInterface::id().
129 130
   */
  public function id() {
131 132 133 134
    return $this->id->value;
  }

  /**
135
   * Implements \Drupal\Core\Entity\EntityInterface::bundle().
136 137 138
   */
  public function bundle() {
    return $this->bundle;
139 140 141 142 143 144 145 146 147
  }

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

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
  /**
   * {@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;
  }

203
  /**
204
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
205 206
   */
  public function get($property_name) {
207 208 209 210
    // Values in default language are always stored using the
    // Language::LANGCODE_DEFAULT constant.
    if (!isset($this->fields[$property_name][Language::LANGCODE_DEFAULT])) {
      return $this->getTranslatedField($property_name, Language::LANGCODE_DEFAULT);
211
    }
212
    return $this->fields[$property_name][Language::LANGCODE_DEFAULT];
213 214 215 216 217 218 219 220
  }

  /**
   * Gets a translated field.
   *
   * @return \Drupal\Core\Entity\Field\FieldInterface
   */
  protected function getTranslatedField($property_name, $langcode) {
221 222
    // Populate $this->fields to speed-up further look-ups and to keep track of
    // fields objects, possibly holding changes to field values.
223 224 225 226 227
    if (!isset($this->fields[$property_name][$langcode])) {
      $definition = $this->getPropertyDefinition($property_name);
      if (!$definition) {
        throw new InvalidArgumentException('Field ' . check_plain($property_name) . ' is unknown.');
      }
228 229 230 231
      // 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);
232 233
      }
      else {
234 235 236 237
        $value = NULL;
        if (isset($this->values[$property_name][$langcode])) {
          $value = $this->values[$property_name][$langcode];
        }
238 239
        // @todo Remove this once the BC decorator is gone.
        elseif ($property_name != 'langcode') {
240
          $default_langcode = $this->language()->id;
241 242 243 244
          if ($langcode == Language::LANGCODE_DEFAULT && isset($this->values[$property_name][$default_langcode])) {
            $value = $this->values[$property_name][$default_langcode];
          }
        }
245
        $this->fields[$property_name][$langcode] = \Drupal::typedData()->getPropertyInstance($this, $property_name, $value);
246 247 248 249 250 251
      }
    }
    return $this->fields[$property_name][$langcode];
  }

  /**
252
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::set().
253
   */
254 255
  public function set($property_name, $value, $notify = TRUE) {
    $this->get($property_name)->setValue($value, FALSE);
256 257 258
  }

  /**
259
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getProperties().
260 261 262 263 264 265 266 267 268 269 270 271
   */
  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;
  }

  /**
272
   * Implements \IteratorAggregate::getIterator().
273 274 275 276 277 278
   */
  public function getIterator() {
    return new ArrayIterator($this->getProperties());
  }

  /**
279
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition().
280 281
   */
  public function getPropertyDefinition($name) {
282 283 284 285 286
    if (!isset($this->fieldDefinitions)) {
      $this->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
      return $this->fieldDefinitions[$name];
287
    }
288 289
    else {
      return FALSE;
290 291 292 293
    }
  }

  /**
294
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
295 296
   */
  public function getPropertyDefinitions() {
297
    if (!isset($this->fieldDefinitions)) {
298 299
      $bundle = $this->bundle != $this->entityType ? $this->bundle : NULL;
      $this->fieldDefinitions = \Drupal::entityManager()->getFieldDefinitions($this->entityType, $bundle);
300 301
    }
    return $this->fieldDefinitions;
302 303 304
  }

  /**
305
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues().
306 307 308 309 310 311 312 313 314 315
   */
  public function getPropertyValues() {
    $values = array();
    foreach ($this->getProperties() as $name => $property) {
      $values[$name] = $property->getValue();
    }
    return $values;
  }

  /**
316
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues().
317 318 319 320 321 322 323 324
   */
  public function setPropertyValues($values) {
    foreach ($values as $name => $value) {
      $this->get($name)->setValue($value);
    }
  }

  /**
325
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
326 327 328 329 330 331 332 333 334 335 336 337 338 339
   */
  public function isEmpty() {
    if (!$this->isNew()) {
      return FALSE;
    }
    foreach ($this->getProperties() as $property) {
      if ($property->getValue() !== NULL) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
340
   * Implements \Drupal\Core\TypedData\TranslatableInterface::language().
341 342
   */
  public function language() {
343 344 345 346 347 348 349 350 351
    // 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.
      if ($this->getPropertyDefinition('langcode')) {
        $this->language = $this->get('langcode')->language;
      }
      if (empty($this->language)) {
        // Make sure we return a proper language object.
352
        $this->language = new Language(array('id' => Language::LANGCODE_NOT_SPECIFIED));
353
      }
354
    }
355 356 357 358 359 360 361 362 363 364 365
    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;
366
    }
367 368 369
  }

  /**
370
   * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation().
371
   *
372
   * @return \Drupal\Core\Entity\Plugin\DataType\EntityTranslation
373 374
   */
  public function getTranslation($langcode, $strict = TRUE) {
375 376
    // If the default language is Language::LANGCODE_NOT_SPECIFIED, the entity is not
    // translatable, so we use Language::LANGCODE_DEFAULT.
377
    if ($langcode == Language::LANGCODE_DEFAULT || in_array($this->language()->id, array(Language::LANGCODE_NOT_SPECIFIED, $langcode))) {
378 379 380 381 382
      // No translation needed, return the entity.
      return $this;
    }
    // Check whether the language code is valid, thus is of an available
    // language.
383
    $languages = language_list(Language::STATE_ALL);
384 385 386 387 388 389 390 391 392 393
    if (!isset($languages[$langcode])) {
      throw new InvalidArgumentException("Unable to get translation for the invalid language '$langcode'.");
    }
    $fields = array();
    foreach ($this->getPropertyDefinitions() as $name => $definition) {
      // Load only translatable properties in strict mode.
      if (!empty($definition['translatable']) || !$strict) {
        $fields[$name] = $this->getTranslatedField($name, $langcode);
      }
    }
394 395 396
    // @todo: Add a way to get the definition of a translation to the
    // TranslatableInterface and leverage TypeDataManager::getPropertyInstance
    // also.
397 398 399 400 401 402 403
    $translation_definition = array(
      'type' => 'entity_translation',
      'constraints' => array(
        'entity type' => $this->entityType(),
        'bundle' => $this->bundle(),
      ),
    );
404
    $translation = \Drupal::typedData()->create($translation_definition, $fields);
405
    $translation->setStrictMode($strict);
406
    $translation->setContext('@' . $langcode, $this);
407 408 409 410
    return $translation;
  }

  /**
411
   * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
412 413 414
   */
  public function getTranslationLanguages($include_default = TRUE) {
    $translations = array();
415
    $definitions = $this->getPropertyDefinitions();
416
    // Build an array with the translation langcodes set as keys. Empty
417
    // translations should not be included and must be skipped.
418 419 420 421 422 423
    foreach ($definitions as $name => $definition) {
      if (isset($this->fields[$name])) {
        foreach ($this->fields[$name] as $langcode => $field) {
          if (!$field->isEmpty()) {
            $translations[$langcode] = TRUE;
          }
424
        }
425 426 427 428 429 430 431
      }
      if (isset($this->values[$name])) {
        foreach ($this->values[$name] as $langcode => $values) {
          // If a value is there but the field object is empty, it has been
          // unset, so we need to skip the field also.
          if ($values && !empty($definition['translatable']) && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]->isEmpty())) {
            $translations[$langcode] = TRUE;
432 433
          }
        }
434 435
      }
    }
436 437 438
    // We include the default language code instead of the
    // Language::LANGCODE_DEFAULT constant.
    unset($translations[Language::LANGCODE_DEFAULT]);
439 440

    if ($include_default) {
441
      $translations[$this->language()->id] = TRUE;
442
    }
443
    // Now load language objects based upon translation langcodes.
444
    return array_intersect_key(language_list(Language::STATE_ALL), $translations);
445 446 447
  }

  /**
448
   * Overrides Entity::translations().
449
   *
450
   * @todo: Remove once Entity::translations() gets removed.
451
   */
452 453
  public function translations() {
    return $this->getTranslationLanguages(FALSE);
454 455 456
  }

  /**
457
   * Overrides Entity::getBCEntity().
458
   */
459 460
  public function getBCEntity() {
    if (!isset($this->bcEntity)) {
461 462 463
      // Initialize field definitions so that we can pass them by reference.
      $this->getPropertyDefinitions();
      $this->bcEntity = new EntityBCDecorator($this, $this->fieldDefinitions);
464 465
    }
    return $this->bcEntity;
466 467 468 469 470 471
  }

  /**
   * Updates the original values with the interim changes.
   */
  public function updateOriginalValues() {
472 473 474 475 476 477
    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) {
478
          $field->filterEmptyValues();
479 480
          $this->values[$name][$langcode] = $field->getValue();
        }
481 482 483 484 485
      }
    }
  }

  /**
486
   * Implements the magic method for setting object properties.
487
   *
488
   * Uses default language always.
489 490 491
   * For compatibility mode to work this must return a reference.
   */
  public function &__get($name) {
492 493
    // 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.
494 495
    if (isset($this->fields[$name][Language::LANGCODE_DEFAULT])) {
      return $this->fields[$name][Language::LANGCODE_DEFAULT];
496
    }
497 498 499 500 501
    // Inline getPropertyDefinition() to speed up things.
    if (!isset($this->fieldDefinitions)) {
      $this->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
502
      $return = $this->getTranslatedField($name, Language::LANGCODE_DEFAULT);
503 504
      return $return;
    }
505 506 507 508 509
    // 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;
    }
510 511
    // Else directly read/write plain values. That way, non-field entity
    // properties can always be accessed directly.
512 513
    if (!isset($this->values[$name])) {
      $this->values[$name] = NULL;
514
    }
515
    return $this->values[$name];
516 517 518
  }

  /**
519 520 521
   * Implements the magic method for setting object properties.
   *
   * Uses default language always.
522 523 524
   */
  public function __set($name, $value) {
    // Support setting values via property objects.
525
    if ($value instanceof TypedDataInterface && !$value instanceof EntityInterface) {
526 527
      $value = $value->getValue();
    }
528 529
    // 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.
530 531
    if (isset($this->fields[$name][Language::LANGCODE_DEFAULT])) {
      $this->fields[$name][Language::LANGCODE_DEFAULT]->setValue($value);
532 533
    }
    elseif ($this->getPropertyDefinition($name)) {
534
      $this->getTranslatedField($name, Language::LANGCODE_DEFAULT)->setValue($value);
535
    }
536 537
    // Else directly read/write plain values. That way, fields not yet converted
    // to the entity field API can always be directly accessed.
538
    else {
539
      $this->values[$name] = $value;
540 541 542 543
    }
  }

  /**
544
   * Implements the magic method for isset().
545 546
   */
  public function __isset($name) {
547 548
    if ($this->getPropertyDefinition($name)) {
      return $this->get($name)->getValue() !== NULL;
549
    }
550 551
    else {
      return isset($this->values[$name]);
552 553 554 555
    }
  }

  /**
556
   * Implements the magic method for unset.
557 558
   */
  public function __unset($name) {
559 560
    if ($this->getPropertyDefinition($name)) {
      $this->get($name)->setValue(NULL);
561
    }
562 563
    else {
      unset($this->values[$name]);
564 565 566 567 568 569 570 571 572
    }
  }

  /**
   * Overrides Entity::createDuplicate().
   */
  public function createDuplicate() {
    $duplicate = clone $this;
    $entity_info = $this->entityInfo();
573
    $duplicate->{$entity_info['entity_keys']['id']}->value = NULL;
574 575

    // Check if the entity type supports UUIDs and generate a new one if so.
576
    if (!empty($entity_info['entity_keys']['uuid'])) {
577
      $duplicate->{$entity_info['entity_keys']['uuid']}->applyDefaultValue();
578
    }
579 580 581 582 583 584

    // 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;
    }

585 586 587 588
    return $duplicate;
  }

  /**
589
   * Magic method: Implements a deep clone.
590 591
   */
  public function __clone() {
592 593
    $this->bcEntity = NULL;

594 595 596
    foreach ($this->fields as $name => $properties) {
      foreach ($properties as $langcode => $property) {
        $this->fields[$name][$langcode] = clone $property;
597
        $this->fields[$name][$langcode]->setContext($name, $this);
598 599 600
      }
    }
  }
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615

  /**
   * Overrides Entity::label() to access the label field with the new API.
   */
  public function label($langcode = NULL) {
    $label = NULL;
    $entity_info = $this->entityInfo();
    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;
  }
616 617 618 619 620 621 622 623

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