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

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$objecttrue);
  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$objectfalse);
  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($eaget_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 $eaobject $objectbool $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$translatableFieldstrue)) {
  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 $oidstring $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 $oidstring $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 $eaobject $transobject $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. }