Newer
Older
Dries Buytaert
committed
<?php
namespace Drupal\Core\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\Plugin\DataType\EntityReference;
use Drupal\Core\Field\BaseFieldDefinition;
Alex Pott
committed
use Drupal\Core\Field\ChangedFieldItemList;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\TranslationStatusInterface;
Dries Buytaert
committed
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Implements Entity Field API specific enhancements to the Entity class.
Angie Byron
committed
*
* @ingroup entity_api
Dries Buytaert
committed
*/
abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface {
Dries Buytaert
committed
/**
* 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 LanguageInterface::LANGCODE_DEFAULT
* is used for values in default language.
Dries Buytaert
committed
*
* @todo: Add methods for getting original fields and for determining
* changes.
* @todo: Provide a better way for defining default values.
Dries Buytaert
committed
*
* @var array
*/
protected $values = [];
Dries Buytaert
committed
/**
* The array of fields, each being an instance of FieldItemListInterface.
Dries Buytaert
committed
*
* @var array
*/
protected $fields = [];
Dries Buytaert
committed
/**
* Local cache for field definitions.
Dries Buytaert
committed
*
* @see ContentEntityBase::getFieldDefinitions()
*
* @var array
*/
protected $fieldDefinitions;
/**
* Local cache for the available language objects.
*
* @var \Drupal\Core\Language\LanguageInterface[]
*/
protected $languages;
/**
* The language entity key.
*
* @var string
*/
protected $langcodeKey;
Alex Pott
committed
/**
* The default langcode entity key.
*
* @var string
*/
protected $defaultLangcodeKey;
/**
* 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 = LanguageInterface::LANGCODE_DEFAULT;
/**
* Local cache for the default language code.
*
* @var string
*/
protected $defaultLangcode;
/**
* An array of entity translation metadata.
*
* An associative array keyed by translation language code. Every value is an
* array containing the translation status and the translation object, if it has
* already been instantiated.
*
* @var array
*/
protected $translations = [];
/**
* A flag indicating whether a translation object is being initialized.
*
* @var bool
*/
protected $translationInitialize = FALSE;
/**
* Boolean indicating whether a new revision should be created on save.
*
* @var bool
*/
protected $newRevision = FALSE;
/**
* Indicates whether this is the default revision.
*
* @var bool
*/
protected $isDefaultRevision = TRUE;
Alex Pott
committed
/**
* Holds untranslatable entity keys such as the ID, bundle, and revision ID.
Alex Pott
committed
*
* @var array
*/
protected $entityKeys = [];
Alex Pott
committed
catch
committed
/**
* Holds translatable entity keys such as the label.
*
* @var array
*/
protected $translatableEntityKeys = [];
catch
committed
/**
* Whether entity validation was performed.
*
* @var bool
*/
protected $validated = FALSE;
/**
* Whether entity validation is required before saving the entity.
*
* @var bool
*/
protected $validationRequired = FALSE;
/**
* The loaded revision ID before the new revision was set.
*
* @var int
*/
protected $loadedRevisionId;
/**
* The revision translation affected entity key.
*
* @var string
*/
protected $revisionTranslationAffectedKey;
/**
* Whether the revision translation affected flag has been enforced.
*
* An array, keyed by the translation language code.
*
* @var bool[]
*/
protected $enforceRevisionTranslationAffected = [];
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = []) {
$this->entityTypeId = $entity_type;
Alex Pott
committed
$this->entityKeys['bundle'] = $bundle ? $bundle : $this->entityTypeId;
$this->langcodeKey = $this->getEntityType()->getKey('langcode');
Alex Pott
committed
$this->defaultLangcodeKey = $this->getEntityType()->getKey('default_langcode');
$this->revisionTranslationAffectedKey = $this->getEntityType()->getKey('revision_translation_affected');
foreach ($values as $key => $value) {
Angie Byron
committed
// If the key matches an existing property set the value to the property
Alex Pott
committed
// to set properties like isDefaultRevision.
// @todo: Should this be converted somehow?
if (property_exists($this, $key) && isset($value[LanguageInterface::LANGCODE_DEFAULT])) {
$this->$key = $value[LanguageInterface::LANGCODE_DEFAULT];
Angie Byron
committed
}
Alex Pott
committed
}
$this->values = $values;
foreach ($this->getEntityType()->getKeys() as $key => $field_name) {
if (isset($this->values[$field_name])) {
catch
committed
if (is_array($this->values[$field_name])) {
// We store untranslatable fields into an entity key without using a
// langcode key.
if (!$this->getFieldDefinition($field_name)->isTranslatable()) {
if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'];
}
}
else {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
}
}
}
else {
catch
committed
// We save translatable fields such as the publishing status of a node
// into an entity key array keyed by langcode as a performance
// optimization, so we don't have to go through TypedData when we
// need these values.
foreach ($this->values[$field_name] as $langcode => $field_value) {
if (is_array($this->values[$field_name][$langcode])) {
if (isset($this->values[$field_name][$langcode][0]['value'])) {
$this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode][0]['value'];
}
}
else {
$this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode];
}
}
Alex Pott
committed
}
}
}
// Initialize translations. Ensure we have at least an entry for the default
// language.
// We determine if the entity is new by checking in the entity values for
// the presence of the id entity key, as the usage of ::isNew() is not
// possible in the constructor.
$data = isset($values[$this->getEntityType()->getKey('id')]) ? ['status' => static::TRANSLATION_EXISTING] : ['status' => static::TRANSLATION_CREATED];
$this->translations[LanguageInterface::LANGCODE_DEFAULT] = $data;
$this->setDefaultLangcode();
if ($translations) {
foreach ($translations as $langcode) {
if ($langcode != $this->defaultLangcode && $langcode != LanguageInterface::LANGCODE_DEFAULT) {
$this->translations[$langcode] = $data;
}
}
}
if ($this->getEntityType()->isRevisionable()) {
// Store the loaded revision ID the entity has been loaded with to
// keep it safe from changes.
$this->updateLoadedRevisionId();
}
}
/**
* {@inheritdoc}
*/
protected function getLanguages() {
if (empty($this->languages)) {
$this->languages = $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL);
// If the entity references a language that is not or no longer available,
// we return a mock language object to avoid disrupting the consuming
// code.
if (!isset($this->languages[$this->defaultLangcode])) {
$this->languages[$this->defaultLangcode] = new Language(['id' => $this->defaultLangcode]);
}
}
return $this->languages;
}
catch
committed
/**
* {@inheritdoc}
*/
Angie Byron
committed
public function postCreate(EntityStorageInterface $storage) {
catch
committed
$this->newRevision = TRUE;
}
/**
* {@inheritdoc}
*/
public function setNewRevision($value = TRUE) {
catch
committed
if (!$this->getEntityType()->hasKey('revision')) {
catch
committed
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support revisions.");
catch
committed
}
if ($value && !$this->newRevision) {
// When saving a new revision, set any existing revision ID to NULL so as
// to ensure that a new revision will actually be created.
$this->set($this->getEntityType()->getKey('revision'), NULL);
}
elseif (!$value && $this->newRevision) {
// If ::setNewRevision(FALSE) is called after ::setNewRevision(TRUE) we
// have to restore the loaded revision ID.
$this->set($this->getEntityType()->getKey('revision'), $this->getLoadedRevisionId());
catch
committed
}
catch
committed
$this->newRevision = $value;
}
/**
* {@inheritdoc}
*/
public function getLoadedRevisionId() {
return $this->loadedRevisionId;
}
/**
* {@inheritdoc}
*/
public function updateLoadedRevisionId() {
$this->loadedRevisionId = $this->getRevisionId() ?: $this->loadedRevisionId;
return $this;
}
/**
* {@inheritdoc}
*/
public function isNewRevision() {
return $this->newRevision || ($this->getEntityType()->hasKey('revision') && !$this->getRevisionId());
}
/**
* {@inheritdoc}
*/
public function isDefaultRevision($new_value = NULL) {
$return = $this->isDefaultRevision;
if (isset($new_value)) {
$this->isDefaultRevision = (bool) $new_value;
}
// New entities should always ensure at least one default revision exists,
// creating an entity without a default revision is an invalid state.
return $this->isNew() || $return;
}
Alex Bronstein
committed
/**
* {@inheritdoc}
*/
public function wasDefaultRevision() {
/** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
$entity_type = $this->getEntityType();
if (!$entity_type->isRevisionable()) {
return TRUE;
}
$revision_default_key = $entity_type->getRevisionMetadataKey('revision_default');
$value = $this->isNew() || $this->get($revision_default_key)->value;
return $value;
}
Alex Bronstein
committed
/**
* {@inheritdoc}
*/
public function isLatestRevision() {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id());
}
/**
* {@inheritdoc}
*/
public function isLatestTranslationAffectedRevision() {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
return $this->getLoadedRevisionId() == $storage->getLatestTranslationAffectedRevisionId($this->id(), $this->language()->getId());
}
catch
committed
/**
* {@inheritdoc}
*/
public function isRevisionTranslationAffected() {
return $this->hasField($this->revisionTranslationAffectedKey) ? $this->get($this->revisionTranslationAffectedKey)->value : TRUE;
catch
committed
}
/**
* {@inheritdoc}
*/
public function setRevisionTranslationAffected($affected) {
if ($this->hasField($this->revisionTranslationAffectedKey)) {
$this->set($this->revisionTranslationAffectedKey, $affected);
catch
committed
}
return $this;
}
/**
* {@inheritdoc}
*/
public function isRevisionTranslationAffectedEnforced() {
return !empty($this->enforceRevisionTranslationAffected[$this->activeLangcode]);
}
/**
* {@inheritdoc}
*/
public function setRevisionTranslationAffectedEnforced($enforced) {
$this->enforceRevisionTranslationAffected[$this->activeLangcode] = $enforced;
return $this;
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
public function isDefaultTranslation() {
return $this->activeLangcode === LanguageInterface::LANGCODE_DEFAULT;
}
/**
* {@inheritdoc}
*/
public function getRevisionId() {
Alex Pott
committed
return $this->getEntityKey('revision');
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
// Check that the bundle is translatable, the entity has a language defined
// and if we have more than one language on the site.
Alex Pott
committed
$bundles = $this->entityManager()->getBundleInfo($this->entityTypeId);
Alex Pott
committed
return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual();
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
// An entity requiring validation should not be saved if it has not been
// actually validated.
if ($this->validationRequired && !$this->validated) {
// @todo Make this an assertion in https://www.drupal.org/node/2408013.
throw new \LogicException('Entity validation was skipped.');
}
else {
$this->validated = FALSE;
}
parent::preSave($storage);
}
/**
* {@inheritdoc}
*/
catch
committed
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// Update the status of all saved translations.
$removed = [];
foreach ($this->translations as $langcode => &$data) {
if ($data['status'] == static::TRANSLATION_REMOVED) {
$removed[$langcode] = TRUE;
}
else {
$data['status'] = static::TRANSLATION_EXISTING;
}
}
$this->translations = array_diff_key($this->translations, $removed);
// Reset the new revision flag.
$this->newRevision = FALSE;
// Reset the enforcement of the revision translation affected flag.
$this->enforceRevisionTranslationAffected = [];
/**
* {@inheritdoc}
*/
public function validate() {
$this->validated = TRUE;
$violations = $this->getTypedData()->validate();
return new EntityConstraintViolationList($this, iterator_to_array($violations));
}
/**
* {@inheritdoc}
*/
public function isValidationRequired() {
return (bool) $this->validationRequired;
}
/**
* {@inheritdoc}
*/
public function setValidationRequired($required) {
$this->validationRequired = $required;
return $this;
}
/**
* Clear entity translation object cache to remove stale references.
*/
protected function clearTranslationCache() {
foreach ($this->translations as &$translation) {
unset($translation['entity']);
}
}
/**
* {@inheritdoc}
*/
public function __sleep() {
// Get the values of instantiated field objects, only serialize the values.
foreach ($this->fields as $name => $fields) {
foreach ($fields as $langcode => $field) {
$this->values[$name][$langcode] = $field->getValue();
}
}
$this->fields = [];
$this->fieldDefinitions = NULL;
$this->languages = NULL;
$this->clearTranslationCache();
Alex Pott
committed
return parent::__sleep();
}
Dries Buytaert
committed
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function id() {
Alex Pott
committed
return $this->getEntityKey('id');
}
/**
* {@inheritdoc}
*/
public function bundle() {
Alex Pott
committed
return $this->getEntityKey('bundle');
Dries Buytaert
committed
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function uuid() {
Alex Pott
committed
return $this->getEntityKey('uuid');
Dries Buytaert
committed
}
/**
* {@inheritdoc}
*/
public function hasField($field_name) {
return (bool) $this->getFieldDefinition($field_name);
}
Dries Buytaert
committed
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
Angie Byron
committed
public function get($field_name) {
if (!isset($this->fields[$field_name][$this->activeLangcode])) {
return $this->getTranslatedField($field_name, $this->activeLangcode);
Dries Buytaert
committed
}
Angie Byron
committed
return $this->fields[$field_name][$this->activeLangcode];
Dries Buytaert
committed
}
/**
* Gets a translated field.
*
Alex Pott
committed
* @return \Drupal\Core\Field\FieldItemListInterface
Dries Buytaert
committed
*/
protected function getTranslatedField($name, $langcode) {
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
catch
committed
throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.");
}
// 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[$name][$langcode])) {
$definition = $this->getFieldDefinition($name);
Dries Buytaert
committed
if (!$definition) {
catch
committed
throw new \InvalidArgumentException("Field $name is unknown.");
Dries Buytaert
committed
}
// Non-translatable fields are always stored with
// LanguageInterface::LANGCODE_DEFAULT as key.
Alex Pott
committed
$default = $langcode == LanguageInterface::LANGCODE_DEFAULT;
Alex Pott
committed
if (!$default && !$definition->isTranslatable()) {
if (!isset($this->fields[$name][LanguageInterface::LANGCODE_DEFAULT])) {
$this->fields[$name][LanguageInterface::LANGCODE_DEFAULT] = $this->getTranslatedField($name, LanguageInterface::LANGCODE_DEFAULT);
}
$this->fields[$name][$langcode] = &$this->fields[$name][LanguageInterface::LANGCODE_DEFAULT];
Dries Buytaert
committed
}
else {
$value = NULL;
if (isset($this->values[$name][$langcode])) {
$value = $this->values[$name][$langcode];
}
$field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this->getTranslation($langcode), $name, $value);
if ($default) {
// $this->defaultLangcode might not be set if we are initializing the
// default language code cache, in which case there is no valid
// langcode to assign.
$field_langcode = isset($this->defaultLangcode) ? $this->defaultLangcode : LanguageInterface::LANGCODE_NOT_SPECIFIED;
}
else {
$field_langcode = $langcode;
}
$field->setLangcode($field_langcode);
$this->fields[$name][$langcode] = $field;
Dries Buytaert
committed
}
}
return $this->fields[$name][$langcode];
Dries Buytaert
committed
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function set($name, $value, $notify = TRUE) {
Alex Pott
committed
// Assign the value on the child and overrule notify such that we get
// notified to handle changes afterwards. We can ignore notify as there is
// no parent to notify anyway.
$this->get($name)->setValue($value, TRUE);
Alex Pott
committed
return $this;
Dries Buytaert
committed
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function getFields($include_computed = TRUE) {
$fields = [];
foreach ($this->getFieldDefinitions() as $name => $definition) {
Angie Byron
committed
if ($include_computed || !$definition->isComputed()) {
Angie Byron
committed
$fields[$name] = $this->get($name);
Dries Buytaert
committed
}
}
Angie Byron
committed
return $fields;
Dries Buytaert
committed
}
/**
* {@inheritdoc}
*/
public function getTranslatableFields($include_computed = TRUE) {
$fields = [];
foreach ($this->getFieldDefinitions() as $name => $definition) {
if (($include_computed || !$definition->isComputed()) && $definition->isTranslatable()) {
$fields[$name] = $this->get($name);
}
}
return $fields;
}
Dries Buytaert
committed
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function getIterator() {
Angie Byron
committed
return new \ArrayIterator($this->getFields());
Dries Buytaert
committed
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function getFieldDefinition($name) {
if (!isset($this->fieldDefinitions)) {
$this->getFieldDefinitions();
}
if (isset($this->fieldDefinitions[$name])) {
return $this->fieldDefinitions[$name];
Dries Buytaert
committed
}
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function getFieldDefinitions() {
if (!isset($this->fieldDefinitions)) {
$this->fieldDefinitions = $this->entityManager()->getFieldDefinitions($this->entityTypeId, $this->bundle());
}
return $this->fieldDefinitions;
Dries Buytaert
committed
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function toArray() {
$values = [];
Angie Byron
committed
foreach ($this->getFields() as $name => $property) {
Dries Buytaert
committed
$values[$name] = $property->getValue();
}
return $values;
}
/**
* {@inheritdoc}
*/
catch
committed
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($operation == 'create') {
Alex Pott
committed
return $this->entityManager()
->getAccessControlHandler($this->entityTypeId)
catch
committed
->createAccess($this->bundle(), $account, [], $return_as_object);
}
Alex Pott
committed
return $this->entityManager()
->getAccessControlHandler($this->entityTypeId)
Alex Bronstein
committed
->access($this, $operation, $account, $return_as_object);
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function language() {
$language = NULL;
if ($this->activeLangcode != LanguageInterface::LANGCODE_DEFAULT) {
if (!isset($this->languages[$this->activeLangcode])) {
$this->getLanguages();
}
$language = $this->languages[$this->activeLangcode];
}
else {
// @todo Avoid this check by getting the language from the language
// manager directly in https://www.drupal.org/node/2303877.
if (!isset($this->languages[$this->defaultLangcode])) {
$this->getLanguages();
$language = $this->languages[$this->defaultLangcode];
}
return $language;
}
/**
* Populates the local cache for the default language code.
*/
protected function setDefaultLangcode() {
// Get the language code if the property exists.
// Try to read the value directly from the list of entity keys which got
// initialized in __construct(). This avoids creating a field item object.
catch
committed
if (isset($this->translatableEntityKeys['langcode'][$this->activeLangcode])) {
$this->defaultLangcode = $this->translatableEntityKeys['langcode'][$this->activeLangcode];
}
elseif ($this->hasField($this->langcodeKey) && ($item = $this->get($this->langcodeKey)) && isset($item->language)) {
$this->defaultLangcode = $item->language->getId();
catch
committed
$this->translatableEntityKeys['langcode'][$this->activeLangcode] = $this->defaultLangcode;
}
if (empty($this->defaultLangcode)) {
// Make sure we return a proper language object, if the entity has a
// langcode field, default to the site's default language.
if ($this->hasField($this->langcodeKey)) {
$this->defaultLangcode = $this->languageManager()->getDefaultLanguage()->getId();
}
else {
$this->defaultLangcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
}
}
// This needs to be initialized manually as it is skipped when instantiating
// the language field object to avoid infinite recursion.
if (!empty($this->fields[$this->langcodeKey])) {
$this->fields[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT]->setLangcode($this->defaultLangcode);
}
}
/**
* Updates language for already instantiated fields.
*/
protected function updateFieldLangcodes($langcode) {
foreach ($this->fields as $name => $items) {
if (!empty($items[LanguageInterface::LANGCODE_DEFAULT])) {
$items[LanguageInterface::LANGCODE_DEFAULT]->setLangcode($langcode);
}
}
}
/**
* {@inheritdoc}
*/
public function onChange($name) {
Alex Pott
committed
// Check if the changed name is the value of an entity key and if the value
// of that is currently cached, if so, reset it. Exclude the bundle from
// that check, as it ready only and must not change, unsetting it could
// lead to recursions.
if ($key = array_search($name, $this->getEntityType()->getKeys())) {
catch
committed
if ($key != 'bundle') {
if (isset($this->entityKeys[$key])) {
unset($this->entityKeys[$key]);
}
elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
}
// If the revision identifier field is being populated with the original
// value, we need to make sure the "new revision" flag is reset
// accordingly.
Lee Rowlands
committed
if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId() && !$this->isNew()) {
$this->newRevision = FALSE;
}
Alex Pott
committed
}
}
Alex Pott
committed
switch ($name) {
case $this->langcodeKey:
if ($this->isDefaultTranslation()) {
// Update the default internal language cache.
$this->setDefaultLangcode();
if (isset($this->translations[$this->defaultLangcode])) {
$message = SafeMarkup::format('A translation already exists for the specified language (@langcode).', ['@langcode' => $this->defaultLangcode]);
Alex Pott
committed
throw new \InvalidArgumentException($message);
}
$this->updateFieldLangcodes($this->defaultLangcode);
}
else {
// @todo Allow the translation language to be changed. See
// https://www.drupal.org/node/2443989.
$items = $this->get($this->langcodeKey);
if ($items->value != $this->activeLangcode) {
$items->setValue($this->activeLangcode, FALSE);
$message = SafeMarkup::format('The translation language cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
Alex Pott
committed
throw new \LogicException($message);
}
}
break;
case $this->defaultLangcodeKey:
// @todo Use a standard method to make the default_langcode field
// read-only. See https://www.drupal.org/node/2443991.
if (isset($this->values[$this->defaultLangcodeKey]) && $this->get($this->defaultLangcodeKey)->value != $this->isDefaultTranslation()) {
Alex Pott
committed
$this->get($this->defaultLangcodeKey)->setValue($this->isDefaultTranslation(), FALSE);
$message = SafeMarkup::format('The default translation flag cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
Alex Pott
committed
throw new \LogicException($message);
}
break;
case $this->revisionTranslationAffectedKey:
// If the revision translation affected flag is being set then enforce
// its value.
$this->setRevisionTranslationAffectedEnforced(TRUE);
break;
}
Dries Buytaert
committed
}
/**
* {@inheritdoc}
*/
public function getTranslation($langcode) {
// Ensure we always use the default language code when dealing with the
// original entity language.
if ($langcode != LanguageInterface::LANGCODE_DEFAULT && $langcode == $this->defaultLangcode) {
$langcode = LanguageInterface::LANGCODE_DEFAULT;
Dries Buytaert
committed
}
// Populate entity translation object cache so it will be available for all
// translation objects.
if ($langcode == $this->activeLangcode) {
$this->translations[$langcode]['entity'] = $this;
Dries Buytaert
committed
}
// 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'];
}
// Otherwise if an existing translation language was specified we need to
// instantiate the related translation.
elseif (isset($this->translations[$langcode])) {
$translation = $this->initializeTranslation($langcode);
$this->translations[$langcode]['entity'] = $translation;
}
if (empty($translation)) {
catch
committed
throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
Dries Buytaert
committed
}
Dries Buytaert
committed
return $translation;
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function getUntranslated() {
return $this->getTranslation(LanguageInterface::LANGCODE_DEFAULT);
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
}
/**
* 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->enforceIsNew = &$this->enforceIsNew;
catch
committed
$translation->newRevision = &$this->newRevision;
Alex Pott
committed
$translation->entityKeys = &$this->entityKeys;
$translation->translatableEntityKeys = &$this->translatableEntityKeys;
$translation->translationInitialize = FALSE;
Alex Pott
committed
$translation->typedData = NULL;
$translation->loadedRevisionId = &$this->loadedRevisionId;
Alex Pott
committed
$translation->isDefaultRevision = &$this->isDefaultRevision;
$translation->enforceRevisionTranslationAffected = &$this->enforceRevisionTranslationAffected;
return $translation;
}
/**
* {@inheritdoc}
*/
public function hasTranslation($langcode) {
if ($langcode == $this->defaultLangcode) {
$langcode = LanguageInterface::LANGCODE_DEFAULT;
return !empty($this->translations[$langcode]['status']);
}
Alex Bronstein
committed
/**
* {@inheritdoc}
*/
public function isNewTranslation() {
return $this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_CREATED;
}
/**
* {@inheritdoc}
*/
public function addTranslation($langcode, array $values = []) {
// Make sure we do not attempt to create a translation if an invalid
// language is specified or the entity cannot be translated.
$this->getLanguages();
if (!isset($this->languages[$langcode]) || $this->hasTranslation($langcode) || $this->languages[$langcode]->isLocked()) {
catch
committed
throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
}
if ($this->languages[$this->defaultLangcode]->isLocked()) {
throw new \InvalidArgumentException("The entity cannot be translated since it is language neutral ({$this->defaultLangcode}).");
}
Alex Bronstein
committed
// Initialize the translation object.
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityManager()->getStorage($this->getEntityTypeId());
$this->translations[$langcode]['status'] = !isset($this->translations[$langcode]['status_existed']) ? static::TRANSLATION_CREATED : static::TRANSLATION_EXISTING;
Alex Bronstein
committed
return $storage->createTranslation($this, $langcode, $values);
}
/**
* {@inheritdoc}
*/
public function removeTranslation($langcode) {
if (isset($this->translations[$langcode]) && $langcode != LanguageInterface::LANGCODE_DEFAULT && $langcode != $this->defaultLangcode) {
foreach ($this->getFieldDefinitions() as $name => $definition) {
Alex Pott
committed
if ($definition->isTranslatable()) {
unset($this->values[$name][$langcode]);
unset($this->fields[$name][$langcode]);
}
Dries Buytaert
committed
}
// If removing a translation which has not been saved yet, then we have
// to remove it completely so that ::getTranslationStatus returns the
// proper status.
if ($this->translations[$langcode]['status'] == static::TRANSLATION_CREATED) {
unset($this->translations[$langcode]);
}
else {
if ($this->translations[$langcode]['status'] == static::TRANSLATION_EXISTING) {
$this->translations[$langcode]['status_existed'] = TRUE;
}
$this->translations[$langcode]['status'] = static::TRANSLATION_REMOVED;
}
}
else {
catch
committed
throw new \InvalidArgumentException("The specified translation ($langcode) cannot be removed.");
}
}
/**
* {@inheritdoc}
*/
public function getTranslationStatus($langcode) {
if ($langcode == $this->defaultLangcode) {
$langcode = LanguageInterface::LANGCODE_DEFAULT;
}
return isset($this->translations[$langcode]) ? $this->translations[$langcode]['status'] : NULL;
}
/**
* {@inheritdoc}
*/
public function getTranslationLanguages($include_default = TRUE) {
$translations = array_filter($this->translations, function ($translation) {
return $translation['status'];