Преглед на файлове

[translatable] single listener through event adapter

gediminasm преди 14 години
родител
ревизия
c809a2509a

+ 13 - 13
lib/Gedmo/Translatable/Document/Repository/TranslationRepository.php

@@ -8,7 +8,7 @@ use Doctrine\ODM\MongoDB\DocumentRepository,
 /**
  * The TranslationRepository has some useful functions
  * to interact with translations.
- * 
+ *
  * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  * @package Gedmo.Translatable.Document.Repository
  * @subpackage TranslationRepository
@@ -16,11 +16,11 @@ use Doctrine\ODM\MongoDB\DocumentRepository,
  * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  */
 class TranslationRepository extends DocumentRepository
-{    
+{
     /**
      * Loads all translations with all translatable
      * fields from the given entity
-     * 
+     *
      * @param object $document
      * @return array list of translations in locale groups
      */
@@ -31,19 +31,19 @@ class TranslationRepository extends DocumentRepository
             $meta = $this->dm->getClassMetadata(get_class($document));
             $identifier = $meta->identifier;
             $documentId = $meta->getReflectionProperty($identifier)->getValue($document);
-            
+
             $translationMeta = $this->getClassMetadata();
             $qb = $this->createQueryBuilder();
             $q = $qb->field('foreignKey')->equals($documentId)
                 ->field('objectClass')->equals($meta->name)
                 ->sort('locale', 'asc')
                 ->getQuery();
-            
+
             $q->setHydrate(false);
             $data = $q->execute();
             if ($data instanceof Cursor) {
                 $data = $data->toArray();
-            }            
+            }
             if ($data && is_array($data) && count($data)) {
                 foreach ($data as $row) {
                     $result[$row['locale']][$row['field']] = $row['content'];
@@ -52,13 +52,13 @@ class TranslationRepository extends DocumentRepository
         }
         return $result;
     }
-    
+
     /**
      * Find the object $class by the translated field.
      * Result is the first occurence of translated field.
      * Query can be slow, since there are no indexes on such
      * columns
-     * 
+     *
      * @param string $field
      * @param string $value
      * @param string $class
@@ -75,7 +75,7 @@ class TranslationRepository extends DocumentRepository
                 ->field('objectClass')->equals($meta->name)
                 ->field('content')->equals($value)
                 ->getQuery();
-            
+
             $q->setHydrate(false);
             $result = $q->execute();
             if ($result instanceof Cursor) {
@@ -88,7 +88,7 @@ class TranslationRepository extends DocumentRepository
         }
         return $entity;
     }
-    
+
     /**
      * Loads all translations with all translatable
      * fields by a given document primary key
@@ -105,13 +105,13 @@ class TranslationRepository extends DocumentRepository
             $q = $qb->field('foreignKey')->equals($id)
                 ->sort('locale', 'asc')
                 ->getQuery();
-            
+
             $q->setHydrate(false);
             $data = $q->execute();
-                       
+
             if ($data instanceof Cursor) {
                 $data = $data->toArray();
-            }            
+            }
             if ($data && is_array($data) && count($data)) {
                 foreach ($data as $row) {
                     $result[$row['locale']][$row['field']] = $row['content'];

+ 130 - 0
lib/Gedmo/Translatable/Mapping/Event/Adapter/ODM.php

@@ -0,0 +1,130 @@
+<?php
+
+namespace Gedmo\Translatable\Mapping\Event\Adapter;
+
+use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
+use Doctrine\ODM\MongoDB\DocumentManager;
+use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
+use Doctrine\ODM\MongoDB\Cursor;
+
+/**
+ * Doctrine event adapter for ODM adapted
+ * for Translatable behavior
+ *
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo\Translatable\Mapping\Event\Adapter
+ * @subpackage ODM
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+final class ODM extends BaseAdapterODM
+{
+    /**
+     * Get default LogEntry class used to store the logs
+     *
+     * @return string
+     */
+    public function getDefaultTranslationClass()
+    {
+        return 'Gedmo\\Translatable\\Document\\Translation';
+    }
+
+    /**
+     * Load the translations for a given object
+     *
+     * @param object $object
+     * @param string $translationClass
+     * @param string $locale
+     * @return array
+     */
+    public function loadTranslations($object, $translationClass, $locale)
+    {
+        $dm = $this->getObjectManager();
+        $meta = $dm->getClassMetadata(get_class($object));
+
+        // load translated content for all translatable fields
+        $identifier = $this->extractIdentifier($dm, $object);
+        // construct query
+        $qb = $dm->createQueryBuilder($translationClass);
+        $q = $qb->field('foreignKey')->equals($identifier)
+            ->field('locale')->equals($locale)
+            ->field('objectClass')->equals($meta->name)
+            ->getQuery();
+
+        $q->setHydrate(false);
+        $result = $q->execute();
+        if ($result instanceof Cursor) {
+            $result = $result->toArray();
+        }
+        return $result;
+    }
+
+    /**
+     * Search for existing translation record
+     *
+     * @param mixed $objectId
+     * @param string $objectClass
+     * @param string $locale
+     * @param string $field
+     * @param string $translationClass
+     * @return mixed - null if nothing is found, Translation otherwise
+     */
+    public function findTranslation($objectId, $objectClass, $locale, $field, $translationClass)
+    {
+        $dm = $this->getObjectManager();
+        $qb = $dm->createQueryBuilder($translationClass);
+        $q = $qb->field('foreignKey')->equals($objectId)
+            ->field('locale')->equals($locale)
+            ->field('field')->equals($field)
+            ->field('objectClass')->equals($objectClass)
+            ->getQuery();
+
+        $result = $q->execute();
+        if ($result instanceof Cursor) {
+            $result = current($result->toArray());
+        }
+        $q->setHydrate(false);
+        return $result;
+    }
+
+    /**
+     * Removes all associated translations for given object
+     *
+     * @param mixed $objectId
+     * @param string $transClass
+     * @return void
+     */
+    public function removeAssociatedTranslations($objectId, $transClass)
+    {
+        $dm = $this->getObjectManager();
+        $qb = $dm->createQueryBuilder($transClass);
+        $q = $qb->remove()
+            ->field('foreignKey')->equals($objectId)
+            ->getQuery();
+        return $q->execute();
+    }
+
+    /**
+     * Inserts the translation record
+     *
+     * @param object $translation
+     * @return void
+     */
+    public function insertTranslationRecord($translation)
+    {
+        $dm = $this->getObjectManager();
+        $meta = $dm->getClassMetadata(get_class($translation));
+        $collection = $dm->getDocumentCollection($meta->name);
+        $data = array();
+
+        foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) {
+            if (!$meta->isIdentifier($fieldName)) {
+                $data[$meta->fieldMappings[$fieldName]['name']] = $reflProp->getValue($translation);
+            }
+        }
+
+        if (!$collection->insert($data)) {
+            throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record');
+        }
+    }
+}

+ 133 - 0
lib/Gedmo/Translatable/Mapping/Event/Adapter/ORM.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace Gedmo\Translatable\Mapping\Event\Adapter;
+
+use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\Mapping\ClassMetadataInfo;
+use Doctrine\ORM\Query;
+
+/**
+ * Doctrine event adapter for ORM adapted
+ * for Translatable behavior
+ *
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo\Translatable\Mapping\Event\Adapter
+ * @subpackage ORM
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+final class ORM extends BaseAdapterORM
+{
+    /**
+     * Get default LogEntry class used to store the logs
+     *
+     * @return string
+     */
+    public function getDefaultTranslationClass()
+    {
+        return 'Gedmo\\Translatable\\Entity\\Translation';
+    }
+
+    /**
+     * Load the translations for a given object
+     *
+     * @param object $object
+     * @param string $translationClass
+     * @param string $locale
+     * @return array
+     */
+    public function loadTranslations($object, $translationClass, $locale)
+    {
+        $em = $this->getObjectManager();
+        $meta = $em->getClassMetadata(get_class($object));
+        // load translated content for all translatable fields
+        $objectId = $this->extractIdentifier($em, $object);
+        // construct query
+        $dql = 'SELECT t.content, t.field FROM ' . $translationClass . ' t';
+        $dql .= ' WHERE t.foreignKey = :objectId';
+        $dql .= ' AND t.locale = :locale';
+        $dql .= ' AND t.objectClass = :objectClass';
+        // fetch results
+        $objectClass = $meta->name;
+        $q = $em->createQuery($dql);
+        $q->setParameters(compact('objectId', 'locale', 'objectClass'));
+        return $q->getArrayResult();
+    }
+
+    /**
+     * Search for existing translation record
+     *
+     * @param mixed $objectId
+     * @param string $objectClass
+     * @param string $locale
+     * @param string $field
+     * @param string $translationClass
+     * @return mixed - null if nothing is found, Translation otherwise
+     */
+    public function findTranslation($objectId, $objectClass, $locale, $field, $translationClass)
+    {
+        $em = $this->getObjectManager();
+        $qb = $em->createQueryBuilder();
+        $qb->select('trans')
+            ->from($translationClass, 'trans')
+            ->where(
+                'trans.foreignKey = :objectId',
+                'trans.locale = :locale',
+                'trans.field = :field',
+                'trans.objectClass = :objectClass'
+            );
+        $q = $qb->getQuery();
+        $result = $q->execute(
+            compact('field', 'locale', 'objectId', 'objectClass'),
+            Query::HYDRATE_OBJECT
+        );
+
+        if ($result) {
+            return array_shift($result);
+        }
+        return null;
+    }
+
+    /**
+     * Removes all associated translations for given object
+     *
+     * @param mixed $objectId
+     * @param string $transClass
+     * @return void
+     */
+    public function removeAssociatedTranslations($objectId, $transClass)
+    {
+        $em = $this->getObjectManager();
+        $dql = 'DELETE ' . $transClass . ' trans';
+        $dql .= ' WHERE trans.foreignKey = :objectId';
+
+        $q = $em->createQuery($dql);
+        $q->setParameters(compact('objectId'));
+        return $q->getSingleScalarResult();
+    }
+
+    /**
+     * Inserts the translation record
+     *
+     * @param object $translation
+     * @return void
+     */
+    public function insertTranslationRecord($translation)
+    {
+        $em = $this->getObjectManager();
+        $meta = $em->getClassMetadata(get_class($translation));
+        $data = array();
+
+        foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) {
+            if (!$meta->isIdentifier($fieldName)) {
+                $data[$meta->getColumnName($fieldName)] = $reflProp->getValue($translation);
+            }
+        }
+
+        $table = $meta->getTableName();
+        if (!$em->getConnection()->insert($table, $data)) {
+            throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record');
+        }
+    }
+}

+ 334 - 132
lib/Gedmo/Translatable/TranslationListener.php

@@ -2,216 +2,418 @@
 
 namespace Gedmo\Translatable;
 
-use Doctrine\ORM\Events,
-    Doctrine\Common\EventArgs,
+use Doctrine\Common\EventArgs,
     Doctrine\Common\Persistence\ObjectManager,
     Doctrine\Common\Persistence\Mapping\ClassMetadata,
-    Doctrine\ORM\Query;
+    Gedmo\Mapping\MappedEventSubscriber,
+    Gedmo\Mapping\Event\AdapterInterface;
 
 /**
  * The translation listener handles the generation and
  * loading of translations for entities which implements
  * the Translatable interface.
- * 
+ *
  * This behavior can inpact the performance of your application
  * since it does an additional query for each field to translate.
- * 
+ *
  * Nevertheless the annotation metadata is properly cached and
  * it is not a big overhead to lookup all entity annotations since
  * the caching is activated for metadata
- * 
+ *
  * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  * @package Gedmo.Translatable
  * @subpackage TranslationListener
  * @link http://www.gediminasm.org
  * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  */
-class TranslationListener extends AbstractTranslationListener
-{    
+class TranslationListener extends MappedEventSubscriber
+{
     /**
-     * The translation entity class used to store the translations
-     * 
+     * Locale which is set on this listener.
+     * If Entity being translated has locale defined it
+     * will override this one
+     *
      * @var string
      */
-    protected $_defaultTranslationEntity = 'Gedmo\Translatable\Entity\Translation';
-    
+    protected $locale = 'en_us';
+
     /**
-     * Specifies the list of events to listen
-     * 
-     * @return array
+     * Default locale, this changes behavior
+     * to not update the original record field if locale
+     * which is used for updating is not default. This
+     * will load the default translation in other locales
+     * if record is not translated yet
+     *
+     * @var string
      */
-    public function getSubscribedEvents()
-    {
-        return array(
-            Events::postLoad,
-            Events::postPersist,
-            Events::onFlush,
-            Events::loadClassMetadata
-        );
-    }
-    
+    private $defaultLocale = '';
+
     /**
-     * {@inheritdoc}
+     * If this is set to false, when if entity does
+     * not have a translation for requested locale
+     * it will show a blank value
+     *
+     * @var boolean
      */
-    protected function getDefaultTranslationClass()
-    {
-        return $this->_defaultTranslationEntity;
-    }
-    
+    private $translationFallback = true;
+
     /**
-     * {@inheritdoc}
+     * List of translations which do not have the foreign
+     * key generated yet - MySQL case. These translations
+     * will be updated with new keys on postPersist event
+     *
+     * @var array
      */
-    protected function getObjectManager(EventArgs $args)
-    {
-        return $args->getEntityManager();
-    }
-    
+    private $pendingTranslationInserts = array();
+
     /**
-     * {@inheritdoc}
+     * Specifies the list of events to listen
+     *
+     * @return array
      */
-    protected function getObject(EventArgs $args)
+    public function getSubscribedEvents()
     {
-        return $args->getEntity();
+        return array(
+            'postLoad',
+            'postPersist',
+            'onFlush',
+            'loadClassMetadata'
+        );
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Mapps additional metadata
+     *
+     * @param EventArgs $eventArgs
+     * @return void
      */
-    protected function getObjectChangeSet($uow, $object)
+    public function loadClassMetadata(EventArgs $eventArgs)
     {
-        return $uow->getEntityChangeSet($object);
+        $ea = $this->getEventAdapter($eventArgs);
+        $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Enable or disable translation fallback
+     * to original record value
+     *
+     * @param boolean $bool
+     * @return void
      */
-    protected function getScheduledObjectUpdates($uow)
+    public function setTranslationFallback($bool)
     {
-        return $uow->getScheduledEntityUpdates();
+        $this->translationFallback = (bool)$bool;
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Get the translation class to be used
+     * for the object $class
+     *
+     * @param AdapterInterface $ea
+     * @param string $class
+     * @return string
      */
-    protected function getScheduledObjectInsertions($uow)
+    public function getTranslationClass(AdapterInterface $ea, $class)
     {
-        return $uow->getScheduledEntityInsertions();
+        return isset($this->configurations[$class]['translationClass']) ?
+            $this->configurations[$class]['translationClass'] :
+            $ea->getDefaultTranslationClass();
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Set the locale to use for translation listener
+     *
+     * @param string $locale
+     * @return void
      */
-    protected function getScheduledObjectDeletions($uow)
+    public function setTranslatableLocale($locale)
     {
-        return $uow->getScheduledEntityDeletions();
+        $this->locale = $locale;
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Gets the locale to use for translation. Loads object
+     * defined locale first..
+     *
+     * @param object $object
+     * @param ClassMetadata $meta
+     * @throws RuntimeException - if language or locale property is not
+     *         found in entity
+     * @return string
      */
-    protected function getSingleIdentifierFieldName(ClassMetadata $meta)
+    public function getTranslatableLocale($object, ClassMetadata $meta)
     {
-        return $meta->getSingleIdentifierFieldName();
+        $locale = $this->locale;
+        if (isset($this->configurations[$meta->name]['locale'])) {
+            $class = $meta->getReflectionClass();
+            $reflectionProperty = $class->getProperty($this->configurations[$meta->name]['locale']);
+            if (!$reflectionProperty) {
+                $column = $this->configurations[$meta->name]['locale'];
+                throw new \Gedmo\Exception\RuntimeException("There is no locale or language property ({$column}) found on object: {$meta->name}");
+            }
+            $reflectionProperty->setAccessible(true);
+            $value = $reflectionProperty->getValue($object);
+            if (is_string($value) && strlen($value)) {
+                $locale = $value;
+            }
+        }
+        $this->validateLocale($locale);
+        return strtolower($locale);
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Looks for translatable objects being inserted or updated
+     * for further processing
+     *
+     * @param EventArgs $args
+     * @return void
      */
-    protected function removeAssociatedTranslations(ObjectManager $om, $objectId, $transClass)
+    public function onFlush(EventArgs $args)
     {
-        $dql = 'DELETE ' . $transClass . ' trans';
-        $dql .= ' WHERE trans.foreignKey = :objectId';
-            
-        $q = $om->createQuery($dql);
-        $q->setParameters(compact('objectId'));
-        return $q->getSingleScalarResult();
+        $ea = $this->getEventAdapter($args);
+        $om = $ea->getObjectManager();
+        $uow = $om->getUnitOfWork();
+        // check all scheduled inserts for Translatable objects
+        foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
+            $meta = $om->getClassMetadata(get_class($object));
+            $config = $this->getConfiguration($om, $meta->name);
+            if (isset($config['fields'])) {
+                $this->handleTranslatableObjectUpdate($ea, $object, true);
+            }
+        }
+        // check all scheduled updates for Translatable entities
+        foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
+            $meta = $om->getClassMetadata(get_class($object));
+            $config = $this->getConfiguration($om, $meta->name);
+            if (isset($config['fields'])) {
+                // check if there are translation changes
+                $changeSet = $ea->getObjectChangeSet($uow, $object);
+                foreach ($config['fields'] as $field) {
+                    if (array_key_exists($field, $changeSet)) {
+                        // needs handling
+                        $this->handleTranslatableObjectUpdate($ea, $object, false);
+                        break;
+                    }
+                }
+            }
+        }
+        // check scheduled deletions for Translatable entities
+        foreach ($ea->getScheduledObjectDeletions($uow) as $object) {
+            $meta = $om->getClassMetadata(get_class($object));
+            $config = $this->getConfiguration($om, $meta->name);
+            if (isset($config['fields'])) {
+                $identifierField = $ea->getSingleIdentifierFieldName($meta);
+                $objectId = $meta->getReflectionProperty($identifierField)->getValue($object);
+
+                $transClass = $this->getTranslationClass($ea, $meta->name);
+                $ea->removeAssociatedTranslations($objectId, $transClass);
+            }
+        }
     }
-    
-    /**
-     * {@inheritdoc}
+
+     /**
+     * Checks for inserted object to update their translation
+     * foreign keys
+     *
+     * @param EventArgs $args
+     * @return void
      */
-    protected function insertTranslationRecord(ObjectManager $om, $translation)
+    public function postPersist(EventArgs $args)
     {
-        $meta = $om->getClassMetadata(get_class($translation));        
-        $data = array();
+        $ea = $this->getEventAdapter($args);
+        $om = $ea->getObjectManager();
+        $object = $ea->getObject();
+        $uow = $om->getUnitOfWork();
+        $meta = $om->getClassMetadata(get_class($object));
+        // check if entity is tracked by translatable and without foreign key
+        if (array_key_exists($meta->name, $this->configurations) && count($this->pendingTranslationInserts)) {
+            $oid = spl_object_hash($object);
 
-        foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) {
-            if (!$meta->isIdentifier($fieldName)) {
-                $data[$meta->getColumnName($fieldName)] = $reflProp->getValue($translation);
+            // there should be single identifier
+            $identifierField = $ea->getSingleIdentifierFieldName($meta);
+            $translationMeta = $om->getClassMetadata($this->getTranslationClass($ea, $meta->name));
+            if (array_key_exists($oid, $this->pendingTranslationInserts)) {
+                // load the pending translations without key
+                $translations = $this->pendingTranslationInserts[$oid];
+                foreach ($translations as $translation) {
+                    $translationMeta->getReflectionProperty('foreignKey')->setValue(
+                        $translation,
+                        $meta->getReflectionProperty($identifierField)->getValue($object)
+                    );
+                    $ea->insertTranslationRecord($translation);
+                }
+                unset($this->pendingTranslationInserts[$oid]);
             }
         }
-        
-        $table = $meta->getTableName();
-        if (!$om->getConnection()->insert($table, $data)) {
-            throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record');
-        }
     }
-    
+
     /**
-     * {@inheritdoc}
+     * After object is loaded, listener updates the translations
+     * by currently used locale
+     *
+     * @param EventArgs $args
+     * @return void
      */
-    protected function findTranslation(ObjectManager $om, $objectId, $objectClass, $locale, $field)
+    public function postLoad(EventArgs $args)
     {
-        $qb = $om->createQueryBuilder();
-        $qb->select('trans')
-            ->from($this->getTranslationClass($objectClass), 'trans')
-            ->where(
-                'trans.foreignKey = :objectId',
-                'trans.locale = :locale',
-                'trans.field = :field',
-                'trans.objectClass = :objectClass'
+        $ea = $this->getEventAdapter($args);
+        $om = $ea->getObjectManager();
+        $object = $ea->getObject();
+        $meta = $om->getClassMetadata(get_class($object));
+        $config = $this->getConfiguration($om, $meta->name);
+
+        if (isset($config['fields'])) {
+            // fetch translations
+            $result = $ea->loadTranslations(
+                $object,
+                $this->getTranslationClass($ea, $meta->name),
+                $this->getTranslatableLocale($object, $meta)
             );
-        $q = $qb->getQuery();
-        $result = $q->execute(
-            compact('field', 'locale', 'objectId', 'objectClass'),
-            Query::HYDRATE_OBJECT
-        );
-        
-        if ($result) {
-            return array_shift($result);
+            // translate object's translatable properties
+            foreach ($config['fields'] as $field) {
+                $translated = '';
+                foreach ((array)$result as $entry) {
+                    if ($entry['field'] == $field) {
+                        $translated = $entry['content'];
+                        break;
+                    }
+                }
+                // update translation
+                if (strlen($translated) || !$this->translationFallback) {
+                    $meta->getReflectionProperty($field)->setValue($object, $translated);
+                    // ensure clean changeset
+                    $ea->setOriginalObjectProperty(
+                        $om->getUnitOfWork(),
+                        spl_object_hash($object),
+                        $field,
+                        $translated
+                    );
+                }
+            }
         }
-        return null;
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Sets the default locale, this changes behavior
+     * to not update the original record field if locale
+     * which is used for updating is not default
+     *
+     * @param string $locale
+     * @return void
      */
-    protected function setOriginalObjectProperty($uow, $oid, $property, $value)
+    public function setDefaultLocale($locale)
     {
-        $uow->setOriginalEntityProperty($oid, $property, $value);
+        $this->defaultLocale = $locale;
     }
-    
+
     /**
-     * {@inheritdoc}
+     * {@inheritDoc}
      */
-    protected function clearObjectChangeSet($uow, $oid)
+    protected function getNamespace()
     {
-        $uow->clearEntityChangeSet($oid);
+        return __NAMESPACE__;
     }
-    
+
     /**
-     * {@inheritdoc}
+     * Creates the translation for object being flushed
+     *
+     * @param AdapterInterface $ea
+     * @param object $object
+     * @param boolean $isInsert
+     * @throws UnexpectedValueException - if locale is not valid, or
+     *      primary key is composite, missing or invalid
+     * @return void
      */
-    protected function loadTranslations(ObjectManager $om, $object)
+    protected function handleTranslatableObjectUpdate(AdapterInterface $ea, $object, $isInsert)
     {
+        $om = $ea->getObjectManager();
         $meta = $om->getClassMetadata(get_class($object));
-        $locale = strtolower($this->getTranslatableLocale($object, $meta));
-        $this->validateLocale($locale);
-        
-        // there should be single identifier
-        $identifierField = $this->getSingleIdentifierFieldName($meta);
-        // load translated content for all translatable fields
-        $translationClass = $this->getTranslationClass($meta->name);
-        $objectId = $meta->getReflectionProperty($identifierField)->getValue($object);
-        // construct query
-        $dql = 'SELECT t.content, t.field FROM ' . $translationClass . ' t';
-        $dql .= ' WHERE t.foreignKey = :objectId';
-        $dql .= ' AND t.locale = :locale';
-        $dql .= ' AND t.objectClass = :objectClass';
-        // fetch results
-        $objectClass = $meta->name;
-        $q = $om->createQuery($dql);
-        $q->setParameters(compact('objectId', 'locale', 'objectClass'));
-        return $q->getArrayResult();
+        // no need cache, metadata is loaded only once in MetadataFactoryClass
+        $translationClass = $this->getTranslationClass($ea, $meta->name);
+        $translationMetadata = $om->getClassMetadata($translationClass);
+
+        // check for the availability of the primary key
+        $objectId = $ea->extractIdentifier($om, $object);
+        if (!$object && $isInsert) {
+            $objectId = null;
+        }
+
+        // load the currently used locale
+        $locale = $this->getTranslatableLocale($object, $meta);
+
+        $uow = $om->getUnitOfWork();
+        $config = $this->getConfiguration($om, $meta->name);
+        $translatableFields = $config['fields'];
+        foreach ($translatableFields as $field) {
+            $translation = null;
+            // check if translation allready is created
+            if (!$isInsert) {
+                $translation = $ea->findTranslation(
+                    $objectId,
+                    $meta->name,
+                    $locale,
+                    $field,
+                    $translationClass
+                );
+            }
+            // create new translation
+            if (!$translation) {
+                $translation = new $translationClass();
+                $translation->setLocale($locale);
+                $translation->setField($field);
+                $translation->setObjectClass($meta->name);
+                $translation->setForeignKey($objectId);
+                $scheduleUpdate = !$isInsert;
+            }
+
+            // set the translated field, take value using reflection
+            $translation->setContent($meta->getReflectionProperty($field)->getValue($object));
+            if ($isInsert && is_null($objectId)) {
+                // if we do not have the primary key yet available
+                // keep this translation in memory to insert it later with foreign key
+                $this->pendingTranslationInserts[spl_object_hash($object)][$field] = $translation;
+            } else {
+                // persist and compute change set for translation
+                $om->persist($translation);
+                $uow->computeChangeSet($translationMetadata, $translation);
+            }
+        }
+        // check if we have default translation and need to reset the translation
+        if (!$isInsert && strlen($this->defaultLocale)) {
+            $this->validateLocale($this->defaultLocale);
+            $changeSet = $modifiedChangeSet = $ea->getObjectChangeSet($uow, $object);
+            foreach ($changeSet as $field => $changes) {
+                if (in_array($field, $translatableFields)) {
+                    if ($locale != $this->defaultLocale && strlen($changes[0])) {
+                        $meta->getReflectionProperty($field)->setValue($object, $changes[0]);
+                        $ea->setOriginalObjectProperty($uow, spl_object_hash($object), $field, $changes[0]);
+                        unset($modifiedChangeSet[$field]);
+                    }
+                }
+            }
+            // cleanup current changeset
+            $ea->clearObjectChangeSet($uow, spl_object_hash($object));
+            // recompute changeset only if there are changes other than reverted translations
+            if ($modifiedChangeSet) {
+                foreach ($modifiedChangeSet as $field => $changes) {
+                    $ea->setOriginalObjectProperty($uow, spl_object_hash($object), $field, $changes[0]);
+                }
+                $uow->computeChangeSet($meta, $object);
+            }
+        }
+    }
+
+    /**
+     * Validates the given locale
+     *
+     * @param string $locale - locale to validate
+     * @throws InvalidArgumentException if locale is not valid
+     * @return void
+     */
+    protected function validateLocale($locale)
+    {
+        if (!is_string($locale) || !strlen($locale)) {
+            throw new \Gedmo\Exception\InvalidArgumentException('Locale or language cannot be empty and must be set through Listener or Entity');
+        }
     }
 }

+ 2 - 2
tests/Gedmo/Translatable/TranslatableDocumentTest.php

@@ -3,7 +3,7 @@
 namespace Gedmo\Translatable;
 
 use Tool\BaseTestCaseMongoODM;
-use Gedmo\Sluggable\ODM\MongoDB\SluggableListener;
+use Gedmo\Sluggable\SluggableListener;
 use Doctrine\Common\EventManager;
 use Translatable\Fixture\Document\Article;
 
@@ -27,7 +27,7 @@ class TranslatableDocumentTest extends BaseTestCaseMongoODM
     {
         parent::setUp();
         $evm = new EventManager();
-        $this->translationListener = new ODM\MongoDB\TranslationListener;
+        $this->translationListener = new TranslationListener;
         $this->translationListener->setTranslatableLocale('en_us');
         $evm->addEventSubscriber(new SluggableListener);
         $evm->addEventSubscriber($this->translationListener);