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

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