vendor/gedmo/doctrine-extensions/src/Translatable/TranslatableListener.php line 217

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Doctrine Behavioral Extensions package.
  4. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. namespace Gedmo\Translatable;
  9. use Doctrine\Common\EventArgs;
  10. use Doctrine\ODM\MongoDB\DocumentManager;
  11. use Doctrine\ORM\ORMInvalidArgumentException;
  12. use Doctrine\Persistence\Event\LifecycleEventArgs;
  13. use Doctrine\Persistence\Event\LoadClassMetadataEventArgs;
  14. use Doctrine\Persistence\Event\ManagerEventArgs;
  15. use Doctrine\Persistence\Mapping\ClassMetadata;
  16. use Doctrine\Persistence\ObjectManager;
  17. use Gedmo\Exception\InvalidArgumentException;
  18. use Gedmo\Exception\RuntimeException;
  19. use Gedmo\Mapping\MappedEventSubscriber;
  20. use Gedmo\Tool\Wrapper\AbstractWrapper;
  21. use Gedmo\Translatable\Mapping\Event\TranslatableAdapter;
  22. /**
  23. * The translation listener handles the generation and
  24. * loading of translations for entities which implements
  25. * the Translatable interface.
  26. *
  27. * This behavior can impact the performance of your application
  28. * since it does an additional query for each field to translate.
  29. *
  30. * Nevertheless the annotation metadata is properly cached and
  31. * it is not a big overhead to lookup all entity annotations since
  32. * the caching is activated for metadata
  33. *
  34. * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  35. *
  36. * @phpstan-type TranslatableConfiguration = array{
  37. * fields?: string[],
  38. * fallback?: array<string, bool>,
  39. * locale?: string,
  40. * translationClass?: class-string,
  41. * useObjectClass?: class-string,
  42. * }
  43. *
  44. * @phpstan-extends MappedEventSubscriber<TranslatableConfiguration, TranslatableAdapter>
  45. *
  46. * @final since gedmo/doctrine-extensions 3.11
  47. */
  48. class TranslatableListener extends MappedEventSubscriber
  49. {
  50. /**
  51. * Query hint to override the fallback of translations
  52. * integer 1 for true, 0 false
  53. */
  54. public const HINT_FALLBACK = 'gedmo.translatable.fallback';
  55. /**
  56. * Query hint to override the fallback locale
  57. */
  58. public const HINT_TRANSLATABLE_LOCALE = 'gedmo.translatable.locale';
  59. /**
  60. * Query hint to use inner join strategy for translations
  61. */
  62. public const HINT_INNER_JOIN = 'gedmo.translatable.inner_join.translations';
  63. /**
  64. * Locale which is set on this listener.
  65. * If Entity being translated has locale defined it
  66. * will override this one
  67. *
  68. * @var string
  69. */
  70. protected $locale = 'en_US';
  71. /**
  72. * Default locale, this changes behavior
  73. * to not update the original record field if locale
  74. * which is used for updating is not default. This
  75. * will load the default translation in other locales
  76. * if record is not translated yet
  77. */
  78. private string $defaultLocale = 'en_US';
  79. /**
  80. * If this is set to false, when if entity does
  81. * not have a translation for requested locale
  82. * it will show a blank value
  83. */
  84. private bool $translationFallback = false;
  85. /**
  86. * List of translations which do not have the foreign
  87. * key generated yet - MySQL case. These translations
  88. * will be updated with new keys on postPersist event
  89. *
  90. * @var array<int, array<int, object|Translatable>>
  91. */
  92. private array $pendingTranslationInserts = [];
  93. /**
  94. * Currently in case if there is TranslationQueryWalker
  95. * in charge. We need to skip issuing additional queries
  96. * on load
  97. */
  98. private bool $skipOnLoad = false;
  99. /**
  100. * Tracks locale the objects currently translated in
  101. *
  102. * @var array<int, string>
  103. */
  104. private array $translatedInLocale = [];
  105. /**
  106. * Whether or not, to persist default locale
  107. * translation or keep it in original record
  108. */
  109. private bool $persistDefaultLocaleTranslation = false;
  110. /**
  111. * Tracks translation object for default locale
  112. *
  113. * @var array<int, array<string, object|Translatable>>
  114. */
  115. private array $translationInDefaultLocale = [];
  116. /**
  117. * Default translation value upon missing translation
  118. */
  119. private ?string $defaultTranslationValue = null;
  120. /**
  121. * Specifies the list of events to listen
  122. *
  123. * @return string[]
  124. */
  125. public function getSubscribedEvents()
  126. {
  127. return [
  128. 'postLoad',
  129. 'postPersist',
  130. 'preFlush',
  131. 'onFlush',
  132. 'loadClassMetadata',
  133. ];
  134. }
  135. /**
  136. * Set to skip or not onLoad event
  137. *
  138. * @param bool $bool
  139. *
  140. * @return static
  141. */
  142. public function setSkipOnLoad($bool)
  143. {
  144. $this->skipOnLoad = (bool) $bool;
  145. return $this;
  146. }
  147. /**
  148. * Whether or not, to persist default locale
  149. * translation or keep it in original record
  150. *
  151. * @param bool $bool
  152. *
  153. * @return static
  154. */
  155. public function setPersistDefaultLocaleTranslation($bool)
  156. {
  157. $this->persistDefaultLocaleTranslation = (bool) $bool;
  158. return $this;
  159. }
  160. /**
  161. * Check if should persist default locale
  162. * translation or keep it in original record
  163. *
  164. * @return bool
  165. */
  166. public function getPersistDefaultLocaleTranslation()
  167. {
  168. return (bool) $this->persistDefaultLocaleTranslation;
  169. }
  170. /**
  171. * Add additional $translation for pending $oid object
  172. * which is being inserted
  173. *
  174. * @param int $oid
  175. * @param object $translation
  176. *
  177. * @return void
  178. */
  179. public function addPendingTranslationInsert($oid, $translation)
  180. {
  181. $this->pendingTranslationInserts[$oid][] = $translation;
  182. }
  183. /**
  184. * Maps additional metadata
  185. *
  186. * @param LoadClassMetadataEventArgs $eventArgs
  187. *
  188. * @phpstan-param LoadClassMetadataEventArgs<ClassMetadata<object>, ObjectManager> $eventArgs
  189. *
  190. * @return void
  191. */
  192. public function loadClassMetadata(EventArgs $eventArgs)
  193. {
  194. $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata());
  195. }
  196. /**
  197. * Get the translation class to be used
  198. * for the object $class
  199. *
  200. * @param string $class
  201. *
  202. * @phpstan-param class-string $class
  203. *
  204. * @return string
  205. *
  206. * @phpstan-return class-string
  207. */
  208. public function getTranslationClass(TranslatableAdapter $ea, $class)
  209. {
  210. return self::$configurations[$this->name][$class]['translationClass'] ?? $ea->getDefaultTranslationClass()
  211. ;
  212. }
  213. /**
  214. * Enable or disable translation fallback
  215. * to original record value
  216. *
  217. * @param bool $bool
  218. *
  219. * @return static
  220. */
  221. public function setTranslationFallback($bool)
  222. {
  223. $this->translationFallback = (bool) $bool;
  224. return $this;
  225. }
  226. /**
  227. * Weather or not is using the translation
  228. * fallback to original record
  229. *
  230. * @return bool
  231. */
  232. public function getTranslationFallback()
  233. {
  234. return $this->translationFallback;
  235. }
  236. /**
  237. * Set the locale to use for translation listener
  238. *
  239. * @param string $locale
  240. *
  241. * @return static
  242. */
  243. public function setTranslatableLocale($locale)
  244. {
  245. $this->validateLocale($locale);
  246. $this->locale = $locale;
  247. return $this;
  248. }
  249. /**
  250. * Set the default translation value on missing translation
  251. *
  252. * @deprecated usage of a non nullable value for defaultTranslationValue is deprecated
  253. * and will be removed on the next major release which will rely on the expected types
  254. */
  255. public function setDefaultTranslationValue(?string $defaultTranslationValue): void
  256. {
  257. $this->defaultTranslationValue = $defaultTranslationValue;
  258. }
  259. /**
  260. * Sets the default locale, this changes behavior
  261. * to not update the original record field if locale
  262. * which is used for updating is not default
  263. *
  264. * @param string $locale
  265. *
  266. * @return static
  267. */
  268. public function setDefaultLocale($locale)
  269. {
  270. $this->validateLocale($locale);
  271. $this->defaultLocale = $locale;
  272. return $this;
  273. }
  274. /**
  275. * Gets the default locale
  276. *
  277. * @return string
  278. */
  279. public function getDefaultLocale()
  280. {
  281. return $this->defaultLocale;
  282. }
  283. /**
  284. * Get currently set global locale, used
  285. * extensively during query execution
  286. *
  287. * @return string
  288. */
  289. public function getListenerLocale()
  290. {
  291. return $this->locale;
  292. }
  293. /**
  294. * Gets the locale to use for translation. Loads object
  295. * defined locale first.
  296. *
  297. * @param object $object
  298. * @param ClassMetadata<object> $meta
  299. * @param object $om
  300. *
  301. * @throws RuntimeException if language or locale property is not found in entity
  302. *
  303. * @return string
  304. */
  305. public function getTranslatableLocale($object, $meta, $om = null)
  306. {
  307. $locale = $this->locale;
  308. $configurationLocale = self::$configurations[$this->name][$meta->getName()]['locale'] ?? null;
  309. if (null !== $configurationLocale) {
  310. $class = $meta->getReflectionClass();
  311. if (!$class->hasProperty($configurationLocale)) {
  312. throw new RuntimeException("There is no locale or language property ({$configurationLocale}) found on object: {$meta->getName()}");
  313. }
  314. $reflectionProperty = $class->getProperty($configurationLocale);
  315. $reflectionProperty->setAccessible(true);
  316. $value = $reflectionProperty->getValue($object);
  317. if (is_object($value) && method_exists($value, '__toString')) {
  318. $value = $value->__toString();
  319. }
  320. if ($this->isValidLocale($value)) {
  321. $locale = $value;
  322. }
  323. } elseif ($om instanceof DocumentManager) {
  324. [, $parentObject] = $om->getUnitOfWork()->getParentAssociation($object);
  325. if (null !== $parentObject) {
  326. $parentMeta = $om->getClassMetadata(get_class($parentObject));
  327. $locale = $this->getTranslatableLocale($parentObject, $parentMeta, $om);
  328. }
  329. }
  330. return $locale;
  331. }
  332. /**
  333. * Handle translation changes in default locale
  334. *
  335. * This has to be done in the preFlush because, when an entity has been loaded
  336. * in a different locale, no changes will be detected.
  337. *
  338. * @param ManagerEventArgs $args
  339. *
  340. * @phpstan-param ManagerEventArgs<ObjectManager> $args
  341. *
  342. * @return void
  343. */
  344. public function preFlush(EventArgs $args)
  345. {
  346. $ea = $this->getEventAdapter($args);
  347. $om = $ea->getObjectManager();
  348. $uow = $om->getUnitOfWork();
  349. foreach ($this->translationInDefaultLocale as $oid => $fields) {
  350. $trans = reset($fields);
  351. assert(false !== $trans);
  352. if ($ea->usesPersonalTranslation(get_class($trans))) {
  353. $entity = $trans->getObject();
  354. } else {
  355. $entity = $uow->tryGetById($trans->getForeignKey(), $trans->getObjectClass());
  356. }
  357. if (!$entity) {
  358. continue;
  359. }
  360. try {
  361. $uow->scheduleForUpdate($entity);
  362. } catch (ORMInvalidArgumentException $e) {
  363. foreach ($fields as $field => $trans) {
  364. $this->removeTranslationInDefaultLocale($oid, $field);
  365. }
  366. }
  367. }
  368. }
  369. /**
  370. * Looks for translatable objects being inserted or updated
  371. * for further processing
  372. *
  373. * @param ManagerEventArgs $args
  374. *
  375. * @phpstan-param ManagerEventArgs<ObjectManager> $args
  376. *
  377. * @return void
  378. */
  379. public function onFlush(EventArgs $args)
  380. {
  381. $ea = $this->getEventAdapter($args);
  382. $om = $ea->getObjectManager();
  383. $uow = $om->getUnitOfWork();
  384. // check all scheduled inserts for Translatable objects
  385. foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
  386. $meta = $om->getClassMetadata(get_class($object));
  387. $config = $this->getConfiguration($om, $meta->getName());
  388. if (isset($config['fields'])) {
  389. $this->handleTranslatableObjectUpdate($ea, $object, true);
  390. }
  391. }
  392. // check all scheduled updates for Translatable entities
  393. foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
  394. $meta = $om->getClassMetadata(get_class($object));
  395. $config = $this->getConfiguration($om, $meta->getName());
  396. if (isset($config['fields'])) {
  397. $this->handleTranslatableObjectUpdate($ea, $object, false);
  398. }
  399. }
  400. // check scheduled deletions for Translatable entities
  401. foreach ($ea->getScheduledObjectDeletions($uow) as $object) {
  402. $meta = $om->getClassMetadata(get_class($object));
  403. $config = $this->getConfiguration($om, $meta->getName());
  404. if (isset($config['fields'])) {
  405. $wrapped = AbstractWrapper::wrap($object, $om);
  406. $transClass = $this->getTranslationClass($ea, $meta->getName());
  407. \assert($wrapped instanceof AbstractWrapper);
  408. $ea->removeAssociatedTranslations($wrapped, $transClass, $config['useObjectClass']);
  409. }
  410. }
  411. }
  412. /**
  413. * Checks for inserted object to update their translation
  414. * foreign keys
  415. *
  416. * @param LifecycleEventArgs $args
  417. *
  418. * @phpstan-param LifecycleEventArgs<ObjectManager> $args
  419. *
  420. * @return void
  421. */
  422. public function postPersist(EventArgs $args)
  423. {
  424. $ea = $this->getEventAdapter($args);
  425. $om = $ea->getObjectManager();
  426. $object = $ea->getObject();
  427. $meta = $om->getClassMetadata(get_class($object));
  428. // check if entity is tracked by translatable and without foreign key
  429. if ($this->getConfiguration($om, $meta->getName()) && [] !== $this->pendingTranslationInserts) {
  430. $oid = spl_object_id($object);
  431. if (array_key_exists($oid, $this->pendingTranslationInserts)) {
  432. // load the pending translations without key
  433. $wrapped = AbstractWrapper::wrap($object, $om);
  434. $objectId = $wrapped->getIdentifier();
  435. $translationClass = $this->getTranslationClass($ea, get_class($object));
  436. foreach ($this->pendingTranslationInserts[$oid] as $translation) {
  437. if ($ea->usesPersonalTranslation($translationClass)) {
  438. $translation->setObject($objectId);
  439. } else {
  440. $translation->setForeignKey($objectId);
  441. }
  442. $ea->insertTranslationRecord($translation);
  443. }
  444. unset($this->pendingTranslationInserts[$oid]);
  445. }
  446. }
  447. }
  448. /**
  449. * After object is loaded, listener updates the translations
  450. * by currently used locale
  451. *
  452. * @param ManagerEventArgs $args
  453. *
  454. * @phpstan-param ManagerEventArgs<ObjectManager> $args
  455. *
  456. * @return void
  457. */
  458. public function postLoad(EventArgs $args)
  459. {
  460. $ea = $this->getEventAdapter($args);
  461. $om = $ea->getObjectManager();
  462. $object = $ea->getObject();
  463. $meta = $om->getClassMetadata(get_class($object));
  464. $config = $this->getConfiguration($om, $meta->getName());
  465. $locale = $this->defaultLocale;
  466. $oid = null;
  467. if (isset($config['fields'])) {
  468. $locale = $this->getTranslatableLocale($object, $meta, $om);
  469. $oid = spl_object_id($object);
  470. $this->translatedInLocale[$oid] = $locale;
  471. }
  472. if ($this->skipOnLoad) {
  473. return;
  474. }
  475. if (isset($config['fields']) && ($locale !== $this->defaultLocale || $this->persistDefaultLocaleTranslation)) {
  476. // fetch translations
  477. $translationClass = $this->getTranslationClass($ea, $config['useObjectClass']);
  478. $result = $ea->loadTranslations(
  479. $object,
  480. $translationClass,
  481. $locale,
  482. $config['useObjectClass']
  483. );
  484. // translate object's translatable properties
  485. foreach ($config['fields'] as $field) {
  486. $translated = $this->defaultTranslationValue;
  487. foreach ($result as $entry) {
  488. if ($entry['field'] == $field) {
  489. $translated = $entry['content'] ?? null;
  490. break;
  491. }
  492. }
  493. // update translation
  494. if ($this->defaultTranslationValue !== $translated
  495. || (!$this->translationFallback && (!isset($config['fallback'][$field]) || !$config['fallback'][$field]))
  496. || ($this->translationFallback && isset($config['fallback'][$field]) && !$config['fallback'][$field])
  497. ) {
  498. $ea->setTranslationValue($object, $field, $translated);
  499. // ensure clean changeset
  500. $ea->setOriginalObjectProperty(
  501. $om->getUnitOfWork(),
  502. $object,
  503. $field,
  504. $meta->getReflectionProperty($field)->getValue($object)
  505. );
  506. }
  507. }
  508. }
  509. }
  510. /**
  511. * Sets translation object which represents translation in default language.
  512. *
  513. * @param int $oid hash of basic entity
  514. * @param string $field field of basic entity
  515. * @param object|Translatable $trans Translation object
  516. *
  517. * @return void
  518. */
  519. public function setTranslationInDefaultLocale($oid, $field, $trans)
  520. {
  521. if (!isset($this->translationInDefaultLocale[$oid])) {
  522. $this->translationInDefaultLocale[$oid] = [];
  523. }
  524. $this->translationInDefaultLocale[$oid][$field] = $trans;
  525. }
  526. /**
  527. * @return bool
  528. */
  529. public function isSkipOnLoad()
  530. {
  531. return $this->skipOnLoad;
  532. }
  533. /**
  534. * Check if object has any translation object which represents translation in default language.
  535. * This is for internal use only.
  536. *
  537. * @param int $oid hash of the basic entity
  538. *
  539. * @return bool
  540. */
  541. public function hasTranslationsInDefaultLocale($oid)
  542. {
  543. return array_key_exists($oid, $this->translationInDefaultLocale);
  544. }
  545. protected function getNamespace()
  546. {
  547. return __NAMESPACE__;
  548. }
  549. /**
  550. * Validates the given locale
  551. *
  552. * @param string $locale locale to validate
  553. *
  554. * @throws InvalidArgumentException if locale is not valid
  555. *
  556. * @return void
  557. */
  558. protected function validateLocale($locale)
  559. {
  560. if (!$this->isValidLocale($locale)) {
  561. throw new InvalidArgumentException('Locale or language cannot be empty and must be set through Listener or Entity');
  562. }
  563. }
  564. /**
  565. * Check if the given locale is valid
  566. */
  567. private function isValidLocale(?string $locale): bool
  568. {
  569. return is_string($locale) && strlen($locale);
  570. }
  571. /**
  572. * Creates the translation for object being flushed
  573. *
  574. * @throws \UnexpectedValueException if locale is not valid, or
  575. * primary key is composite, missing or invalid
  576. */
  577. private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, object $object, bool $isInsert): void
  578. {
  579. $om = $ea->getObjectManager();
  580. $wrapped = AbstractWrapper::wrap($object, $om);
  581. $meta = $wrapped->getMetadata();
  582. $config = $this->getConfiguration($om, $meta->getName());
  583. // no need cache, metadata is loaded only once in MetadataFactoryClass
  584. $translationClass = $this->getTranslationClass($ea, $config['useObjectClass']);
  585. $translationMetadata = $om->getClassMetadata($translationClass);
  586. // check for the availability of the primary key
  587. $objectId = $wrapped->getIdentifier();
  588. // load the currently used locale
  589. $locale = $this->getTranslatableLocale($object, $meta, $om);
  590. $uow = $om->getUnitOfWork();
  591. $oid = spl_object_id($object);
  592. $changeSet = $ea->getObjectChangeSet($uow, $object);
  593. $translatableFields = $config['fields'];
  594. foreach ($translatableFields as $field) {
  595. $wasPersistedSeparetely = false;
  596. $skip = isset($this->translatedInLocale[$oid]) && $locale === $this->translatedInLocale[$oid];
  597. $skip = $skip && !isset($changeSet[$field]) && !$this->getTranslationInDefaultLocale($oid, $field);
  598. if ($skip) {
  599. continue; // locale is same and nothing changed
  600. }
  601. $translation = null;
  602. foreach ($ea->getScheduledObjectInsertions($uow) as $trans) {
  603. if ($locale !== $this->defaultLocale
  604. && get_class($trans) === $translationClass
  605. && $trans->getLocale() === $this->defaultLocale
  606. && $trans->getField() === $field
  607. && $this->belongsToObject($ea, $trans, $object)) {
  608. $this->setTranslationInDefaultLocale($oid, $field, $trans);
  609. break;
  610. }
  611. }
  612. // lookup persisted translations
  613. foreach ($ea->getScheduledObjectInsertions($uow) as $trans) {
  614. if (get_class($trans) !== $translationClass
  615. || $trans->getLocale() !== $locale
  616. || $trans->getField() !== $field) {
  617. continue;
  618. }
  619. if ($ea->usesPersonalTranslation($translationClass)) {
  620. $wasPersistedSeparetely = $trans->getObject() === $object;
  621. } else {
  622. $wasPersistedSeparetely = $trans->getObjectClass() === $config['useObjectClass']
  623. && $trans->getForeignKey() === $objectId;
  624. }
  625. if ($wasPersistedSeparetely) {
  626. $translation = $trans;
  627. break;
  628. }
  629. }
  630. // check if translation already is created
  631. if (!$isInsert && !$translation) {
  632. \assert($wrapped instanceof AbstractWrapper);
  633. $translation = $ea->findTranslation(
  634. $wrapped,
  635. $locale,
  636. $field,
  637. $translationClass,
  638. $config['useObjectClass']
  639. );
  640. }
  641. // create new translation if translation not already created and locale is different from default locale, otherwise, we have the date in the original record
  642. $persistNewTranslation = !$translation
  643. && ($locale !== $this->defaultLocale || $this->persistDefaultLocaleTranslation)
  644. ;
  645. if ($persistNewTranslation) {
  646. $translation = $translationMetadata->newInstance();
  647. $translation->setLocale($locale);
  648. $translation->setField($field);
  649. if ($ea->usesPersonalTranslation($translationClass)) {
  650. $translation->setObject($object);
  651. } else {
  652. $translation->setObjectClass($config['useObjectClass']);
  653. $translation->setForeignKey($objectId);
  654. }
  655. }
  656. if ($translation) {
  657. // set the translated field, take value using reflection
  658. $content = $ea->getTranslationValue($object, $field);
  659. $translation->setContent($content);
  660. // check if need to update in database
  661. $transWrapper = AbstractWrapper::wrap($translation, $om);
  662. if (((null === $content && !$isInsert) || is_bool($content) || is_int($content) || is_string($content) || !empty($content)) && ($isInsert || !$transWrapper->getIdentifier() || isset($changeSet[$field]))) {
  663. if ($isInsert && !$objectId && !$ea->usesPersonalTranslation($translationClass)) {
  664. // if we do not have the primary key yet available
  665. // keep this translation in memory to insert it later with foreign key
  666. $this->pendingTranslationInserts[spl_object_id($object)][] = $translation;
  667. } else {
  668. // persist and compute change set for translation
  669. if ($wasPersistedSeparetely) {
  670. $ea->recomputeSingleObjectChangeset($uow, $translationMetadata, $translation);
  671. } else {
  672. $om->persist($translation);
  673. $uow->computeChangeSet($translationMetadata, $translation);
  674. }
  675. }
  676. }
  677. }
  678. if ($isInsert && null !== $this->getTranslationInDefaultLocale($oid, $field)) {
  679. // We can't rely on object field value which is created in non-default locale.
  680. // If we provide translation for default locale as well, the latter is considered to be trusted
  681. // and object content should be overridden.
  682. $wrapped->setPropertyValue($field, $this->getTranslationInDefaultLocale($oid, $field)->getContent());
  683. $ea->recomputeSingleObjectChangeset($uow, $meta, $object);
  684. $this->removeTranslationInDefaultLocale($oid, $field);
  685. }
  686. }
  687. $this->translatedInLocale[$oid] = $locale;
  688. // check if we have default translation and need to reset the translation
  689. if (!$isInsert && strlen($this->defaultLocale)) {
  690. $this->validateLocale($this->defaultLocale);
  691. $modifiedChangeSet = $changeSet;
  692. foreach ($changeSet as $field => $changes) {
  693. if (in_array($field, $translatableFields, true)) {
  694. if ($locale !== $this->defaultLocale) {
  695. $ea->setOriginalObjectProperty($uow, $object, $field, $changes[0]);
  696. unset($modifiedChangeSet[$field]);
  697. }
  698. }
  699. }
  700. $ea->recomputeSingleObjectChangeset($uow, $meta, $object);
  701. // cleanup current changeset only if working in a another locale different than de default one, otherwise the changeset will always be reverted
  702. if ($locale !== $this->defaultLocale) {
  703. $ea->clearObjectChangeSet($uow, $object);
  704. // recompute changeset only if there are changes other than reverted translations
  705. if ($modifiedChangeSet || $this->hasTranslationsInDefaultLocale($oid)) {
  706. foreach ($modifiedChangeSet as $field => $changes) {
  707. $ea->setOriginalObjectProperty($uow, $object, $field, $changes[0]);
  708. }
  709. foreach ($translatableFields as $field) {
  710. if (null !== $this->getTranslationInDefaultLocale($oid, $field)) {
  711. $wrapped->setPropertyValue($field, $this->getTranslationInDefaultLocale($oid, $field)->getContent());
  712. $this->removeTranslationInDefaultLocale($oid, $field);
  713. }
  714. }
  715. $ea->recomputeSingleObjectChangeset($uow, $meta, $object);
  716. }
  717. }
  718. }
  719. }
  720. /**
  721. * Removes translation object which represents translation in default language.
  722. * This is for internal use only.
  723. *
  724. * @param int $oid hash of the basic entity
  725. * @param string $field field of basic entity
  726. */
  727. private function removeTranslationInDefaultLocale(int $oid, string $field): void
  728. {
  729. if (isset($this->translationInDefaultLocale[$oid])) {
  730. if (isset($this->translationInDefaultLocale[$oid][$field])) {
  731. unset($this->translationInDefaultLocale[$oid][$field]);
  732. }
  733. if (!$this->translationInDefaultLocale[$oid]) {
  734. // We removed the final remaining elements from the
  735. // translationInDefaultLocale[$oid] array, so we might as well
  736. // completely remove the entry at $oid.
  737. unset($this->translationInDefaultLocale[$oid]);
  738. }
  739. }
  740. }
  741. /**
  742. * Gets translation object which represents translation in default language.
  743. * This is for internal use only.
  744. *
  745. * @param int $oid hash of the basic entity
  746. * @param string $field field of basic entity
  747. *
  748. * @return object|Translatable|null Returns translation object if it exists or NULL otherwise
  749. */
  750. private function getTranslationInDefaultLocale(int $oid, string $field)
  751. {
  752. return $this->translationInDefaultLocale[$oid][$field] ?? null;
  753. }
  754. /**
  755. * Checks if the translation entity belongs to the object in question
  756. */
  757. private function belongsToObject(TranslatableAdapter $ea, object $trans, object $object): bool
  758. {
  759. if ($ea->usesPersonalTranslation(get_class($trans))) {
  760. return $trans->getObject() === $object;
  761. }
  762. return $trans->getForeignKey() === $object->getId()
  763. && ($trans->getObjectClass() === get_class($object));
  764. }
  765. }