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.
Expanded class hierarchy of EntityNG
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);
}
}