EntityNG.php

Contains \Drupal\Core\Entity\EntityNG.

Namespace

Drupal\Core\Entity

File

drupal/core/lib/Drupal/Core/Entity/EntityNG.php
View source
<?php

/**
 * @file
 * Contains \Drupal\Core\Entity\EntityNG.
 */
namespace Drupal\Core\Entity;

use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Component\Uuid\Uuid;
use ArrayIterator;
use InvalidArgumentException;

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

  /**
   * Local cache holding the value of the bundle field.
   *
   * @var string
   */
  protected $bundle;

  /**
   * The plain data values of the contained fields.
   *
   * This always holds the original, unchanged values of the entity. The values
   * are keyed by language code, whereas Language::LANGCODE_DEFAULT is used for
   * values in default language.
   *
   * @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::LANGCODE_DEFAULT => array(
        0 => array(
          'value' => Language::LANGCODE_NOT_SPECIFIED,
        ),
      ),
    ),
  );

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

  /**
   * An instance of the backward compatibility decorator.
   *
   * @var EntityBCDecorator
   */
  protected $bcEntity;

  /**
   * Local cache for field definitions.
   *
   * @see EntityNG::getPropertyDefinitions()
   *
   * @var array
   */
  protected $fieldDefinitions;

  /**
   * Local cache for URI placeholder substitution values.
   *
   * @var array
   */
  protected $uriPlaceholderReplacements;

  /**
   * 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) {

      // If the key matches an existing property set the value to the property
      // to ensure non converted properties have the correct value.
      if (property_exists($this, $key) && isset($value[Language::LANGCODE_DEFAULT])) {
        $this->{$key} = $value[Language::LANGCODE_DEFAULT];
      }
      $this->values[$key] = $value;
    }
    $this
      ->init();
  }

  /**
   * Gets the typed data type of the entity.
   *
   * @return string
   */
  public function getType() {
    return $this->entityType;
  }

  /**
   * Initialize the object. Invoked upon construction and wake up.
   */
  protected function init() {

    // We unset all defined properties, so magic getters apply.
    unset($this->langcode);
  }

  /**
   * Magic __wakeup() implementation.
   */
  public function __wakeup() {
    $this
      ->init();
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::id().
   */
  public function id() {
    return $this->id->value;
  }

  /**
   * Implements \Drupal\Core\Entity\EntityInterface::bundle().
   */
  public function bundle() {
    return $this->bundle;
  }

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

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

  /**
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
   */
  public function get($property_name) {

    // 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);
    }
    return $this->fields[$property_name][Language::LANGCODE_DEFAULT];
  }

  /**
   * Gets a translated field.
   *
   * @return \Drupal\Core\Entity\Field\FieldInterface
   */
  protected function getTranslatedField($property_name, $langcode) {

    // Populate $this->fields to speed-up further look-ups and to keep track of
    // fields objects, possibly holding changes to field values.
    if (!isset($this->fields[$property_name][$langcode])) {
      $definition = $this
        ->getPropertyDefinition($property_name);
      if (!$definition) {
        throw new InvalidArgumentException('Field ' . check_plain($property_name) . ' is unknown.');
      }

      // 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);
      }
      else {
        $value = NULL;
        if (isset($this->values[$property_name][$langcode])) {
          $value = $this->values[$property_name][$langcode];
        }
        $this->fields[$property_name][$langcode] = \Drupal::typedData()
          ->getPropertyInstance($this, $property_name, $value);
      }
    }
    return $this->fields[$property_name][$langcode];
  }

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

  /**
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getProperties().
   */
  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;
  }

  /**
   * Implements \IteratorAggregate::getIterator().
   */
  public function getIterator() {
    return new ArrayIterator($this
      ->getProperties());
  }

  /**
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinition().
   */
  public function getPropertyDefinition($name) {
    if (!isset($this->fieldDefinitions)) {
      $this
        ->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
      return $this->fieldDefinitions[$name];
    }
    else {
      return FALSE;
    }
  }

  /**
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyDefinitions().
   */
  public function getPropertyDefinitions() {
    if (!isset($this->fieldDefinitions)) {
      $this->fieldDefinitions = \Drupal::entityManager()
        ->getStorageController($this->entityType)
        ->getFieldDefinitions(array(
        'EntityType' => $this->entityType,
        'Bundle' => $this->bundle,
      ));
    }
    return $this->fieldDefinitions;
  }

  /**
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::getPropertyValues().
   */
  public function getPropertyValues() {
    $values = array();
    foreach ($this
      ->getProperties() as $name => $property) {
      $values[$name] = $property
        ->getValue();
    }
    return $values;
  }

  /**
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::setPropertyValues().
   */
  public function setPropertyValues($values) {
    foreach ($values as $name => $value) {
      $this
        ->get($name)
        ->setValue($value);
    }
  }

  /**
   * Implements \Drupal\Core\TypedData\ComplexDataInterface::isEmpty().
   */
  public function isEmpty() {
    if (!$this
      ->isNew()) {
      return FALSE;
    }
    foreach ($this
      ->getProperties() as $property) {
      if ($property
        ->getValue() !== NULL) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
   * Implements \Drupal\Core\TypedData\TranslatableInterface::language().
   */
  public function language() {

    // Get the language code if the property exists.
    if ($this
      ->getPropertyDefinition('langcode')) {
      $language = $this
        ->get('langcode')->language;
    }
    if (empty($language)) {

      // Make sure we return a proper language object.
      $language = new Language(array(
        'langcode' => Language::LANGCODE_NOT_SPECIFIED,
      ));
    }
    return $language;
  }

  /**
   * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslation().
   *
   * @return \Drupal\Core\Entity\Field\Type\EntityTranslation
   */
  public function getTranslation($langcode, $strict = TRUE) {

    // If the default language is Language::LANGCODE_NOT_SPECIFIED, the entity is not
    // translatable, so we use Language::LANGCODE_DEFAULT.
    if ($langcode == Language::LANGCODE_DEFAULT || in_array($this
      ->language()->langcode, array(
      Language::LANGCODE_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::STATE_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);
      }
    }

    // @todo: Add a way to get the definition of a translation to the
    // TranslatableInterface and leverage TypeDataManager::getPropertyInstance
    // also.
    $translation_definition = array(
      'type' => 'entity_translation',
      'constraints' => array(
        'entity type' => $this
          ->entityType(),
        'bundle' => $this
          ->bundle(),
      ),
    );
    $translation = \Drupal::typedData()
      ->create($translation_definition, $fields);
    $translation
      ->setStrictMode($strict);
    $translation
      ->setContext('@' . $langcode, $this);
    return $translation;
  }

  /**
   * Implements \Drupal\Core\TypedData\TranslatableInterface::getTranslationLanguages().
   */
  public function getTranslationLanguages($include_default = TRUE) {
    $translations = array();
    $definitions = $this
      ->getPropertyDefinitions();

    // Build an array with the translation langcodes set as keys. Empty
    // translations should not be included and must be skipped.
    foreach ($this
      ->getProperties() as $name => $property) {
      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) {

            // 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($definitions[$name]['translatable']) && !(isset($this->fields[$name][$langcode]) && $this->fields[$name][$langcode]
              ->isEmpty())) {
              $translations[$langcode] = TRUE;
            }
          }
        }
      }
    }

    // We include the default language code instead of the
    // Language::LANGCODE_DEFAULT constant.
    unset($translations[Language::LANGCODE_DEFAULT]);
    if ($include_default) {
      $translations[$this
        ->language()->langcode] = TRUE;
    }

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

  /**
   * Overrides Entity::translations().
   *
   * @todo: Remove once Entity::translations() gets removed.
   */
  public function translations() {
    return $this
      ->getTranslationLanguages(FALSE);
  }

  /**
   * Overrides Entity::getBCEntity().
   */
  public function getBCEntity() {
    if (!isset($this->bcEntity)) {

      // Initialize field definitions so that we can pass them by reference.
      $this
        ->getPropertyDefinitions();
      $this->bcEntity = new EntityBCDecorator($this, $this->fieldDefinitions);
    }
    return $this->bcEntity;
  }

  /**
   * Updates the original values with the interim changes.
   */
  public function updateOriginalValues() {
    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) {
          $field
            ->filterEmptyValues();
          $this->values[$name][$langcode] = $field
            ->getValue();
        }
      }
    }
  }

  /**
   * Implements the magic method for setting object properties.
   *
   * Uses default language always.
   * For compatibility mode to work this must return a reference.
   */
  public function &__get($name) {

    // 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.
    if (isset($this->fields[$name][Language::LANGCODE_DEFAULT])) {
      return $this->fields[$name][Language::LANGCODE_DEFAULT];
    }

    // Inline getPropertyDefinition() to speed up things.
    if (!isset($this->fieldDefinitions)) {
      $this
        ->getPropertyDefinitions();
    }
    if (isset($this->fieldDefinitions[$name])) {
      $return = $this
        ->getTranslatedField($name, Language::LANGCODE_DEFAULT);
      return $return;
    }

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

    // Else directly read/write plain values. That way, non-field entity
    // properties can always be accessed directly.
    if (!isset($this->values[$name])) {
      $this->values[$name] = NULL;
    }
    return $this->values[$name];
  }

  /**
   * Implements the magic method for setting object properties.
   *
   * Uses default language always.
   */
  public function __set($name, $value) {

    // Support setting values via property objects.
    if ($value instanceof TypedDataInterface && !$value instanceof EntityInterface) {
      $value = $value
        ->getValue();
    }

    // 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.
    if (isset($this->fields[$name][Language::LANGCODE_DEFAULT])) {
      $this->fields[$name][Language::LANGCODE_DEFAULT]
        ->setValue($value);
    }
    elseif ($this
      ->getPropertyDefinition($name)) {
      $this
        ->getTranslatedField($name, Language::LANGCODE_DEFAULT)
        ->setValue($value);
    }
    else {
      $this->values[$name] = $value;
    }
  }

  /**
   * Implements the magic method for isset().
   */
  public function __isset($name) {
    if ($this
      ->getPropertyDefinition($name)) {
      return $this
        ->get($name)
        ->getValue() !== NULL;
    }
    else {
      return isset($this->values[$name]);
    }
  }

  /**
   * Implements the magic method for unset.
   */
  public function __unset($name) {
    if ($this
      ->getPropertyDefinition($name)) {
      $this
        ->get($name)
        ->setValue(NULL);
    }
    else {
      unset($this->values[$name]);
    }
  }

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

    // Check if the entity type supports UUIDs and generate a new one if so.
    if (!empty($entity_info['entity_keys']['uuid'])) {
      $uuid = new Uuid();
      $duplicate->{$entity_info['entity_keys']['uuid']}->value = $uuid
        ->generate();
    }

    // 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;
    }
    return $duplicate;
  }

  /**
   * Magic method: Implements a deep clone.
   */
  public function __clone() {
    $this->bcEntity = NULL;
    foreach ($this->fields as $name => $properties) {
      foreach ($properties as $langcode => $property) {
        $this->fields[$name][$langcode] = clone $property;
        $this->fields[$name][$langcode]
          ->setContext($name, $this);
      }
    }
  }

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

  /**
   * {@inheritdoc}
   */
  public function validate() {

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

}

Classes

Namesort descending Description
EntityNG Implements Entity Field API specific enhancements to the Entity class.