EntityNG.php 15.5 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 18 19
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Component\Uuid\Uuid;
use ArrayIterator;
use InvalidArgumentException;

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

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

37 38 39 40
  /**
   * The plain data values of the contained fields.
   *
   * This always holds the original, unchanged values of the entity. The values
41 42
   * are keyed by language code, whereas LANGUAGE_DEFAULT is used for values in
   * default language.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
   *
   * @todo: Add methods for getting original fields and for determining
   * changes.
   * @todo: Provide a better way for defining default values.
   *
   * @var array
   */
  protected $values = array(
    'langcode' => array(LANGUAGE_DEFAULT => array(0 => array('value' => LANGUAGE_NOT_SPECIFIED))),
  );

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

  /**
62 63 64 65 66 67 68 69
   * An instance of the backward compatibility decorator.
   *
   * @var EntityBCDecorator
   */
  protected $bcEntity;

  /**
   * Local cache for field definitions.
70
   *
71
   * @see EntityNG::getPropertyDefinitions()
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
   *
   * @var array
   */
  protected $fieldDefinitions;

  /**
   * 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) {
      $this->values[$key] = $value;
    }
    $this->init();
  }

  /**
   * Gets the typed data type of the entity.
91
   *
92 93 94 95 96 97 98 99
   * @return string
   */
  public function getType() {
    return $this->entityType;
  }

  /**
   * Initialize the object. Invoked upon construction and wake up.
100
   */
101 102 103 104
  protected function init() {
    // We unset all defined properties, so magic getters apply.
    unset($this->langcode);
  }
105

106
  /**
107
   * Magic __wakeup() implementation.
108 109 110 111
   */
  public function __wakeup() {
    $this->init();
  }
112 113

  /**
114
   * Implements \Drupal\Core\Entity\EntityInterface::id().
115 116
   */
  public function id() {
117 118 119 120
    return $this->id->value;
  }

  /**
121
   * Implements \Drupal\Core\Entity\EntityInterface::bundle().
122 123 124
   */
  public function bundle() {
    return $this->bundle;
125 126 127 128 129 130 131 132 133 134
  }

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

  /**
135
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
   */
  public function get($property_name) {
    // Values in default language are always stored using the LANGUAGE_DEFAULT
    // constant.
    if (!isset($this->fields[$property_name][LANGUAGE_DEFAULT])) {
      return $this->getTranslatedField($property_name, LANGUAGE_DEFAULT);
    }
    return $this->fields[$property_name][LANGUAGE_DEFAULT];
  }

  /**
   * Gets a translated field.
   *
   * @return \Drupal\Core\Entity\Field\FieldInterface
   */
  protected function getTranslatedField($property_name, $langcode) {
152 153
    // Populate $this->fields to speed-up further look-ups and to keep track of
    // fields objects, possibly holding changes to field values.
154 155 156 157 158
    if (!isset($this->fields[$property_name][$langcode])) {
      $definition = $this->getPropertyDefinition($property_name);
      if (!$definition) {
        throw new InvalidArgumentException('Field ' . check_plain($property_name) . ' is unknown.');
      }
159
      // Non-translatable fields are always stored with LANGUAGE_DEFAULT as key.
160 161 162 163
      if ($langcode != LANGUAGE_DEFAULT && empty($definition['translatable'])) {
        $this->fields[$property_name][$langcode] = $this->getTranslatedField($property_name, LANGUAGE_DEFAULT);
      }
      else {
164 165 166 167 168
        $value = NULL;
        if (isset($this->values[$property_name][$langcode])) {
          $value = $this->values[$property_name][$langcode];
        }
        $this->fields[$property_name][$langcode] = typed_data()->getPropertyInstance($this, $property_name, $value);
169 170 171 172 173 174
      }
    }
    return $this->fields[$property_name][$langcode];
  }

  /**
175
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::set().
176 177 178 179 180 181
   */
  public function set($property_name, $value) {
    $this->get($property_name)->setValue($value);
  }

  /**
182
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getProperties().
183 184 185 186 187 188 189 190 191 192 193 194
   */
  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;
  }

  /**
195
   * Implements \IteratorAggregate::getIterator().
196 197 198 199 200 201
   */
  public function getIterator() {
    return new ArrayIterator($this->getProperties());
  }

  /**
202
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition().
203 204
   */
  public function getPropertyDefinition($name) {
205 206 207 208 209
    if (!isset($this->fieldDefinitions)) {
      $this->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
      return $this->fieldDefinitions[$name];
210
    }
211 212
    else {
      return FALSE;
213 214 215 216
    }
  }

  /**
217
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
218 219
   */
  public function getPropertyDefinitions() {
220
    if (!isset($this->fieldDefinitions)) {
221
      $this->fieldDefinitions = drupal_container()->get('plugin.manager.entity')->getStorageController($this->entityType)->getFieldDefinitions(array(
222 223
        'EntityType' => $this->entityType,
        'Bundle' => $this->bundle,
224 225 226
      ));
    }
    return $this->fieldDefinitions;
227 228 229
  }

  /**
230
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues().
231 232 233 234 235 236 237 238 239 240
   */
  public function getPropertyValues() {
    $values = array();
    foreach ($this->getProperties() as $name => $property) {
      $values[$name] = $property->getValue();
    }
    return $values;
  }

  /**
241
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues().
242 243 244 245 246 247 248 249
   */
  public function setPropertyValues($values) {
    foreach ($values as $name => $value) {
      $this->get($name)->setValue($value);
    }
  }

  /**
250
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
251 252 253 254 255 256 257 258 259 260 261 262 263 264
   */
  public function isEmpty() {
    if (!$this->isNew()) {
      return FALSE;
    }
    foreach ($this->getProperties() as $property) {
      if ($property->getValue() !== NULL) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
265
   * Implements \Drupal\Core\TypedData\TranslatableInterface::language().
266 267
   */
  public function language() {
268 269 270 271 272
    // Get the language code if the property exists.
    if ($this->getPropertyDefinition('langcode')) {
      $language = $this->get('langcode')->language;
    }
    if (!isset($language)) {
273
      // Make sure we return a proper language object.
274
      $language = new Language(array('langcode' => LANGUAGE_NOT_SPECIFIED));
275 276
    }
    return $language;
277 278 279
  }

  /**
280
   * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation().
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
   *
   * @return \Drupal\Core\Entity\Field\Type\EntityTranslation
   */
  public function getTranslation($langcode, $strict = TRUE) {
    // If the default language is LANGUAGE_NOT_SPECIFIED, the entity is not
    // translatable, so we use LANGUAGE_DEFAULT.
    if ($langcode == LANGUAGE_DEFAULT || in_array($this->language()->langcode, array(LANGUAGE_NOT_SPECIFIED, $langcode))) {
      // No translation needed, return the entity.
      return $this;
    }
    // Check whether the language code is valid, thus is of an available
    // language.
    $languages = language_list(LANGUAGE_ALL);
    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);
      }
    }
304 305 306
    // @todo: Add a way to get the definition of a translation to the
    // TranslatableInterface and leverage TypeDataManager::getPropertyInstance
    // also.
307 308 309 310 311 312 313
    $translation_definition = array(
      'type' => 'entity_translation',
      'constraints' => array(
        'entity type' => $this->entityType(),
        'bundle' => $this->bundle(),
      ),
    );
314
    $translation = typed_data()->create($translation_definition, $fields);
315
    $translation->setStrictMode($strict);
316 317 318
    if ($translation instanceof ContextAwareInterface) {
      $translation->setContext('@' . $langcode, $this);
    }
319 320 321 322
    return $translation;
  }

  /**
323
   * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
324 325 326
   */
  public function getTranslationLanguages($include_default = TRUE) {
    $translations = array();
327
    // Build an array with the translation langcodes set as keys. Empty
328
    // translations should not be included and must be skipped.
329
    foreach ($this->getProperties() as $name => $property) {
330 331 332 333 334 335
      foreach ($this->fields[$name] as $langcode => $field) {
        if (!$field->isEmpty()) {
          $translations[$langcode] = TRUE;
        }
        if (isset($this->values[$name])) {
          foreach ($this->values[$name] as $langcode => $values) {
336 337
            // If a value is there but the field object is empty, it has been
            // unset, so we need to skip the field also.
338 339 340 341 342
            if ($values && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]->isEmpty())) {
              $translations[$langcode] = TRUE;
            }
          }
        }
343 344
      }
    }
345 346
    // We include the default language code instead of the LANGUAGE_DEFAULT
    // constant.
347 348 349 350 351
    unset($translations[LANGUAGE_DEFAULT]);

    if ($include_default) {
      $translations[$this->language()->langcode] = TRUE;
    }
352
    // Now load language objects based upon translation langcodes.
353
    return array_intersect_key(language_list(LANGUAGE_ALL), $translations);
354 355 356
  }

  /**
357
   * Overrides Entity::translations().
358
   *
359
   * @todo: Remove once Entity::translations() gets removed.
360
   */
361 362
  public function translations() {
    return $this->getTranslationLanguages(FALSE);
363 364 365
  }

  /**
366
   * Overrides Entity::getBCEntity().
367
   */
368 369 370 371 372
  public function getBCEntity() {
    if (!isset($this->bcEntity)) {
      $this->bcEntity = new EntityBCDecorator($this);
    }
    return $this->bcEntity;
373 374 375 376 377 378
  }

  /**
   * Updates the original values with the interim changes.
   */
  public function updateOriginalValues() {
379 380 381 382 383 384 385 386
    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) {
          $this->values[$name][$langcode] = $field->getValue();
        }
387 388 389 390 391
      }
    }
  }

  /**
392
   * Implements the magic method for setting object properties.
393
   *
394
   * Uses default language always.
395 396 397
   * For compatibility mode to work this must return a reference.
   */
  public function &__get($name) {
398 399
    // 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.
400 401 402
    if (isset($this->fields[$name][LANGUAGE_DEFAULT])) {
      return $this->fields[$name][LANGUAGE_DEFAULT];
    }
403 404 405 406 407 408
    // Inline getPropertyDefinition() to speed up things.
    if (!isset($this->fieldDefinitions)) {
      $this->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
      $return = $this->getTranslatedField($name, LANGUAGE_DEFAULT);
409 410
      return $return;
    }
411 412 413 414 415
    // 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;
    }
416 417
    // Else directly read/write plain values. That way, non-field entity
    // properties can always be accessed directly.
418 419
    if (!isset($this->values[$name])) {
      $this->values[$name] = NULL;
420
    }
421
    return $this->values[$name];
422 423 424
  }

  /**
425 426 427
   * Implements the magic method for setting object properties.
   *
   * Uses default language always.
428 429 430 431 432 433
   */
  public function __set($name, $value) {
    // Support setting values via property objects.
    if ($value instanceof TypedDataInterface) {
      $value = $value->getValue();
    }
434 435
    // 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.
436
    if (isset($this->fields[$name][LANGUAGE_DEFAULT])) {
437 438 439
      $this->fields[$name][LANGUAGE_DEFAULT]->setValue($value);
    }
    elseif ($this->getPropertyDefinition($name)) {
440
      $this->getTranslatedField($name, LANGUAGE_DEFAULT)->setValue($value);
441
    }
442 443
    // Else directly read/write plain values. That way, fields not yet converted
    // to the entity field API can always be directly accessed.
444
    else {
445
      $this->values[$name] = $value;
446 447 448 449
    }
  }

  /**
450
   * Implements the magic method for isset().
451 452
   */
  public function __isset($name) {
453 454
    if ($this->getPropertyDefinition($name)) {
      return $this->get($name)->getValue() !== NULL;
455
    }
456 457
    else {
      return isset($this->values[$name]);
458 459 460 461
    }
  }

  /**
462
   * Implements the magic method for unset.
463 464
   */
  public function __unset($name) {
465 466
    if ($this->getPropertyDefinition($name)) {
      $this->get($name)->setValue(NULL);
467
    }
468 469
    else {
      unset($this->values[$name]);
470 471 472 473 474 475 476 477 478
    }
  }

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

    // Check if the entity type supports UUIDs and generate a new one if so.
482
    if (!empty($entity_info['entity_keys']['uuid'])) {
483
      $uuid = new Uuid();
484
      $duplicate->{$entity_info['entity_keys']['uuid']}->value = $uuid->generate();
485 486 487 488 489
    }
    return $duplicate;
  }

  /**
490
   * Magic method: Implements a deep clone.
491 492
   */
  public function __clone() {
493 494
    $this->bcEntity = NULL;

495 496 497 498
    foreach ($this->fields as $name => $properties) {
      foreach ($properties as $langcode => $property) {
        $this->fields[$name][$langcode] = clone $property;
        if ($property instanceof ContextAwareInterface) {
499
          $this->fields[$name][$langcode]->setContext($name, $this);
500 501 502 503
        }
      }
    }
  }
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518

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