ContentEntityBase.php 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462
  1. <?php
  2. namespace Drupal\Core\Entity;
  3. use Drupal\Component\Render\FormattableMarkup;
  4. use Drupal\Core\Entity\Plugin\DataType\EntityReference;
  5. use Drupal\Core\Field\BaseFieldDefinition;
  6. use Drupal\Core\Language\Language;
  7. use Drupal\Core\Language\LanguageInterface;
  8. use Drupal\Core\Session\AccountInterface;
  9. use Drupal\Core\StringTranslation\TranslatableMarkup;
  10. use Drupal\Core\TypedData\TranslationStatusInterface;
  11. use Drupal\Core\TypedData\TypedDataInterface;
  12. /**
  13. * Implements Entity Field API specific enhancements to the Entity class.
  14. *
  15. * @ingroup entity_api
  16. */
  17. abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface {
  18. use EntityChangesDetectionTrait {
  19. getFieldsToSkipFromTranslationChangesCheck as traitGetFieldsToSkipFromTranslationChangesCheck;
  20. }
  21. /**
  22. * The plain data values of the contained fields.
  23. *
  24. * This always holds the original, unchanged values of the entity. The values
  25. * are keyed by language code, whereas LanguageInterface::LANGCODE_DEFAULT
  26. * is used for values in default language.
  27. *
  28. * @todo: Add methods for getting original fields and for determining
  29. * changes.
  30. * @todo: Provide a better way for defining default values.
  31. *
  32. * @var array
  33. */
  34. protected $values = [];
  35. /**
  36. * The array of fields, each being an instance of FieldItemListInterface.
  37. *
  38. * @var array
  39. */
  40. protected $fields = [];
  41. /**
  42. * Local cache for field definitions.
  43. *
  44. * @see ContentEntityBase::getFieldDefinitions()
  45. *
  46. * @var array
  47. */
  48. protected $fieldDefinitions;
  49. /**
  50. * Local cache for the available language objects.
  51. *
  52. * @var \Drupal\Core\Language\LanguageInterface[]
  53. */
  54. protected $languages;
  55. /**
  56. * The language entity key.
  57. *
  58. * @var string
  59. */
  60. protected $langcodeKey;
  61. /**
  62. * The default langcode entity key.
  63. *
  64. * @var string
  65. */
  66. protected $defaultLangcodeKey;
  67. /**
  68. * Language code identifying the entity active language.
  69. *
  70. * This is the language field accessors will use to determine which field
  71. * values manipulate.
  72. *
  73. * @var string
  74. */
  75. protected $activeLangcode = LanguageInterface::LANGCODE_DEFAULT;
  76. /**
  77. * Local cache for the default language code.
  78. *
  79. * @var string
  80. */
  81. protected $defaultLangcode;
  82. /**
  83. * An array of entity translation metadata.
  84. *
  85. * An associative array keyed by translation language code. Every value is an
  86. * array containing the translation status and the translation object, if it has
  87. * already been instantiated.
  88. *
  89. * @var array
  90. */
  91. protected $translations = [];
  92. /**
  93. * A flag indicating whether a translation object is being initialized.
  94. *
  95. * @var bool
  96. */
  97. protected $translationInitialize = FALSE;
  98. /**
  99. * Boolean indicating whether a new revision should be created on save.
  100. *
  101. * @var bool
  102. */
  103. protected $newRevision = FALSE;
  104. /**
  105. * Indicates whether this is the default revision.
  106. *
  107. * @var bool
  108. */
  109. protected $isDefaultRevision = TRUE;
  110. /**
  111. * Holds untranslatable entity keys such as the ID, bundle, and revision ID.
  112. *
  113. * @var array
  114. */
  115. protected $entityKeys = [];
  116. /**
  117. * Holds translatable entity keys such as the label.
  118. *
  119. * @var array
  120. */
  121. protected $translatableEntityKeys = [];
  122. /**
  123. * Whether entity validation was performed.
  124. *
  125. * @var bool
  126. */
  127. protected $validated = FALSE;
  128. /**
  129. * Whether entity validation is required before saving the entity.
  130. *
  131. * @var bool
  132. */
  133. protected $validationRequired = FALSE;
  134. /**
  135. * The loaded revision ID before the new revision was set.
  136. *
  137. * @var int
  138. */
  139. protected $loadedRevisionId;
  140. /**
  141. * The revision translation affected entity key.
  142. *
  143. * @var string
  144. */
  145. protected $revisionTranslationAffectedKey;
  146. /**
  147. * Whether the revision translation affected flag has been enforced.
  148. *
  149. * An array, keyed by the translation language code.
  150. *
  151. * @var bool[]
  152. */
  153. protected $enforceRevisionTranslationAffected = [];
  154. /**
  155. * Local cache for fields to skip from the checking for translation changes.
  156. *
  157. * @var array
  158. */
  159. protected static $fieldsToSkipFromTranslationChangesCheck = [];
  160. /**
  161. * {@inheritdoc}
  162. */
  163. public function __construct(array $values, $entity_type, $bundle = FALSE, $translations = []) {
  164. $this->entityTypeId = $entity_type;
  165. $this->entityKeys['bundle'] = $bundle ? $bundle : $this->entityTypeId;
  166. $this->langcodeKey = $this->getEntityType()->getKey('langcode');
  167. $this->defaultLangcodeKey = $this->getEntityType()->getKey('default_langcode');
  168. $this->revisionTranslationAffectedKey = $this->getEntityType()->getKey('revision_translation_affected');
  169. foreach ($values as $key => $value) {
  170. // If the key matches an existing property set the value to the property
  171. // to set properties like isDefaultRevision.
  172. // @todo: Should this be converted somehow?
  173. if (property_exists($this, $key) && isset($value[LanguageInterface::LANGCODE_DEFAULT])) {
  174. $this->$key = $value[LanguageInterface::LANGCODE_DEFAULT];
  175. }
  176. }
  177. $this->values = $values;
  178. foreach ($this->getEntityType()->getKeys() as $key => $field_name) {
  179. if (isset($this->values[$field_name])) {
  180. if (is_array($this->values[$field_name])) {
  181. // We store untranslatable fields into an entity key without using a
  182. // langcode key.
  183. if (!$this->getFieldDefinition($field_name)->isTranslatable()) {
  184. if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
  185. if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
  186. if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) {
  187. $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'];
  188. }
  189. }
  190. else {
  191. $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
  192. }
  193. }
  194. }
  195. else {
  196. // We save translatable fields such as the publishing status of a node
  197. // into an entity key array keyed by langcode as a performance
  198. // optimization, so we don't have to go through TypedData when we
  199. // need these values.
  200. foreach ($this->values[$field_name] as $langcode => $field_value) {
  201. if (is_array($this->values[$field_name][$langcode])) {
  202. if (isset($this->values[$field_name][$langcode][0]['value'])) {
  203. $this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode][0]['value'];
  204. }
  205. }
  206. else {
  207. $this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode];
  208. }
  209. }
  210. }
  211. }
  212. }
  213. }
  214. // Initialize translations. Ensure we have at least an entry for the default
  215. // language.
  216. // We determine if the entity is new by checking in the entity values for
  217. // the presence of the id entity key, as the usage of ::isNew() is not
  218. // possible in the constructor.
  219. $data = isset($values[$this->getEntityType()->getKey('id')]) ? ['status' => static::TRANSLATION_EXISTING] : ['status' => static::TRANSLATION_CREATED];
  220. $this->translations[LanguageInterface::LANGCODE_DEFAULT] = $data;
  221. $this->setDefaultLangcode();
  222. if ($translations) {
  223. foreach ($translations as $langcode) {
  224. if ($langcode != $this->defaultLangcode && $langcode != LanguageInterface::LANGCODE_DEFAULT) {
  225. $this->translations[$langcode] = $data;
  226. }
  227. }
  228. }
  229. if ($this->getEntityType()->isRevisionable()) {
  230. // Store the loaded revision ID the entity has been loaded with to
  231. // keep it safe from changes.
  232. $this->updateLoadedRevisionId();
  233. }
  234. }
  235. /**
  236. * {@inheritdoc}
  237. */
  238. protected function getLanguages() {
  239. if (empty($this->languages)) {
  240. $this->languages = $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL);
  241. // If the entity references a language that is not or no longer available,
  242. // we return a mock language object to avoid disrupting the consuming
  243. // code.
  244. if (!isset($this->languages[$this->defaultLangcode])) {
  245. $this->languages[$this->defaultLangcode] = new Language(['id' => $this->defaultLangcode]);
  246. }
  247. }
  248. return $this->languages;
  249. }
  250. /**
  251. * {@inheritdoc}
  252. */
  253. public function postCreate(EntityStorageInterface $storage) {
  254. $this->newRevision = TRUE;
  255. }
  256. /**
  257. * {@inheritdoc}
  258. */
  259. public function setNewRevision($value = TRUE) {
  260. if (!$this->getEntityType()->hasKey('revision')) {
  261. throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support revisions.");
  262. }
  263. if ($value && !$this->newRevision) {
  264. // When saving a new revision, set any existing revision ID to NULL so as
  265. // to ensure that a new revision will actually be created.
  266. $this->set($this->getEntityType()->getKey('revision'), NULL);
  267. }
  268. elseif (!$value && $this->newRevision) {
  269. // If ::setNewRevision(FALSE) is called after ::setNewRevision(TRUE) we
  270. // have to restore the loaded revision ID.
  271. $this->set($this->getEntityType()->getKey('revision'), $this->getLoadedRevisionId());
  272. }
  273. $this->newRevision = $value;
  274. }
  275. /**
  276. * {@inheritdoc}
  277. */
  278. public function getLoadedRevisionId() {
  279. return $this->loadedRevisionId;
  280. }
  281. /**
  282. * {@inheritdoc}
  283. */
  284. public function updateLoadedRevisionId() {
  285. $this->loadedRevisionId = $this->getRevisionId() ?: $this->loadedRevisionId;
  286. return $this;
  287. }
  288. /**
  289. * {@inheritdoc}
  290. */
  291. public function isNewRevision() {
  292. return $this->newRevision || ($this->getEntityType()->hasKey('revision') && !$this->getRevisionId());
  293. }
  294. /**
  295. * {@inheritdoc}
  296. */
  297. public function isDefaultRevision($new_value = NULL) {
  298. $return = $this->isDefaultRevision;
  299. if (isset($new_value)) {
  300. $this->isDefaultRevision = (bool) $new_value;
  301. }
  302. // New entities should always ensure at least one default revision exists,
  303. // creating an entity without a default revision is an invalid state.
  304. return $this->isNew() || $return;
  305. }
  306. /**
  307. * {@inheritdoc}
  308. */
  309. public function wasDefaultRevision() {
  310. /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
  311. $entity_type = $this->getEntityType();
  312. if (!$entity_type->isRevisionable()) {
  313. return TRUE;
  314. }
  315. $revision_default_key = $entity_type->getRevisionMetadataKey('revision_default');
  316. $value = $this->isNew() || $this->get($revision_default_key)->value;
  317. return $value;
  318. }
  319. /**
  320. * {@inheritdoc}
  321. */
  322. public function isLatestRevision() {
  323. /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
  324. $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
  325. return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id());
  326. }
  327. /**
  328. * {@inheritdoc}
  329. */
  330. public function isLatestTranslationAffectedRevision() {
  331. /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
  332. $storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
  333. return $this->getLoadedRevisionId() == $storage->getLatestTranslationAffectedRevisionId($this->id(), $this->language()->getId());
  334. }
  335. /**
  336. * {@inheritdoc}
  337. */
  338. public function isRevisionTranslationAffected() {
  339. return $this->hasField($this->revisionTranslationAffectedKey) ? $this->get($this->revisionTranslationAffectedKey)->value : TRUE;
  340. }
  341. /**
  342. * {@inheritdoc}
  343. */
  344. public function setRevisionTranslationAffected($affected) {
  345. if ($this->hasField($this->revisionTranslationAffectedKey)) {
  346. $this->set($this->revisionTranslationAffectedKey, $affected);
  347. }
  348. return $this;
  349. }
  350. /**
  351. * {@inheritdoc}
  352. */
  353. public function isRevisionTranslationAffectedEnforced() {
  354. return !empty($this->enforceRevisionTranslationAffected[$this->activeLangcode]);
  355. }
  356. /**
  357. * {@inheritdoc}
  358. */
  359. public function setRevisionTranslationAffectedEnforced($enforced) {
  360. $this->enforceRevisionTranslationAffected[$this->activeLangcode] = $enforced;
  361. return $this;
  362. }
  363. /**
  364. * {@inheritdoc}
  365. */
  366. public function isDefaultTranslation() {
  367. return $this->activeLangcode === LanguageInterface::LANGCODE_DEFAULT;
  368. }
  369. /**
  370. * {@inheritdoc}
  371. */
  372. public function getRevisionId() {
  373. return $this->getEntityKey('revision');
  374. }
  375. /**
  376. * {@inheritdoc}
  377. */
  378. public function isTranslatable() {
  379. // Check the bundle is translatable, the entity has a language defined, and
  380. // the site has more than one language.
  381. $bundles = $this->entityManager()->getBundleInfo($this->entityTypeId);
  382. return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual();
  383. }
  384. /**
  385. * {@inheritdoc}
  386. */
  387. public function preSave(EntityStorageInterface $storage) {
  388. // An entity requiring validation should not be saved if it has not been
  389. // actually validated.
  390. if ($this->validationRequired && !$this->validated) {
  391. // @todo Make this an assertion in https://www.drupal.org/node/2408013.
  392. throw new \LogicException('Entity validation was skipped.');
  393. }
  394. else {
  395. $this->validated = FALSE;
  396. }
  397. parent::preSave($storage);
  398. }
  399. /**
  400. * {@inheritdoc}
  401. */
  402. public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
  403. }
  404. /**
  405. * {@inheritdoc}
  406. */
  407. public function postSave(EntityStorageInterface $storage, $update = TRUE) {
  408. parent::postSave($storage, $update);
  409. // Update the status of all saved translations.
  410. $removed = [];
  411. foreach ($this->translations as $langcode => &$data) {
  412. if ($data['status'] == static::TRANSLATION_REMOVED) {
  413. $removed[$langcode] = TRUE;
  414. }
  415. else {
  416. $data['status'] = static::TRANSLATION_EXISTING;
  417. }
  418. }
  419. $this->translations = array_diff_key($this->translations, $removed);
  420. // Reset the new revision flag.
  421. $this->newRevision = FALSE;
  422. // Reset the enforcement of the revision translation affected flag.
  423. $this->enforceRevisionTranslationAffected = [];
  424. }
  425. /**
  426. * {@inheritdoc}
  427. */
  428. public function validate() {
  429. $this->validated = TRUE;
  430. $violations = $this->getTypedData()->validate();
  431. return new EntityConstraintViolationList($this, iterator_to_array($violations));
  432. }
  433. /**
  434. * {@inheritdoc}
  435. */
  436. public function isValidationRequired() {
  437. return (bool) $this->validationRequired;
  438. }
  439. /**
  440. * {@inheritdoc}
  441. */
  442. public function setValidationRequired($required) {
  443. $this->validationRequired = $required;
  444. return $this;
  445. }
  446. /**
  447. * Clear entity translation object cache to remove stale references.
  448. */
  449. protected function clearTranslationCache() {
  450. foreach ($this->translations as &$translation) {
  451. unset($translation['entity']);
  452. }
  453. }
  454. /**
  455. * {@inheritdoc}
  456. */
  457. public function __sleep() {
  458. // Get the values of instantiated field objects, only serialize the values.
  459. foreach ($this->fields as $name => $fields) {
  460. foreach ($fields as $langcode => $field) {
  461. $this->values[$name][$langcode] = $field->getValue();
  462. }
  463. }
  464. $this->fields = [];
  465. $this->fieldDefinitions = NULL;
  466. $this->languages = NULL;
  467. $this->clearTranslationCache();
  468. return parent::__sleep();
  469. }
  470. /**
  471. * {@inheritdoc}
  472. */
  473. public function id() {
  474. return $this->getEntityKey('id');
  475. }
  476. /**
  477. * {@inheritdoc}
  478. */
  479. public function bundle() {
  480. return $this->getEntityKey('bundle');
  481. }
  482. /**
  483. * {@inheritdoc}
  484. */
  485. public function uuid() {
  486. return $this->getEntityKey('uuid');
  487. }
  488. /**
  489. * {@inheritdoc}
  490. */
  491. public function hasField($field_name) {
  492. return (bool) $this->getFieldDefinition($field_name);
  493. }
  494. /**
  495. * {@inheritdoc}
  496. */
  497. public function get($field_name) {
  498. if (!isset($this->fields[$field_name][$this->activeLangcode])) {
  499. return $this->getTranslatedField($field_name, $this->activeLangcode);
  500. }
  501. return $this->fields[$field_name][$this->activeLangcode];
  502. }
  503. /**
  504. * Gets a translated field.
  505. *
  506. * @return \Drupal\Core\Field\FieldItemListInterface
  507. */
  508. protected function getTranslatedField($name, $langcode) {
  509. if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
  510. throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.");
  511. }
  512. // Populate $this->fields to speed-up further look-ups and to keep track of
  513. // fields objects, possibly holding changes to field values.
  514. if (!isset($this->fields[$name][$langcode])) {
  515. $definition = $this->getFieldDefinition($name);
  516. if (!$definition) {
  517. throw new \InvalidArgumentException("Field $name is unknown.");
  518. }
  519. // Non-translatable fields are always stored with
  520. // LanguageInterface::LANGCODE_DEFAULT as key.
  521. $default = $langcode == LanguageInterface::LANGCODE_DEFAULT;
  522. if (!$default && !$definition->isTranslatable()) {
  523. if (!isset($this->fields[$name][LanguageInterface::LANGCODE_DEFAULT])) {
  524. $this->fields[$name][LanguageInterface::LANGCODE_DEFAULT] = $this->getTranslatedField($name, LanguageInterface::LANGCODE_DEFAULT);
  525. }
  526. $this->fields[$name][$langcode] = &$this->fields[$name][LanguageInterface::LANGCODE_DEFAULT];
  527. }
  528. else {
  529. $value = NULL;
  530. if (isset($this->values[$name][$langcode])) {
  531. $value = $this->values[$name][$langcode];
  532. }
  533. $field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this->getTranslation($langcode), $name, $value);
  534. if ($default) {
  535. // $this->defaultLangcode might not be set if we are initializing the
  536. // default language code cache, in which case there is no valid
  537. // langcode to assign.
  538. $field_langcode = isset($this->defaultLangcode) ? $this->defaultLangcode : LanguageInterface::LANGCODE_NOT_SPECIFIED;
  539. }
  540. else {
  541. $field_langcode = $langcode;
  542. }
  543. $field->setLangcode($field_langcode);
  544. $this->fields[$name][$langcode] = $field;
  545. }
  546. }
  547. return $this->fields[$name][$langcode];
  548. }
  549. /**
  550. * {@inheritdoc}
  551. */
  552. public function set($name, $value, $notify = TRUE) {
  553. // Assign the value on the child and overrule notify such that we get
  554. // notified to handle changes afterwards. We can ignore notify as there is
  555. // no parent to notify anyway.
  556. $this->get($name)->setValue($value, TRUE);
  557. return $this;
  558. }
  559. /**
  560. * {@inheritdoc}
  561. */
  562. public function getFields($include_computed = TRUE) {
  563. $fields = [];
  564. foreach ($this->getFieldDefinitions() as $name => $definition) {
  565. if ($include_computed || !$definition->isComputed()) {
  566. $fields[$name] = $this->get($name);
  567. }
  568. }
  569. return $fields;
  570. }
  571. /**
  572. * {@inheritdoc}
  573. */
  574. public function getTranslatableFields($include_computed = TRUE) {
  575. $fields = [];
  576. foreach ($this->getFieldDefinitions() as $name => $definition) {
  577. if (($include_computed || !$definition->isComputed()) && $definition->isTranslatable()) {
  578. $fields[$name] = $this->get($name);
  579. }
  580. }
  581. return $fields;
  582. }
  583. /**
  584. * {@inheritdoc}
  585. */
  586. public function getIterator() {
  587. return new \ArrayIterator($this->getFields());
  588. }
  589. /**
  590. * {@inheritdoc}
  591. */
  592. public function getFieldDefinition($name) {
  593. if (!isset($this->fieldDefinitions)) {
  594. $this->getFieldDefinitions();
  595. }
  596. if (isset($this->fieldDefinitions[$name])) {
  597. return $this->fieldDefinitions[$name];
  598. }
  599. }
  600. /**
  601. * {@inheritdoc}
  602. */
  603. public function getFieldDefinitions() {
  604. if (!isset($this->fieldDefinitions)) {
  605. $this->fieldDefinitions = $this->entityManager()->getFieldDefinitions($this->entityTypeId, $this->bundle());
  606. }
  607. return $this->fieldDefinitions;
  608. }
  609. /**
  610. * {@inheritdoc}
  611. */
  612. public function toArray() {
  613. $values = [];
  614. foreach ($this->getFields() as $name => $property) {
  615. $values[$name] = $property->getValue();
  616. }
  617. return $values;
  618. }
  619. /**
  620. * {@inheritdoc}
  621. */
  622. public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
  623. if ($operation == 'create') {
  624. return $this->entityManager()
  625. ->getAccessControlHandler($this->entityTypeId)
  626. ->createAccess($this->bundle(), $account, [], $return_as_object);
  627. }
  628. return $this->entityManager()
  629. ->getAccessControlHandler($this->entityTypeId)
  630. ->access($this, $operation, $account, $return_as_object);
  631. }
  632. /**
  633. * {@inheritdoc}
  634. */
  635. public function language() {
  636. $language = NULL;
  637. if ($this->activeLangcode != LanguageInterface::LANGCODE_DEFAULT) {
  638. if (!isset($this->languages[$this->activeLangcode])) {
  639. $this->getLanguages();
  640. }
  641. $language = $this->languages[$this->activeLangcode];
  642. }
  643. else {
  644. // @todo Avoid this check by getting the language from the language
  645. // manager directly in https://www.drupal.org/node/2303877.
  646. if (!isset($this->languages[$this->defaultLangcode])) {
  647. $this->getLanguages();
  648. }
  649. $language = $this->languages[$this->defaultLangcode];
  650. }
  651. return $language;
  652. }
  653. /**
  654. * Populates the local cache for the default language code.
  655. */
  656. protected function setDefaultLangcode() {
  657. // Get the language code if the property exists.
  658. // Try to read the value directly from the list of entity keys which got
  659. // initialized in __construct(). This avoids creating a field item object.
  660. if (isset($this->translatableEntityKeys['langcode'][$this->activeLangcode])) {
  661. $this->defaultLangcode = $this->translatableEntityKeys['langcode'][$this->activeLangcode];
  662. }
  663. elseif ($this->hasField($this->langcodeKey) && ($item = $this->get($this->langcodeKey)) && isset($item->language)) {
  664. $this->defaultLangcode = $item->language->getId();
  665. $this->translatableEntityKeys['langcode'][$this->activeLangcode] = $this->defaultLangcode;
  666. }
  667. if (empty($this->defaultLangcode)) {
  668. // Make sure we return a proper language object, if the entity has a
  669. // langcode field, default to the site's default language.
  670. if ($this->hasField($this->langcodeKey)) {
  671. $this->defaultLangcode = $this->languageManager()->getDefaultLanguage()->getId();
  672. }
  673. else {
  674. $this->defaultLangcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
  675. }
  676. }
  677. // This needs to be initialized manually as it is skipped when instantiating
  678. // the language field object to avoid infinite recursion.
  679. if (!empty($this->fields[$this->langcodeKey])) {
  680. $this->fields[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT]->setLangcode($this->defaultLangcode);
  681. }
  682. }
  683. /**
  684. * Updates language for already instantiated fields.
  685. */
  686. protected function updateFieldLangcodes($langcode) {
  687. foreach ($this->fields as $name => $items) {
  688. if (!empty($items[LanguageInterface::LANGCODE_DEFAULT])) {
  689. $items[LanguageInterface::LANGCODE_DEFAULT]->setLangcode($langcode);
  690. }
  691. }
  692. }
  693. /**
  694. * {@inheritdoc}
  695. */
  696. public function onChange($name) {
  697. // Check if the changed name is the value of any entity keys and if any of
  698. // those values are currently cached, if so, reset it. Exclude the bundle
  699. // from that check, as it ready only and must not change, unsetting it could
  700. // lead to recursions.
  701. foreach (array_keys($this->getEntityType()->getKeys(), $name, TRUE) as $key) {
  702. if ($key != 'bundle') {
  703. if (isset($this->entityKeys[$key])) {
  704. unset($this->entityKeys[$key]);
  705. }
  706. elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
  707. unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
  708. }
  709. // If the revision identifier field is being populated with the original
  710. // value, we need to make sure the "new revision" flag is reset
  711. // accordingly.
  712. if ($key === 'revision' && $this->getRevisionId() == $this->getLoadedRevisionId() && !$this->isNew()) {
  713. $this->newRevision = FALSE;
  714. }
  715. }
  716. }
  717. switch ($name) {
  718. case $this->langcodeKey:
  719. if ($this->isDefaultTranslation()) {
  720. // Update the default internal language cache.
  721. $this->setDefaultLangcode();
  722. if (isset($this->translations[$this->defaultLangcode])) {
  723. $message = new FormattableMarkup('A translation already exists for the specified language (@langcode).', ['@langcode' => $this->defaultLangcode]);
  724. throw new \InvalidArgumentException($message);
  725. }
  726. $this->updateFieldLangcodes($this->defaultLangcode);
  727. }
  728. else {
  729. // @todo Allow the translation language to be changed. See
  730. // https://www.drupal.org/node/2443989.
  731. $items = $this->get($this->langcodeKey);
  732. if ($items->value != $this->activeLangcode) {
  733. $items->setValue($this->activeLangcode, FALSE);
  734. $message = new FormattableMarkup('The translation language cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
  735. throw new \LogicException($message);
  736. }
  737. }
  738. break;
  739. case $this->defaultLangcodeKey:
  740. // @todo Use a standard method to make the default_langcode field
  741. // read-only. See https://www.drupal.org/node/2443991.
  742. if (isset($this->values[$this->defaultLangcodeKey]) && $this->get($this->defaultLangcodeKey)->value != $this->isDefaultTranslation()) {
  743. $this->get($this->defaultLangcodeKey)->setValue($this->isDefaultTranslation(), FALSE);
  744. $message = new FormattableMarkup('The default translation flag cannot be changed (@langcode).', ['@langcode' => $this->activeLangcode]);
  745. throw new \LogicException($message);
  746. }
  747. break;
  748. case $this->revisionTranslationAffectedKey:
  749. // If the revision translation affected flag is being set then enforce
  750. // its value.
  751. $this->setRevisionTranslationAffectedEnforced(TRUE);
  752. break;
  753. }
  754. }
  755. /**
  756. * {@inheritdoc}
  757. */
  758. public function getTranslation($langcode) {
  759. // Ensure we always use the default language code when dealing with the
  760. // original entity language.
  761. if ($langcode != LanguageInterface::LANGCODE_DEFAULT && $langcode == $this->defaultLangcode) {
  762. $langcode = LanguageInterface::LANGCODE_DEFAULT;
  763. }
  764. // Populate entity translation object cache so it will be available for all
  765. // translation objects.
  766. if (!isset($this->translations[$this->activeLangcode]['entity'])) {
  767. $this->translations[$this->activeLangcode]['entity'] = $this;
  768. }
  769. // If we already have a translation object for the specified language we can
  770. // just return it.
  771. if (isset($this->translations[$langcode]['entity'])) {
  772. $translation = $this->translations[$langcode]['entity'];
  773. }
  774. // Otherwise if an existing translation language was specified we need to
  775. // instantiate the related translation.
  776. elseif (isset($this->translations[$langcode])) {
  777. $translation = $this->initializeTranslation($langcode);
  778. $this->translations[$langcode]['entity'] = $translation;
  779. }
  780. if (empty($translation)) {
  781. throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
  782. }
  783. return $translation;
  784. }
  785. /**
  786. * {@inheritdoc}
  787. */
  788. public function getUntranslated() {
  789. return $this->getTranslation(LanguageInterface::LANGCODE_DEFAULT);
  790. }
  791. /**
  792. * Instantiates a translation object for an existing translation.
  793. *
  794. * The translated entity will be a clone of the current entity with the
  795. * specified $langcode. All translations share the same field data structures
  796. * to ensure that all of them deal with fresh data.
  797. *
  798. * @param string $langcode
  799. * The language code for the requested translation.
  800. *
  801. * @return \Drupal\Core\Entity\EntityInterface
  802. * The translation object. The content properties of the translation object
  803. * are stored as references to the main entity.
  804. */
  805. protected function initializeTranslation($langcode) {
  806. // If the requested translation is valid, clone it with the current language
  807. // as the active language. The $translationInitialize flag triggers a
  808. // shallow (non-recursive) clone.
  809. $this->translationInitialize = TRUE;
  810. $translation = clone $this;
  811. $this->translationInitialize = FALSE;
  812. $translation->activeLangcode = $langcode;
  813. // Ensure that changes to fields, values and translations are propagated
  814. // to all the translation objects.
  815. // @todo Consider converting these to ArrayObject.
  816. $translation->values = &$this->values;
  817. $translation->fields = &$this->fields;
  818. $translation->translations = &$this->translations;
  819. $translation->enforceIsNew = &$this->enforceIsNew;
  820. $translation->newRevision = &$this->newRevision;
  821. $translation->entityKeys = &$this->entityKeys;
  822. $translation->translatableEntityKeys = &$this->translatableEntityKeys;
  823. $translation->translationInitialize = FALSE;
  824. $translation->typedData = NULL;
  825. $translation->loadedRevisionId = &$this->loadedRevisionId;
  826. $translation->isDefaultRevision = &$this->isDefaultRevision;
  827. $translation->enforceRevisionTranslationAffected = &$this->enforceRevisionTranslationAffected;
  828. return $translation;
  829. }
  830. /**
  831. * {@inheritdoc}
  832. */
  833. public function hasTranslation($langcode) {
  834. if ($langcode == $this->defaultLangcode) {
  835. $langcode = LanguageInterface::LANGCODE_DEFAULT;
  836. }
  837. return !empty($this->translations[$langcode]['status']);
  838. }
  839. /**
  840. * {@inheritdoc}
  841. */
  842. public function isNewTranslation() {
  843. return $this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_CREATED;
  844. }
  845. /**
  846. * {@inheritdoc}
  847. */
  848. public function addTranslation($langcode, array $values = []) {
  849. // Make sure we do not attempt to create a translation if an invalid
  850. // language is specified or the entity cannot be translated.
  851. $this->getLanguages();
  852. if (!isset($this->languages[$langcode]) || $this->hasTranslation($langcode) || $this->languages[$langcode]->isLocked()) {
  853. throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
  854. }
  855. if ($this->languages[$this->defaultLangcode]->isLocked()) {
  856. throw new \InvalidArgumentException("The entity cannot be translated since it is language neutral ({$this->defaultLangcode}).");
  857. }
  858. // Initialize the translation object.
  859. /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
  860. $storage = $this->entityManager()->getStorage($this->getEntityTypeId());
  861. $this->translations[$langcode]['status'] = !isset($this->translations[$langcode]['status_existed']) ? static::TRANSLATION_CREATED : static::TRANSLATION_EXISTING;
  862. return $storage->createTranslation($this, $langcode, $values);
  863. }
  864. /**
  865. * {@inheritdoc}
  866. */
  867. public function removeTranslation($langcode) {
  868. if (isset($this->translations[$langcode]) && $langcode != LanguageInterface::LANGCODE_DEFAULT && $langcode != $this->defaultLangcode) {
  869. foreach ($this->getFieldDefinitions() as $name => $definition) {
  870. if ($definition->isTranslatable()) {
  871. unset($this->values[$name][$langcode]);
  872. unset($this->fields[$name][$langcode]);
  873. }
  874. }
  875. // If removing a translation which has not been saved yet, then we have
  876. // to remove it completely so that ::getTranslationStatus returns the
  877. // proper status.
  878. if ($this->translations[$langcode]['status'] == static::TRANSLATION_CREATED) {
  879. unset($this->translations[$langcode]);
  880. }
  881. else {
  882. if ($this->translations[$langcode]['status'] == static::TRANSLATION_EXISTING) {
  883. $this->translations[$langcode]['status_existed'] = TRUE;
  884. }
  885. $this->translations[$langcode]['status'] = static::TRANSLATION_REMOVED;
  886. }
  887. }
  888. else {
  889. throw new \InvalidArgumentException("The specified translation ($langcode) cannot be removed.");
  890. }
  891. }
  892. /**
  893. * {@inheritdoc}
  894. */
  895. public function getTranslationStatus($langcode) {
  896. if ($langcode == $this->defaultLangcode) {
  897. $langcode = LanguageInterface::LANGCODE_DEFAULT;
  898. }
  899. return isset($this->translations[$langcode]) ? $this->translations[$langcode]['status'] : NULL;
  900. }
  901. /**
  902. * {@inheritdoc}
  903. */
  904. public function getTranslationLanguages($include_default = TRUE) {
  905. $translations = array_filter($this->translations, function ($translation) {
  906. return $translation['status'];
  907. });
  908. unset($translations[LanguageInterface::LANGCODE_DEFAULT]);
  909. if ($include_default) {
  910. $translations[$this->defaultLangcode] = TRUE;
  911. }
  912. // Now load language objects based upon translation langcodes.
  913. return array_intersect_key($this->getLanguages(), $translations);
  914. }
  915. /**
  916. * Updates the original values with the interim changes.
  917. */
  918. public function updateOriginalValues() {
  919. if (!$this->fields) {
  920. return;
  921. }
  922. foreach ($this->getFieldDefinitions() as $name => $definition) {
  923. if (!$definition->isComputed() && !empty($this->fields[$name])) {
  924. foreach ($this->fields[$name] as $langcode => $item) {
  925. $item->filterEmptyItems();
  926. $this->values[$name][$langcode] = $item->getValue();
  927. }
  928. }
  929. }
  930. }
  931. /**
  932. * Implements the magic method for getting object properties.
  933. *
  934. * @todo: A lot of code still uses non-fields (e.g. $entity->content in view
  935. * builders) by reference. Clean that up.
  936. */
  937. public function &__get($name) {
  938. // If this is an entity field, handle it accordingly. We first check whether
  939. // a field object has been already created. If not, we create one.
  940. if (isset($this->fields[$name][$this->activeLangcode])) {
  941. return $this->fields[$name][$this->activeLangcode];
  942. }
  943. // Inline getFieldDefinition() to speed things up.
  944. if (!isset($this->fieldDefinitions)) {
  945. $this->getFieldDefinitions();
  946. }
  947. if (isset($this->fieldDefinitions[$name])) {
  948. $return = $this->getTranslatedField($name, $this->activeLangcode);
  949. return $return;
  950. }
  951. // Else directly read/write plain values. That way, non-field entity
  952. // properties can always be accessed directly.
  953. if (!isset($this->values[$name])) {
  954. $this->values[$name] = NULL;
  955. }
  956. return $this->values[$name];
  957. }
  958. /**
  959. * Implements the magic method for setting object properties.
  960. *
  961. * Uses default language always.
  962. */
  963. public function __set($name, $value) {
  964. // Inline getFieldDefinition() to speed things up.
  965. if (!isset($this->fieldDefinitions)) {
  966. $this->getFieldDefinitions();
  967. }
  968. // Handle Field API fields.
  969. if (isset($this->fieldDefinitions[$name])) {
  970. // Support setting values via property objects.
  971. if ($value instanceof TypedDataInterface) {
  972. $value = $value->getValue();
  973. }
  974. // If a FieldItemList object already exists, set its value.
  975. if (isset($this->fields[$name][$this->activeLangcode])) {
  976. $this->fields[$name][$this->activeLangcode]->setValue($value);
  977. }
  978. // If not, create one.
  979. else {
  980. $this->getTranslatedField($name, $this->activeLangcode)->setValue($value);
  981. }
  982. }
  983. // The translations array is unset when cloning the entity object, we just
  984. // need to restore it.
  985. elseif ($name == 'translations') {
  986. $this->translations = $value;
  987. }
  988. // Directly write non-field values.
  989. else {
  990. $this->values[$name] = $value;
  991. }
  992. }
  993. /**
  994. * Implements the magic method for isset().
  995. */
  996. public function __isset($name) {
  997. // "Official" Field API fields are always set. For non-field properties,
  998. // check the internal values.
  999. return $this->hasField($name) ? TRUE : isset($this->values[$name]);
  1000. }
  1001. /**
  1002. * Implements the magic method for unset().
  1003. */
  1004. public function __unset($name) {
  1005. // Unsetting a field means emptying it.
  1006. if ($this->hasField($name)) {
  1007. $this->get($name)->setValue([]);
  1008. }
  1009. // For non-field properties, unset the internal value.
  1010. else {
  1011. unset($this->values[$name]);
  1012. }
  1013. }
  1014. /**
  1015. * {@inheritdoc}
  1016. */
  1017. public function createDuplicate() {
  1018. if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
  1019. throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.");
  1020. }
  1021. $duplicate = clone $this;
  1022. $entity_type = $this->getEntityType();
  1023. if ($entity_type->hasKey('id')) {
  1024. $duplicate->{$entity_type->getKey('id')}->value = NULL;
  1025. }
  1026. $duplicate->enforceIsNew();
  1027. // Check if the entity type supports UUIDs and generate a new one if so.
  1028. if ($entity_type->hasKey('uuid')) {
  1029. $duplicate->{$entity_type->getKey('uuid')}->value = $this->uuidGenerator()->generate();
  1030. }
  1031. // Check whether the entity type supports revisions and initialize it if so.
  1032. if ($entity_type->isRevisionable()) {
  1033. $duplicate->{$entity_type->getKey('revision')}->value = NULL;
  1034. $duplicate->loadedRevisionId = NULL;
  1035. }
  1036. return $duplicate;
  1037. }
  1038. /**
  1039. * Magic method: Implements a deep clone.
  1040. */
  1041. public function __clone() {
  1042. // Avoid deep-cloning when we are initializing a translation object, since
  1043. // it will represent the same entity, only with a different active language.
  1044. if ($this->translationInitialize) {
  1045. return;
  1046. }
  1047. // The translation is a different object, and needs its own TypedData
  1048. // adapter object.
  1049. $this->typedData = NULL;
  1050. $definitions = $this->getFieldDefinitions();
  1051. // The translation cache has to be cleared before cloning the fields
  1052. // below so that the call to getTranslation() does not re-use the
  1053. // translation objects of the old entity but instead creates new
  1054. // translation objects from the newly cloned entity. Otherwise the newly
  1055. // cloned field item lists would hold references to the old translation
  1056. // objects in their $parent property after the call to setContext().
  1057. $this->clearTranslationCache();
  1058. // Because the new translation objects that are created below are
  1059. // themselves created by *cloning* the newly cloned entity we need to
  1060. // make sure that the references to property values are properly cloned
  1061. // before cloning the fields. Otherwise calling
  1062. // $items->getEntity()->isNew(), for example, would return the
  1063. // $enforceIsNew value of the old entity.
  1064. // Ensure the translations array is actually cloned by overwriting the
  1065. // original reference with one pointing to a copy of the array.
  1066. $translations = $this->translations;
  1067. $this->translations = &$translations;
  1068. // Ensure that the following properties are actually cloned by
  1069. // overwriting the original references with ones pointing to copies of
  1070. // them: enforceIsNew, newRevision, loadedRevisionId, fields, entityKeys,
  1071. // translatableEntityKeys, values, isDefaultRevision and
  1072. // enforceRevisionTranslationAffected.
  1073. $enforce_is_new = $this->enforceIsNew;
  1074. $this->enforceIsNew = &$enforce_is_new;
  1075. $new_revision = $this->newRevision;
  1076. $this->newRevision = &$new_revision;
  1077. $original_revision_id = $this->loadedRevisionId;
  1078. $this->loadedRevisionId = &$original_revision_id;
  1079. $fields = $this->fields;
  1080. $this->fields = &$fields;
  1081. $entity_keys = $this->entityKeys;
  1082. $this->entityKeys = &$entity_keys;
  1083. $translatable_entity_keys = $this->translatableEntityKeys;
  1084. $this->translatableEntityKeys = &$translatable_entity_keys;
  1085. $values = $this->values;
  1086. $this->values = &$values;
  1087. $default_revision = $this->isDefaultRevision;
  1088. $this->isDefaultRevision = &$default_revision;
  1089. $is_revision_translation_affected_enforced = $this->enforceRevisionTranslationAffected;
  1090. $this->enforceRevisionTranslationAffected = &$is_revision_translation_affected_enforced;
  1091. foreach ($this->fields as $name => $fields_by_langcode) {
  1092. $this->fields[$name] = [];
  1093. // Untranslatable fields may have multiple references for the same field
  1094. // object keyed by language. To avoid creating different field objects
  1095. // we retain just the original value, as references will be recreated
  1096. // later as needed.
  1097. if (!$definitions[$name]->isTranslatable() && count($fields_by_langcode) > 1) {
  1098. $fields_by_langcode = array_intersect_key($fields_by_langcode, [LanguageInterface::LANGCODE_DEFAULT => TRUE]);
  1099. }
  1100. foreach ($fields_by_langcode as $langcode => $items) {
  1101. $this->fields[$name][$langcode] = clone $items;
  1102. $this->fields[$name][$langcode]->setContext($name, $this->getTranslation($langcode)->getTypedData());
  1103. }
  1104. }
  1105. }
  1106. /**
  1107. * {@inheritdoc}
  1108. */
  1109. public function label() {
  1110. $label = NULL;
  1111. $entity_type = $this->getEntityType();
  1112. if (($label_callback = $entity_type->getLabelCallback()) && is_callable($label_callback)) {
  1113. $label = call_user_func($label_callback, $this);
  1114. }
  1115. elseif (($label_key = $entity_type->getKey('label'))) {
  1116. $label = $this->getEntityKey('label');
  1117. }
  1118. return $label;
  1119. }
  1120. /**
  1121. * {@inheritdoc}
  1122. */
  1123. public function referencedEntities() {
  1124. $referenced_entities = [];
  1125. // Gather a list of referenced entities.
  1126. foreach ($this->getFields() as $field_items) {
  1127. foreach ($field_items as $field_item) {
  1128. // Loop over all properties of a field item.
  1129. foreach ($field_item->getProperties(TRUE) as $property) {
  1130. if ($property instanceof EntityReference && $entity = $property->getValue()) {
  1131. $referenced_entities[] = $entity;
  1132. }
  1133. }
  1134. }
  1135. }
  1136. return $referenced_entities;
  1137. }
  1138. /**
  1139. * Gets the value of the given entity key, if defined.
  1140. *
  1141. * @param string $key
  1142. * Name of the entity key, for example id, revision or bundle.
  1143. *
  1144. * @return mixed
  1145. * The value of the entity key, NULL if not defined.
  1146. */
  1147. protected function getEntityKey($key) {
  1148. // If the value is known already, return it.
  1149. if (isset($this->entityKeys[$key])) {
  1150. return $this->entityKeys[$key];
  1151. }
  1152. if (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
  1153. return $this->translatableEntityKeys[$key][$this->activeLangcode];
  1154. }
  1155. // Otherwise fetch the value by creating a field object.
  1156. $value = NULL;
  1157. if ($this->getEntityType()->hasKey($key)) {
  1158. $field_name = $this->getEntityType()->getKey($key);
  1159. $definition = $this->getFieldDefinition($field_name);
  1160. $property = $definition->getFieldStorageDefinition()->getMainPropertyName();
  1161. $value = $this->get($field_name)->$property;
  1162. // Put it in the right array, depending on whether it is translatable.
  1163. if ($definition->isTranslatable()) {
  1164. $this->translatableEntityKeys[$key][$this->activeLangcode] = $value;
  1165. }
  1166. else {
  1167. $this->entityKeys[$key] = $value;
  1168. }
  1169. }
  1170. else {
  1171. $this->entityKeys[$key] = $value;
  1172. }
  1173. return $value;
  1174. }
  1175. /**
  1176. * {@inheritdoc}
  1177. */
  1178. public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
  1179. $fields = [];
  1180. if ($entity_type->hasKey('id')) {
  1181. $fields[$entity_type->getKey('id')] = BaseFieldDefinition::create('integer')
  1182. ->setLabel(new TranslatableMarkup('ID'))
  1183. ->setReadOnly(TRUE)
  1184. ->setSetting('unsigned', TRUE);
  1185. }
  1186. if ($entity_type->hasKey('uuid')) {
  1187. $fields[$entity_type->getKey('uuid')] = BaseFieldDefinition::create('uuid')
  1188. ->setLabel(new TranslatableMarkup('UUID'))
  1189. ->setReadOnly(TRUE);
  1190. }
  1191. if ($entity_type->hasKey('revision')) {
  1192. $fields[$entity_type->getKey('revision')] = BaseFieldDefinition::create('integer')
  1193. ->setLabel(new TranslatableMarkup('Revision ID'))
  1194. ->setReadOnly(TRUE)
  1195. ->setSetting('unsigned', TRUE);
  1196. }
  1197. if ($entity_type->hasKey('langcode')) {
  1198. $fields[$entity_type->getKey('langcode')] = BaseFieldDefinition::create('language')
  1199. ->setLabel(new TranslatableMarkup('Language'))
  1200. ->setDisplayOptions('view', [
  1201. 'region' => 'hidden',
  1202. ])
  1203. ->setDisplayOptions('form', [
  1204. 'type' => 'language_select',
  1205. 'weight' => 2,
  1206. ]);
  1207. if ($entity_type->isRevisionable()) {
  1208. $fields[$entity_type->getKey('langcode')]->setRevisionable(TRUE);
  1209. }
  1210. if ($entity_type->isTranslatable()) {
  1211. $fields[$entity_type->getKey('langcode')]->setTranslatable(TRUE);
  1212. }
  1213. }
  1214. if ($entity_type->hasKey('bundle')) {
  1215. if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
  1216. $fields[$entity_type->getKey('bundle')] = BaseFieldDefinition::create('entity_reference')
  1217. ->setLabel($entity_type->getBundleLabel())
  1218. ->setSetting('target_type', $bundle_entity_type_id)
  1219. ->setRequired(TRUE)
  1220. ->setReadOnly(TRUE);
  1221. }
  1222. else {
  1223. $fields[$entity_type->getKey('bundle')] = BaseFieldDefinition::create('string')
  1224. ->setLabel($entity_type->getBundleLabel())
  1225. ->setRequired(TRUE)
  1226. ->setReadOnly(TRUE);
  1227. }
  1228. }
  1229. return $fields;
  1230. }
  1231. /**
  1232. * {@inheritdoc}
  1233. */
  1234. public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
  1235. return [];
  1236. }
  1237. /**
  1238. * Returns an array of field names to skip in ::hasTranslationChanges.
  1239. *
  1240. * @return array
  1241. * An array of field names.
  1242. */
  1243. protected function getFieldsToSkipFromTranslationChangesCheck() {
  1244. $bundle = $this->bundle();
  1245. if (!isset(static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle])) {
  1246. static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle] = $this->traitGetFieldsToSkipFromTranslationChangesCheck($this);
  1247. }
  1248. return static::$fieldsToSkipFromTranslationChangesCheck[$this->entityTypeId][$bundle];
  1249. }
  1250. /**
  1251. * {@inheritdoc}
  1252. */
  1253. public function hasTranslationChanges() {
  1254. if ($this->isNew()) {
  1255. return TRUE;
  1256. }
  1257. // $this->original only exists during save. See
  1258. // \Drupal\Core\Entity\EntityStorageBase::save(). If it exists we re-use it
  1259. // here for performance reasons.
  1260. /** @var \Drupal\Core\Entity\ContentEntityBase $original */
  1261. $original = $this->original ? $this->original : NULL;
  1262. if (!$original) {
  1263. $id = $this->getOriginalId() !== NULL ? $this->getOriginalId() : $this->id();
  1264. $original = $this->entityManager()->getStorage($this->getEntityTypeId())->loadUnchanged($id);
  1265. }
  1266. // If the current translation has just been added, we have a change.
  1267. $translated = count($this->translations) > 1;
  1268. if ($translated && !$original->hasTranslation($this->activeLangcode)) {
  1269. return TRUE;
  1270. }
  1271. // Compare field item current values with the original ones to determine
  1272. // whether we have changes. If a field is not translatable and the entity is
  1273. // translated we skip it because, depending on the use case, it would make
  1274. // sense to mark all translations as changed or none of them. We skip also
  1275. // computed fields as comparing them with their original values might not be
  1276. // possible or be meaningless.
  1277. /** @var \Drupal\Core\Entity\ContentEntityBase $translation */
  1278. $translation = $original->getTranslation($this->activeLangcode);
  1279. $langcode = $this->language()->getId();
  1280. // The list of fields to skip from the comparision.
  1281. $skip_fields = $this->getFieldsToSkipFromTranslationChangesCheck();
  1282. // We also check untranslatable fields, so that a change to those will mark
  1283. // all translations as affected, unless they are configured to only affect
  1284. // the default translation.
  1285. $skip_untranslatable_fields = !$this->isDefaultTranslation() && $this->isDefaultTranslationAffectedOnly();
  1286. foreach ($this->getFieldDefinitions() as $field_name => $definition) {
  1287. // @todo Avoid special-casing the following fields. See
  1288. // https://www.drupal.org/node/2329253.
  1289. if (in_array($field_name, $skip_fields, TRUE) || ($skip_untranslatable_fields && !$definition->isTranslatable())) {
  1290. continue;
  1291. }
  1292. $items = $this->get($field_name)->filterEmptyItems();
  1293. $original_items = $translation->get($field_name)->filterEmptyItems();
  1294. if ($items->hasAffectingChanges($original_items, $langcode)) {
  1295. return TRUE;
  1296. }
  1297. }
  1298. return FALSE;
  1299. }
  1300. /**
  1301. * {@inheritdoc}
  1302. */
  1303. public function isDefaultTranslationAffectedOnly() {
  1304. $bundle_name = $this->bundle();
  1305. $bundle_info = \Drupal::service('entity_type.bundle.info')
  1306. ->getBundleInfo($this->getEntityTypeId());
  1307. return !empty($bundle_info[$bundle_name]['untranslatable_fields.default_translation_affected']);
  1308. }
  1309. }