Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
64.29% |
18 / 28 |
CRAP | |
36.90% |
62 / 168 |
ContentEntityBase | |
0.00% |
0 / 1 |
|
82.46% |
47 / 57 |
10760.95 | |
36.90% |
62 / 168 |
__construct | |
0.00% |
0 / 1 |
380 | |
0.00% |
0 / 30 |
|||
getLanguages | |
100.00% |
1 / 1 |
3 | |
100.00% |
0 / 0 |
|||
postCreate | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
setNewRevision | |
100.00% |
1 / 1 |
6 | |
100.00% |
9 / 9 |
|||
isNewRevision | |
100.00% |
1 / 1 |
3 | |
100.00% |
1 / 1 |
|||
isDefaultRevision | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
isRevisionTranslationAffected | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
setRevisionTranslationAffected | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
isDefaultTranslation | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
getRevisionId | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
isTranslatable | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
preSave | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
preSaveRevision | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
validate | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
isValidationRequired | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
setValidationRequired | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
clearTranslationCache | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
__sleep | |
100.00% |
1 / 1 |
3 | |
100.00% |
0 / 0 |
|||
id | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
bundle | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
uuid | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
hasField | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
get | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
getTranslatedField | |
0.00% |
0 / 1 |
110 | |
0.00% |
0 / 21 |
|||
set | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
getFields | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
getTranslatableFields | |
100.00% |
1 / 1 |
5 | |
100.00% |
0 / 0 |
|||
getIterator | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
getFieldDefinition | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
|||
getFieldDefinitions | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
toArray | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
access | |
100.00% |
1 / 1 |
2 | |
100.00% |
7 / 7 |
|||
language | |
100.00% |
1 / 1 |
4 | |
100.00% |
0 / 0 |
|||
setDefaultLangcode | |
0.00% |
0 / 1 |
72 | |
0.00% |
0 / 12 |
|||
updateFieldLangcodes | |
100.00% |
1 / 1 |
3 | |
100.00% |
0 / 0 |
|||
onChange | |
100.00% |
1 / 1 |
12 | |
100.00% |
0 / 0 |
|||
getTranslation | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 12 |
|||
getUntranslated | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
initializeTranslation | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
hasTranslation | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
isNewTranslation | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
addTranslation | |
100.00% |
1 / 1 |
5 | |
100.00% |
0 / 0 |
|||
removeTranslation | |
100.00% |
1 / 1 |
6 | |
100.00% |
0 / 0 |
|||
getTranslationLanguages | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
anonymous function | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
updateOriginalValues | |
100.00% |
1 / 1 |
6 | |
100.00% |
0 / 0 |
|||
__get | |
100.00% |
1 / 1 |
5 | |
100.00% |
0 / 0 |
|||
__set | |
100.00% |
1 / 1 |
6 | |
100.00% |
0 / 0 |
|||
__isset | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
__unset | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
createDuplicate | |
100.00% |
1 / 1 |
4 | |
100.00% |
0 / 0 |
|||
__clone | |
100.00% |
1 / 1 |
6 | |
100.00% |
8 / 8 |
|||
label | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
referencedEntities | |
100.00% |
1 / 1 |
6 | |
100.00% |
0 / 0 |
|||
getEntityKey | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 15 |
|||
bundleFieldDefinitions | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
hasTranslationChanges | |
100.00% |
1 / 1 |
14 | |
100.00% |
0 / 0 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Entity\ContentEntityBase. | |
*/ | |
namespace Drupal\Core\Entity; | |
use Drupal\Component\Utility\SafeMarkup; | |
use Drupal\Core\Entity\Plugin\DataType\EntityReference; | |
use Drupal\Core\Language\Language; | |
use Drupal\Core\Language\LanguageInterface; | |
use Drupal\Core\Session\AccountInterface; | |
use Drupal\Core\TypedData\TypedDataInterface; | |
/** | |
* Implements Entity Field API specific enhancements to the Entity class. | |
* | |
* @ingroup entity_api | |
*/ | |
abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface { | |
/** | |
* Status code identifying a removed translation. | |
*/ | |
const TRANSLATION_REMOVED = 0; | |
/** | |
* Status code identifying an existing translation. | |
*/ | |
const TRANSLATION_EXISTING = 1; | |
/** | |
* Status code identifying a newly created translation. | |
*/ | |
const TRANSLATION_CREATED = 2; | |
/** | |
* 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. | |
* | |
* @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(); | |
/** | |
* The array of fields, each being an instance of FieldItemListInterface. | |
* | |
* @var array | |
*/ | |
protected $fields = array(); | |
/** | |
* Local cache for field definitions. | |
* | |
* @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; | |
/** | |
* 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 = array(); | |
/** | |
* 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; | |
/** | |
* Holds untranslatable entity keys such as the ID, bundle, and revision ID. | |
* | |
* @var array | |
*/ | |
protected $entityKeys = array(); | |
/** | |
* Holds translatable entity keys such as the label. | |
* | |
* @var array | |
*/ | |
protected $translatableEntityKeys = array(); | |
/** | |
* Whether entity validation was performed. | |
* | |
* @var bool | |
*/ | |
protected $validated = FALSE; | |
/** | |
* Whether entity validation is required before saving the entity. | |
* | |
* @var bool | |
*/ | |
protected $validationRequired = FALSE; | |
/** | |
* {@inheritdoc} | |
*/ | |
public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = array()) { | |
$this->entityTypeId = $entity_type; | |
$this->entityKeys['bundle'] = $bundle ? $bundle : $this->entityTypeId; | |
$this->langcodeKey = $this->getEntityType()->getKey('langcode'); | |
$this->defaultLangcodeKey = $this->getEntityType()->getKey('default_langcode'); | |
foreach ($values as $key => $value) { | |
// If the key matches an existing property set the value to the property | |
// 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]; | |
} | |
} | |
$this->values = $values; | |
foreach ($this->getEntityType()->getKeys() as $key => $field_name) { | |
if (isset($this->values[$field_name])) { | |
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 { | |
// 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]; | |
} | |
} | |
} | |
} | |
} | |
} | |
// Initialize translations. Ensure we have at least an entry for the default | |
// language. | |
$data = array('status' => static::TRANSLATION_EXISTING); | |
$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; | |
} | |
} | |
} | |
} | |
/** | |
* {@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(array('id' => $this->defaultLangcode)); | |
} | |
} | |
return $this->languages; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function postCreate(EntityStorageInterface $storage) { | |
$this->newRevision = TRUE; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setNewRevision($value = TRUE) { | |
if (!$this->getEntityType()->hasKey('revision')) { | |
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support revisions."); | |
} | |
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); | |
// Make sure that the flag tracking which translations are affected by the | |
// current revision is reset. | |
foreach ($this->translations as $langcode => $data) { | |
// But skip removed translations. | |
if ($this->hasTranslation($langcode)) { | |
$this->getTranslation($langcode)->setRevisionTranslationAffected(NULL); | |
} | |
} | |
} | |
$this->newRevision = $value; | |
} | |
/** | |
* {@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; | |
} | |
return $return; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function isRevisionTranslationAffected() { | |
$field_name = 'revision_translation_affected'; | |
return $this->hasField($field_name) ? $this->get($field_name)->value : TRUE; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function setRevisionTranslationAffected($affected) { | |
$field_name = 'revision_translation_affected'; | |
if ($this->hasField($field_name)) { | |
$this->set($field_name, $affected); | |
} | |
return $this; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function isDefaultTranslation() { | |
return $this->activeLangcode === LanguageInterface::LANGCODE_DEFAULT; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getRevisionId() { | |
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. | |
$bundles = $this->entityManager()->getBundleInfo($this->entityTypeId); | |
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} | |
*/ | |
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { | |
} | |
/** | |
* {@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 = array(); | |
$this->fieldDefinitions = NULL; | |
$this->languages = NULL; | |
$this->clearTranslationCache(); | |
return parent::__sleep(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function id() { | |
return $this->getEntityKey('id'); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function bundle() { | |
return $this->getEntityKey('bundle'); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function uuid() { | |
return $this->getEntityKey('uuid'); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function hasField($field_name) { | |
return (bool) $this->getFieldDefinition($field_name); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function get($field_name) { | |
if (!isset($this->fields[$field_name][$this->activeLangcode])) { | |
return $this->getTranslatedField($field_name, $this->activeLangcode); | |
} | |
return $this->fields[$field_name][$this->activeLangcode]; | |
} | |
/** | |
* Gets a translated field. | |
* | |
* @return \Drupal\Core\Field\FieldItemListInterface | |
*/ | |
protected function getTranslatedField($name, $langcode) { | |
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) { | |
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); | |
if (!$definition) { | |
throw new \InvalidArgumentException("Field $name is unknown."); | |
} | |
// Non-translatable fields are always stored with | |
// LanguageInterface::LANGCODE_DEFAULT as key. | |
$default = $langcode == LanguageInterface::LANGCODE_DEFAULT; | |
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]; | |
} | |
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; | |
} | |
} | |
return $this->fields[$name][$langcode]; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function set($name, $value, $notify = TRUE) { | |
// 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); | |
return $this; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getFields($include_computed = TRUE) { | |
$fields = array(); | |
foreach ($this->getFieldDefinitions() as $name => $definition) { | |
if ($include_computed || !$definition->isComputed()) { | |
$fields[$name] = $this->get($name); | |
} | |
} | |
return $fields; | |
} | |
/** | |
* {@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; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getIterator() { | |
return new \ArrayIterator($this->getFields()); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getFieldDefinition($name) { | |
if (!isset($this->fieldDefinitions)) { | |
$this->getFieldDefinitions(); | |
} | |
if (isset($this->fieldDefinitions[$name])) { | |
return $this->fieldDefinitions[$name]; | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getFieldDefinitions() { | |
if (!isset($this->fieldDefinitions)) { | |
$this->fieldDefinitions = $this->entityManager()->getFieldDefinitions($this->entityTypeId, $this->bundle()); | |
} | |
return $this->fieldDefinitions; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function toArray() { | |
$values = array(); | |
foreach ($this->getFields() as $name => $property) { | |
$values[$name] = $property->getValue(); | |
} | |
return $values; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { | |
if ($operation == 'create') { | |
return $this->entityManager() | |
->getAccessControlHandler($this->entityTypeId) | |
->createAccess($this->bundle(), $account, [], $return_as_object); | |
} | |
return $this->entityManager() | |
->getAccessControlHandler($this->entityTypeId) | |
->access($this, $operation, $account, $return_as_object); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
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. | |
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(); | |
$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) { | |
// 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())) { | |
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]); | |
} | |
} | |
} | |
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).', array('@langcode' => $this->defaultLangcode)); | |
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).', array('@langcode' => $this->activeLangcode)); | |
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()) { | |
$this->get($this->defaultLangcodeKey)->setValue($this->isDefaultTranslation(), FALSE); | |
$message = SafeMarkup::format('The default translation flag cannot be changed (@langcode).', array('@langcode' => $this->activeLangcode)); | |
throw new \LogicException($message); | |
} | |
break; | |
} | |
} | |
/** | |
* {@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; | |
} | |
// Populate entity translation object cache so it will be available for all | |
// translation objects. | |
if ($langcode == $this->activeLangcode) { | |
$this->translations[$langcode]['entity'] = $this; | |
} | |
// 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)) { | |
throw new \InvalidArgumentException("Invalid translation language ($langcode) specified."); | |
} | |
return $translation; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getUntranslated() { | |
return $this->getTranslation(LanguageInterface::LANGCODE_DEFAULT); | |
} | |
/** | |
* 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; | |
$translation->newRevision = &$this->newRevision; | |
$translation->entityKeys = &$this->entityKeys; | |
$translation->translatableEntityKeys = &$this->translatableEntityKeys; | |
$translation->translationInitialize = FALSE; | |
$translation->typedData = NULL; | |
return $translation; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function hasTranslation($langcode) { | |
if ($langcode == $this->defaultLangcode) { | |
$langcode = LanguageInterface::LANGCODE_DEFAULT; | |
} | |
return !empty($this->translations[$langcode]['status']); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function isNewTranslation() { | |
return $this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_CREATED; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function addTranslation($langcode, array $values = array()) { | |
// 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()) { | |
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})."); | |
} | |
// Initialize the translation object. | |
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ | |
$storage = $this->entityManager()->getStorage($this->getEntityTypeId()); | |
$this->translations[$langcode]['status'] = static::TRANSLATION_CREATED; | |
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) { | |
if ($definition->isTranslatable()) { | |
unset($this->values[$name][$langcode]); | |
unset($this->fields[$name][$langcode]); | |
} | |
} | |
$this->translations[$langcode]['status'] = static::TRANSLATION_REMOVED; | |
} | |
else { | |
throw new \InvalidArgumentException("The specified translation ($langcode) cannot be removed."); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getTranslationLanguages($include_default = TRUE) { | |
$translations = array_filter($this->translations, function($translation) { return $translation['status']; }); | |
unset($translations[LanguageInterface::LANGCODE_DEFAULT]); | |
if ($include_default) { | |
$translations[$this->defaultLangcode] = TRUE; | |
} | |
// Now load language objects based upon translation langcodes. | |
return array_intersect_key($this->getLanguages(), $translations); | |
} | |
/** | |
* Updates the original values with the interim changes. | |
*/ | |
public function updateOriginalValues() { | |
if (!$this->fields) { | |
return; | |
} | |
foreach ($this->getFieldDefinitions() as $name => $definition) { | |
if (!$definition->isComputed() && !empty($this->fields[$name])) { | |
foreach ($this->fields[$name] as $langcode => $item) { | |
$item->filterEmptyItems(); | |
$this->values[$name][$langcode] = $item->getValue(); | |
} | |
} | |
} | |
} | |
/** | |
* Implements the magic method for getting object properties. | |
* | |
* @todo: A lot of code still uses non-fields (e.g. $entity->content in view | |
* builders) by reference. Clean that up. | |
*/ | |
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][$this->activeLangcode])) { | |
return $this->fields[$name][$this->activeLangcode]; | |
} | |
// Inline getFieldDefinition() to speed things up. | |
if (!isset($this->fieldDefinitions)) { | |
$this->getFieldDefinitions(); | |
} | |
if (isset($this->fieldDefinitions[$name])) { | |
$return = $this->getTranslatedField($name, $this->activeLangcode); | |
return $return; | |
} | |
// 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) { | |
// Inline getFieldDefinition() to speed things up. | |
if (!isset($this->fieldDefinitions)) { | |
$this->getFieldDefinitions(); | |
} | |
// Handle Field API fields. | |
if (isset($this->fieldDefinitions[$name])) { | |
// Support setting values via property objects. | |
if ($value instanceof TypedDataInterface) { | |
$value = $value->getValue(); | |
} | |
// If a FieldItemList object already exists, set its value. | |
if (isset($this->fields[$name][$this->activeLangcode])) { | |
$this->fields[$name][$this->activeLangcode]->setValue($value); | |
} | |
// If not, create one. | |
else { | |
$this->getTranslatedField($name, $this->activeLangcode)->setValue($value); | |
} | |
} | |
// The translations array is unset when cloning the entity object, we just | |
// need to restore it. | |
elseif ($name == 'translations') { | |
$this->translations = $value; | |
} | |
// Directly write non-field values. | |
else { | |
$this->values[$name] = $value; | |
} | |
} | |
/** | |
* Implements the magic method for isset(). | |
*/ | |
public function __isset($name) { | |
// "Official" Field API fields are always set. | |
if ($this->hasField($name)) { | |
return TRUE; | |
} | |
// For non-field properties, check the internal values. | |
else { | |
return isset($this->values[$name]); | |
} | |
} | |
/** | |
* Implements the magic method for unset(). | |
*/ | |
public function __unset($name) { | |
// Unsetting a field means emptying it. | |
if ($this->hasField($name)) { | |
$this->get($name)->setValue(array()); | |
} | |
// For non-field properties, unset the internal value. | |
else { | |
unset($this->values[$name]); | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function createDuplicate() { | |
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) { | |
throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated."); | |
} | |
$duplicate = clone $this; | |
$entity_type = $this->getEntityType(); | |
$duplicate->{$entity_type->getKey('id')}->value = NULL; | |
$duplicate->enforceIsNew(); | |
// Check if the entity type supports UUIDs and generate a new one if so. | |
if ($entity_type->hasKey('uuid')) { | |
$duplicate->{$entity_type->getKey('uuid')}->value = $this->uuidGenerator()->generate(); | |
} | |
// Check whether the entity type supports revisions and initialize it if so. | |
if ($entity_type->isRevisionable()) { | |
$duplicate->{$entity_type->getKey('revision')}->value = NULL; | |
} | |
return $duplicate; | |
} | |
/** | |
* Magic method: Implements a deep clone. | |
*/ | |
public function __clone() { | |
// Avoid deep-cloning when we are initializing a translation object, since | |
// it will represent the same entity, only with a different active language. | |
if (!$this->translationInitialize) { | |
// The translation is a different object, and needs its own TypedData | |
// adapter object. | |
$this->typedData = NULL; | |
$definitions = $this->getFieldDefinitions(); | |
foreach ($this->fields as $name => $values) { | |
$this->fields[$name] = array(); | |
// Untranslatable fields may have multiple references for the same field | |
// object keyed by language. To avoid creating different field objects | |
// we retain just the original value, as references will be recreated | |
// later as needed. | |
if (!$definitions[$name]->isTranslatable() && count($values) > 1) { | |
$values = array_intersect_key($values, array(LanguageInterface::LANGCODE_DEFAULT => TRUE)); | |
} | |
foreach ($values as $langcode => $items) { | |
$this->fields[$name][$langcode] = clone $items; | |
$this->fields[$name][$langcode]->setContext($name, $this->getTranslation($langcode)->getTypedData()); | |
} | |
} | |
// Ensure the translations array is actually cloned by overwriting the | |
// original reference with one pointing to a copy of the array. | |
$this->clearTranslationCache(); | |
$translations = $this->translations; | |
$this->translations = &$translations; | |
} | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function label() { | |
$label = NULL; | |
$entity_type = $this->getEntityType(); | |
if (($label_callback = $entity_type->getLabelCallback()) && is_callable($label_callback)) { | |
$label = call_user_func($label_callback, $this); | |
} | |
elseif (($label_key = $entity_type->getKey('label'))) { | |
$label = $this->getEntityKey('label'); | |
} | |
return $label; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function referencedEntities() { | |
$referenced_entities = array(); | |
// Gather a list of referenced entities. | |
foreach ($this->getFields() as $field_items) { | |
foreach ($field_items as $field_item) { | |
// Loop over all properties of a field item. | |
foreach ($field_item->getProperties(TRUE) as $property) { | |
if ($property instanceof EntityReference && $entity = $property->getValue()) { | |
$referenced_entities[] = $entity; | |
} | |
} | |
} | |
} | |
return $referenced_entities; | |
} | |
/** | |
* Gets the value of the given entity key, if defined. | |
* | |
* @param string $key | |
* Name of the entity key, for example id, revision or bundle. | |
* | |
* @return mixed | |
* The value of the entity key, NULL if not defined. | |
*/ | |
protected function getEntityKey($key) { | |
// If the value is known already, return it. | |
if (isset($this->entityKeys[$key])) { | |
return $this->entityKeys[$key]; | |
} | |
if (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) { | |
return $this->translatableEntityKeys[$key][$this->activeLangcode]; | |
} | |
// Otherwise fetch the value by creating a field object. | |
$value = NULL; | |
if ($this->getEntityType()->hasKey($key)) { | |
$field_name = $this->getEntityType()->getKey($key); | |
$definition = $this->getFieldDefinition($field_name); | |
$property = $definition->getFieldStorageDefinition()->getMainPropertyName(); | |
$value = $this->get($field_name)->$property; | |
// Put it in the right array, depending on whether it is translatable. | |
if ($definition->isTranslatable()) { | |
$this->translatableEntityKeys[$key][$this->activeLangcode] = $value; | |
} | |
else { | |
$this->entityKeys[$key] = $value; | |
} | |
} | |
else { | |
$this->entityKeys[$key] = $value; | |
} | |
return $value; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { | |
return array(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function hasTranslationChanges() { | |
if ($this->isNew()) { | |
return TRUE; | |
} | |
// $this->original only exists during save. See | |
// \Drupal\Core\Entity\EntityStorageBase::save(). If it exists we re-use it | |
// here for performance reasons. | |
/** @var \Drupal\Core\Entity\ContentEntityBase $original */ | |
$original = $this->original ? $this->original : NULL; | |
if (!$original) { | |
$id = $this->getOriginalId() !== NULL ? $this->getOriginalId() : $this->id(); | |
$original = $this->entityManager()->getStorage($this->getEntityTypeId())->loadUnchanged($id); | |
} | |
// If the current translation has just been added, we have a change. | |
$translated = count($this->translations) > 1; | |
if ($translated && !$original->hasTranslation($this->activeLangcode)) { | |
return TRUE; | |
} | |
// Compare field item current values with the original ones to determine | |
// whether we have changes. If a field is not translatable and the entity is | |
// translated we skip it because, depending on the use case, it would make | |
// sense to mark all translations as changed or none of them. We skip also | |
// computed fields as comparing them with their original values might not be | |
// possible or be meaningless. | |
/** @var \Drupal\Core\Entity\ContentEntityBase $translation */ | |
$translation = $original->getTranslation($this->activeLangcode); | |
foreach ($this->getFieldDefinitions() as $field_name => $definition) { | |
// @todo Avoid special-casing the following fields. See | |
// https://www.drupal.org/node/2329253. | |
if ($field_name == 'revision_translation_affected' || $field_name == 'revision_id') { | |
continue; | |
} | |
if (!$definition->isComputed() && (!$translated || $definition->isTranslatable())) { | |
$items = $this->get($field_name)->filterEmptyItems(); | |
$original_items = $translation->get($field_name)->filterEmptyItems(); | |
if (!$items->equals($original_items)) { | |
return TRUE; | |
} | |
} | |
} | |
return FALSE; | |
} | |
} |