فهرست منبع

[translatable] multiple translation persistence through repository and tests, references #29

Gediminas Morkevicius 14 سال پیش
والد
کامیت
0fa017f64c

+ 38 - 2
doc/translatable.md

@@ -1,4 +1,4 @@
-<h1>Translatable behavior extension for Doctrine 2</h1>
+# Translatable behavior extension for Doctrine 2
 
 **Translatable** behavior offers a very handy solution for translating specific record fields
 in diferent languages. Further more, it loads the translations automatically for a locale
@@ -16,7 +16,12 @@ Document fields then loaded
 
 [blog_test]: http://gediminasm.org/test "Test extensions on this blog"
 
+Update **2011-04-21**
+
+- Implemented multiple translation persistense through repository
+
 Update **2011-04-16**
+
 - Made an ORM query **hint** to hook into any select type query, which will join the translations
 and let you **filter, order or search** by translated fields directly. It also will translate
 all selected **collections or simple components** without issuing additional queries. It also
@@ -24,6 +29,7 @@ supports translation fallbacks
 - For performance reasons, translation fallbacks are disabled by default
 
 Update **2011-04-04**
+
 - Made single listener, one instance can be used for any object manager
 and any number of them
 
@@ -33,7 +39,7 @@ and any number of them
 - Public [Translatable repository](http://github.com/l3pp4rd/DoctrineExtensions "Translatable extension on Github") is available on github
 - Using other extensions on the same Entity fields may result in unexpected way
 - May inpact your application performace since it does an additional query for translation
-- Last update date: **2011-04-16**
+- Last update date: **2011-04-21**
 
 **Portability:**
 
@@ -50,6 +56,7 @@ Content:
 - Document [example](#document)
 - [Yaml](#yaml) mapping example
 - Basic usage [examples](#basic-examples)
+- [Persisting](#multi-translations) multiple translations
 - Using ORM query [hint](#orm-query-hint)
 - Advanced usage [examples](#advanced-examples)
 
@@ -337,6 +344,35 @@ Lets try to load it and it should be translated in English
     echo $article->getContent();
     // prints: "my content in en"
 
+## Persisting multiple translations {#multi-translations}
+
+Usually it is more convinient to persist more translations when creating
+or updating a record. **Translatable** allows to do that through translation repository.
+All additional translations will be tracked by listener and when the flush will be executed,
+it will update or persist all additional translations.
+
+**Notice:** these translations will not be processed as ordinary fields of your object,
+in case if you translate a **slug** additional translation will not know how to generate
+the slug, so the value as an additional translation should be processed when creating it.
+
+### Example of multiple translations:
+
+    // persisting multiple translations, assume default locale is EN
+    $repo = $em->getRepository('Gedmo\\Translatable\\Entity\\Translation');
+    // it works for ODM also
+    $article = new Article;
+    $article->setTitle('My article en');
+    $article->setContent('content en');
+
+    $repo
+        ->translate($article, 'title', 'de', 'my article de')
+        ->translate($article, 'content', 'de', 'content de')
+        ->translate($article, 'title', 'ru', 'my article ru')
+        ->translate($article, 'content', 'ru', 'content ru');
+
+    $em->persist($article);
+    $em->flush();
+
 ## Using ORM query hint {#orm-query-hint}
 
 By default, behind the scenes, when you load a record - translatable hooks into **postLoad**

+ 13 - 3
lib/Gedmo/Translatable/Document/AbstractTranslation.php

@@ -50,7 +50,7 @@ abstract class AbstractTranslation
      * @String
      */
     private $content;
-    
+
     /**
      * Get id
      *
@@ -65,10 +65,12 @@ abstract class AbstractTranslation
      * Set locale
      *
      * @param string $locale
+     * @return AbstractTranslation
      */
     public function setLocale($locale)
     {
         $this->locale = $locale;
+        return $this;
     }
 
     /**
@@ -85,10 +87,12 @@ abstract class AbstractTranslation
      * Set field
      *
      * @param string $field
+     * @return AbstractTranslation
      */
     public function setField($field)
     {
         $this->field = $field;
+        return $this;
     }
 
     /**
@@ -105,10 +109,12 @@ abstract class AbstractTranslation
      * Set object class
      *
      * @param string $objectClass
+     * @return AbstractTranslation
      */
     public function setObjectClass($objectClass)
     {
         $this->objectClass = $objectClass;
+        return $this;
     }
 
     /**
@@ -120,15 +126,17 @@ abstract class AbstractTranslation
     {
         return $this->objectClass;
     }
-    
+
     /**
      * Set foreignKey
      *
      * @param string $foreignKey
+     * @return AbstractTranslation
      */
     public function setForeignKey($foreignKey)
     {
         $this->foreignKey = $foreignKey;
+        return $this;
     }
 
     /**
@@ -140,15 +148,17 @@ abstract class AbstractTranslation
     {
         return $this->foreignKey;
     }
-    
+
     /**
      * Set content
      *
      * @param text $content
+     * @return AbstractTranslation
      */
     public function setContent($content)
     {
         $this->content = $content;
+        return $this;
     }
 
     /**

+ 61 - 2
lib/Gedmo/Translatable/Document/Repository/TranslationRepository.php

@@ -2,8 +2,9 @@
 
 namespace Gedmo\Translatable\Document\Repository;
 
-use Doctrine\ODM\MongoDB\DocumentRepository,
-    Doctrine\ODM\MongoDB\Cursor;
+use Gedmo\Translatable\TranslationListener;
+use Doctrine\ODM\MongoDB\DocumentRepository;
+use Doctrine\ODM\MongoDB\Cursor;
 
 /**
  * The TranslationRepository has some useful functions
@@ -17,6 +18,36 @@ use Doctrine\ODM\MongoDB\DocumentRepository,
  */
 class TranslationRepository extends DocumentRepository
 {
+	/**
+     * Current TranslationListener instance used
+     * in EntityManager
+     *
+     * @var TranslationListener
+     */
+    private $listener;
+
+    /**
+     * Makes additional translation of $document $field into $locale
+     * using $value
+     *
+     * @param object $document
+     * @param string $field
+     * @param string $locale
+     * @param mixed $value
+     * @return TranslationRepository
+     */
+    public function translate($document, $field, $locale, $value)
+    {
+        $meta = $this->dm->getClassMetadata(get_class($document));
+        $config = $this->getTranslationListener()->getConfiguration($this->dm, $meta->name);
+        if (!isset($config['fields']) || !in_array($field, $config['fields'])) {
+            throw new \Gedmo\Exception\InvalidArgumentException("Document: {$meta->name} does not translate - {$field}");
+        }
+        $oid = spl_object_hash($document);
+        $this->listener->addTranslation($oid, $field, $locale, $value);
+        return $this;
+    }
+
     /**
      * Loads all translations with all translatable
      * fields from the given entity
@@ -120,4 +151,32 @@ class TranslationRepository extends DocumentRepository
         }
         return $result;
     }
+
+	/**
+     * Get the currently used TranslationListener
+     *
+     * @throws \Gedmo\Exception\RuntimeException - if listener is not found
+     * @return TranslationListener
+     */
+    private function getTranslationListener()
+    {
+        if (!$this->listener) {
+            foreach ($this->dm->getEventManager()->getListeners() as $event => $listeners) {
+                foreach ($listeners as $hash => $listener) {
+                    if ($listener instanceof TranslationListener) {
+                        $this->listener = $listener;
+                        break;
+                    }
+                }
+                if ($this->listener) {
+                    break;
+                }
+            }
+
+            if (is_null($this->listener)) {
+                throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
+            }
+        }
+        return $this->listener;
+    }
 }

+ 13 - 3
lib/Gedmo/Translatable/Entity/AbstractTranslation.php

@@ -52,7 +52,7 @@ abstract class AbstractTranslation
      * @Column(name="content", type="text", nullable=true)
      */
     private $content;
-    
+
     /**
      * Get id
      *
@@ -67,10 +67,12 @@ abstract class AbstractTranslation
      * Set locale
      *
      * @param string $locale
+     * @return AbstractTranslation
      */
     public function setLocale($locale)
     {
         $this->locale = $locale;
+        return $this;
     }
 
     /**
@@ -87,10 +89,12 @@ abstract class AbstractTranslation
      * Set field
      *
      * @param string $field
+     * @return AbstractTranslation
      */
     public function setField($field)
     {
         $this->field = $field;
+        return $this;
     }
 
     /**
@@ -107,10 +111,12 @@ abstract class AbstractTranslation
      * Set object class
      *
      * @param string $objectClass
+     * @return AbstractTranslation
      */
     public function setObjectClass($objectClass)
     {
         $this->objectClass = $objectClass;
+        return $this;
     }
 
     /**
@@ -122,15 +128,17 @@ abstract class AbstractTranslation
     {
         return $this->objectClass;
     }
-    
+
     /**
      * Set foreignKey
      *
      * @param string $foreignKey
+     * @return AbstractTranslation
      */
     public function setForeignKey($foreignKey)
     {
         $this->foreignKey = $foreignKey;
+        return $this;
     }
 
     /**
@@ -142,15 +150,17 @@ abstract class AbstractTranslation
     {
         return $this->foreignKey;
     }
-    
+
     /**
      * Set content
      *
      * @param text $content
+     * @return AbstractTranslation
      */
     public function setContent($content)
     {
         $this->content = $content;
+        return $this;
     }
 
     /**

+ 71 - 15
lib/Gedmo/Translatable/Entity/Repository/TranslationRepository.php

@@ -2,15 +2,14 @@
 
 namespace Gedmo\Translatable\Entity\Repository;
 
-use Doctrine\ORM\EntityRepository,
-    Doctrine\ORM\Query,
-    Gedmo\Translatable\Exception,
-    Gedmo\Translatable\Translatable;
+use Gedmo\Translatable\TranslationListener;
+use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\Query;
 
 /**
  * The TranslationRepository has some useful functions
  * to interact with translations.
- * 
+ *
  * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  * @package Gedmo.Translatable.Entity.Repository
  * @subpackage TranslationRepository
@@ -18,11 +17,40 @@ use Doctrine\ORM\EntityRepository,
  * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  */
 class TranslationRepository extends EntityRepository
-{    
+{
+    /**
+     * Current TranslationListener instance used
+     * in EntityManager
+     *
+     * @var TranslationListener
+     */
+    private $listener;
+
+    /**
+     * Makes additional translation of $entity $field into $locale
+     * using $value
+     *
+     * @param object $entity
+     * @param string $field
+     * @param string $locale
+     * @param mixed $value
+     * @return TranslationRepository
+     */
+    public function translate($entity, $field, $locale, $value)
+    {
+        $meta = $this->_em->getClassMetadata(get_class($entity));
+        $config = $this->getTranslationListener()->getConfiguration($this->_em, $meta->name);
+        if (!isset($config['fields']) || !in_array($field, $config['fields'])) {
+            throw new \Gedmo\Exception\InvalidArgumentException("Entity: {$meta->name} does not translate - {$field}");
+        }
+        $oid = spl_object_hash($entity);
+        $this->listener->addTranslation($oid, $field, $locale, $value);
+        return $this;
+    }
     /**
      * Loads all translations with all translatable
      * fields from the given entity
-     * 
+     *
      * @param object $entity Must implement Translatable
      * @return array list of translations in locale groups
      */
@@ -37,7 +65,7 @@ class TranslationRepository extends EntityRepository
             $entityClass = $meta->name;
             $identifier = $meta->getSingleIdentifierFieldName();
             $entityId = $meta->getReflectionProperty($identifier)->getValue($entity);
-            
+
             $translationMeta = $this->getClassMetadata(); // table inheritance support
             $qb = $this->_em->createQueryBuilder();
             $qb->select('trans.content, trans.field, trans.locale')
@@ -49,7 +77,7 @@ class TranslationRepository extends EntityRepository
                 compact('entityId', 'entityClass'),
                 Query::HYDRATE_ARRAY
             );
-            
+
             if ($data && is_array($data) && count($data)) {
                 foreach ($data as $row) {
                     $result[$row['locale']][$row['field']] = $row['content'];
@@ -58,13 +86,13 @@ class TranslationRepository extends EntityRepository
         }
         return $result;
     }
-    
+
     /**
      * Find the entity $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
@@ -85,14 +113,14 @@ class TranslationRepository extends EntityRepository
             $q->setMaxResults(1);
             $result = $q->getArrayResult();
             $id = count($result) ? $result[0]['foreignKey'] : null;
-                
+
             if ($id) {
                 $entity = $this->_em->find($class, $id);
             }
         }
         return $entity;
     }
-    
+
     /**
      * Loads all translations with all translatable
      * fields by a given entity primary key
@@ -104,7 +132,7 @@ class TranslationRepository extends EntityRepository
     {
         $result = array();
         if ($id) {
-            $translationMeta = $this->getClassMetadata(); // table inheritance support            
+            $translationMeta = $this->getClassMetadata(); // table inheritance support
             $qb = $this->_em->createQueryBuilder();
             $qb->select('trans.content, trans.field, trans.locale')
                 ->from($translationMeta->rootEntityName, 'trans')
@@ -115,7 +143,7 @@ class TranslationRepository extends EntityRepository
                 array('entityId' => $id),
                 Query::HYDRATE_ARRAY
             );
-            
+
             if ($data && is_array($data) && count($data)) {
                 foreach ($data as $row) {
                     $result[$row['locale']][$row['field']] = $row['content'];
@@ -124,4 +152,32 @@ class TranslationRepository extends EntityRepository
         }
         return $result;
     }
+
+    /**
+     * Get the currently used TranslationListener
+     *
+     * @throws \Gedmo\Exception\RuntimeException - if listener is not found
+     * @return TranslationListener
+     */
+    private function getTranslationListener()
+    {
+        if (!$this->listener) {
+            foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) {
+                foreach ($listeners as $hash => $listener) {
+                    if ($listener instanceof TranslationListener) {
+                        $this->listener = $listener;
+                        break;
+                    }
+                }
+                if ($this->listener) {
+                    break;
+                }
+            }
+
+            if (is_null($this->listener)) {
+                throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
+            }
+        }
+        return $this->listener;
+    }
 }

+ 4 - 3
lib/Gedmo/Translatable/Mapping/Event/Adapter/ODM.php

@@ -112,14 +112,15 @@ final class ODM extends BaseAdapterODM implements TranslatableAdapter
     /**
      * {@inheritDoc}
      */
-    public function getTranslationValue($object, $field)
+    public function getTranslationValue($object, $field, $value = false)
     {
         $dm = $this->getObjectManager();
         $meta = $dm->getClassMetadata(get_class($object));
         $mapping = $meta->getFieldMapping($field);
         $type = Type::getType($mapping['type']);
-
-        $value = $meta->getReflectionProperty($field)->getValue($object);
+        if ($value === false) {
+            $value = $meta->getReflectionProperty($field)->getValue($object);
+        }
         return $type->convertToDatabaseValue($value);
     }
 

+ 4 - 2
lib/Gedmo/Translatable/Mapping/Event/Adapter/ORM.php

@@ -115,12 +115,14 @@ final class ORM extends BaseAdapterORM implements TranslatableAdapter
     /**
      * {@inheritDoc}
      */
-    public function getTranslationValue($object, $field)
+    public function getTranslationValue($object, $field, $value = false)
     {
         $em = $this->getObjectManager();
         $meta = $em->getClassMetadata(get_class($object));
         $type = Type::getType($meta->getTypeOfField($field));
-        $value = $meta->getReflectionProperty($field)->getValue($object);
+        if ($value === false) {
+            $value = $meta->getReflectionProperty($field)->getValue($object);
+        }
         return $type->convertToDatabaseValue($value, $em->getConnection()->getDatabasePlatform());
     }
 

+ 2 - 1
lib/Gedmo/Translatable/Mapping/Event/TranslatableAdapter.php

@@ -68,9 +68,10 @@ interface TranslatableAdapter extends AdapterInterface
      *
      * @param object $object
      * @param string $field
+     * @param mixed $value
      * @return mixed
      */
-    function getTranslationValue($object, $field);
+    function getTranslationValue($object, $field, $value = false);
 
     /**
      * Transform the value from database

+ 76 - 9
lib/Gedmo/Translatable/TranslationListener.php

@@ -74,6 +74,14 @@ class TranslationListener extends MappedEventSubscriber
      */
     private $skipOnLoad = false;
 
+    /**
+     * List of additional translations for object
+     * hash key
+     *
+     * @var array
+     */
+    private $additionalTranslations = array();
+
     /**
      * Specifies the list of events to listen
      *
@@ -101,6 +109,21 @@ class TranslationListener extends MappedEventSubscriber
         return $this;
     }
 
+    /**
+     * Add additional translation for $oid object
+     *
+     * @param string $oid
+     * @param string $field
+     * @param string $locale
+     * @param mixed $value
+     * @return TranslationListener
+     */
+    public function addTranslation($oid, $field, $locale, $value)
+    {
+        $this->additionalTranslations[$oid][$field][$locale] = $value;
+        return $this;
+    }
+
     /**
      * Mapps additional metadata
      *
@@ -239,6 +262,28 @@ class TranslationListener extends MappedEventSubscriber
             if (isset($config['fields'])) {
                 $this->handleTranslatableObjectUpdate($ea, $object, true);
             }
+            $oid = spl_object_hash($object);
+            // check for additional translations
+            if (isset($this->additionalTranslations[$oid])) {
+                $objectId = $ea->extractIdentifier($om, $object);
+                $transClass = $this->getTranslationClass($ea, $meta->name);
+                foreach ($this->additionalTranslations[$oid] as $field => $translations) {
+                    foreach ($translations as $locale => $content) {
+                        $trans = new $transClass;
+                        $trans
+                            ->setField($field)
+                            ->setObjectClass($meta->name)
+                            ->setForeignKey($objectId)
+                            ->setLocale($locale);
+                        $trans->setContent($ea->getTranslationValue($object, $field, $content));
+                        if (!$objectId) {
+                            $this->pendingTranslationInserts[spl_object_hash($object)][] = $trans;
+                        } else {
+                            $ea->insertTranslationRecord($trans);
+                        }
+                    }
+                }
+            }
         }
         // check all scheduled updates for Translatable entities
         foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
@@ -255,6 +300,33 @@ class TranslationListener extends MappedEventSubscriber
                     }
                 }
             }
+            $oid = spl_object_hash($object);
+            // check for additional translations
+            if (isset($this->additionalTranslations[$oid])) {
+                $objectId = $ea->extractIdentifier($om, $object);
+                $transClass = $this->getTranslationClass($ea, $meta->name);
+                foreach ($this->additionalTranslations[$oid] as $field => $translations) {
+                    foreach ($translations as $locale => $content) {
+                        $trans = $ea->findTranslation($objectId, $meta->name, $locale, $field, $transClass);
+                        if (!$trans) {
+                            $trans = new $transClass;
+                            $trans
+                                ->setField($field)
+                                ->setObjectClass($meta->name)
+                                ->setForeignKey($objectId)
+                                ->setLocale($locale);
+                        }
+                        $trans->setContent($ea->getTranslationValue($object, $field, $content));
+                        if ($trans->getId()) {
+                            $om->persist($trans);
+                            $transMeta = $om->getClassMetadata($transClass);
+                            $uow->computeChangeSet($transMeta, $trans);
+                        } else {
+                            $ea->insertTranslationRecord($trans);
+                        }
+                    }
+                }
+            }
         }
         // check scheduled deletions for Translatable entities
         foreach ($ea->getScheduledObjectDeletions($uow) as $object) {
@@ -284,16 +356,11 @@ class TranslationListener extends MappedEventSubscriber
         // 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);
-
-            $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,
-                        $ea->extractIdentifier($om, $object)
-                    );
+                $objectId = $ea->extractIdentifier($om, $object);
+                foreach ($this->pendingTranslationInserts[$oid] as $translation) {
+                    $translation->setForeignKey($objectId);
                     $ea->insertTranslationRecord($translation);
                 }
                 unset($this->pendingTranslationInserts[$oid]);
@@ -426,7 +493,7 @@ class TranslationListener extends MappedEventSubscriber
             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;
+                $this->pendingTranslationInserts[spl_object_hash($object)][] = $translation;
             } else {
                 // persist and compute change set for translation
                 $om->persist($translation);

+ 49 - 0
tests/Gedmo/Translatable/Fixture/Document/SimpleArticle.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Translatable\Fixture\Document;
+
+/**
+ * @Document(collection="articles")
+ */
+class SimpleArticle
+{
+    /** @Id */
+    private $id;
+
+    /**
+     * @gedmo:Translatable
+     * @String
+     */
+    private $title;
+
+    /**
+     * @gedmo:Translatable
+     * @String
+     */
+    private $content;
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    public function setContent($content)
+    {
+        $this->content = $content;
+    }
+
+    public function getContent()
+    {
+        return $this->content;
+    }
+}

+ 120 - 0
tests/Gedmo/Translatable/TranslatableDocumentCollectionTest.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace Gedmo\Translatable;
+
+use Tool\BaseTestCaseMongoODM;
+use Doctrine\Common\EventManager;
+use Translatable\Fixture\Document\SimpleArticle as Article;
+
+/**
+ * These are tests for translatable behavior
+ *
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Translatable
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class TranslatableDocumentCollectionTest extends BaseTestCaseMongoODM
+{
+    const ARTICLE = 'Translatable\\Fixture\\Document\\SimpleArticle';
+    const TRANSLATION = 'Gedmo\\Translatable\\Document\\Translation';
+
+    private $translatableListener;
+    private $id;
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $evm = new EventManager;
+        $this->translatableListener = new TranslationListener();
+        $this->translatableListener->setTranslatableLocale('en_us');
+        $evm->addEventSubscriber($this->translatableListener);
+
+        $this->getMockDocumentManager($evm);
+        $this->populate();
+    }
+
+    public function testMultipleTranslationPersistence()
+    {
+        $repo = $this->dm->getRepository(self::TRANSLATION);
+        $sport = $this->dm->getRepository(self::ARTICLE)->find($this->id);
+        $translations = $repo->findTranslations($sport);
+
+        $this->assertArrayHasKey('en_us', $translations);
+        $this->assertArrayHasKey('title', $translations['en_us']);
+        $this->assertArrayHasKey('content', $translations['en_us']);
+        $this->assertEquals('Sport', $translations['en_us']['title']);
+        $this->assertEquals('about sport', $translations['en_us']['content']);
+
+        $this->assertArrayHasKey('de_de', $translations);
+        $this->assertArrayHasKey('title', $translations['de_de']);
+        $this->assertArrayHasKey('content', $translations['de_de']);
+        $this->assertEquals('sport de', $translations['de_de']['title']);
+        $this->assertEquals('content de', $translations['de_de']['content']);
+
+        $this->assertArrayHasKey('ru_ru', $translations);
+        $this->assertArrayHasKey('title', $translations['ru_ru']);
+        $this->assertArrayHasKey('content', $translations['ru_ru']);
+        $this->assertEquals('sport ru', $translations['ru_ru']['title']);
+        $this->assertEquals('content ru', $translations['ru_ru']['content']);
+    }
+
+    public function testMultipleTranslationUpdates()
+    {
+        $repo = $this->dm->getRepository(self::TRANSLATION);
+        $sport = $this->dm->getRepository(self::ARTICLE)->find($this->id);
+        $sport->setTitle('Changed');
+        $repo
+            ->translate($sport, 'title', 'lt_lt', 'sport lt')
+            ->translate($sport, 'content', 'lt_lt', 'content lt')
+            ->translate($sport, 'title', 'ru_ru', 'sport ru change')
+            ->translate($sport, 'content', 'ru_ru', 'content ru change');
+
+        $this->dm->persist($sport);
+        $this->dm->flush();
+        $translations = $repo->findTranslations($sport);
+
+        $this->assertArrayHasKey('en_us', $translations);
+        $this->assertArrayHasKey('title', $translations['en_us']);
+        $this->assertArrayHasKey('content', $translations['en_us']);
+        $this->assertEquals('Changed', $translations['en_us']['title']);
+        $this->assertEquals('about sport', $translations['en_us']['content']);
+
+        $this->assertArrayHasKey('de_de', $translations);
+        $this->assertArrayHasKey('title', $translations['de_de']);
+        $this->assertArrayHasKey('content', $translations['de_de']);
+        $this->assertEquals('sport de', $translations['de_de']['title']);
+        $this->assertEquals('content de', $translations['de_de']['content']);
+
+        $this->assertArrayHasKey('ru_ru', $translations);
+        $this->assertArrayHasKey('title', $translations['ru_ru']);
+        $this->assertArrayHasKey('content', $translations['ru_ru']);
+        $this->assertEquals('sport ru change', $translations['ru_ru']['title']);
+        $this->assertEquals('content ru change', $translations['ru_ru']['content']);
+
+        $this->assertArrayHasKey('lt_lt', $translations);
+        $this->assertArrayHasKey('title', $translations['lt_lt']);
+        $this->assertArrayHasKey('content', $translations['lt_lt']);
+        $this->assertEquals('sport lt', $translations['lt_lt']['title']);
+        $this->assertEquals('content lt', $translations['lt_lt']['content']);
+    }
+
+    private function populate()
+    {
+        $repo = $this->dm->getRepository(self::TRANSLATION);
+        $sport = new Article;
+        $sport->setTitle('Sport');
+        $sport->setContent('about sport');
+
+        $repo
+            ->translate($sport, 'title', 'de_de', 'sport de')
+            ->translate($sport, 'content', 'de_de', 'content de')
+            ->translate($sport, 'title', 'ru_ru', 'sport ru')
+            ->translate($sport, 'content', 'ru_ru', 'content ru');
+
+        $this->dm->persist($sport);
+        $this->dm->flush();
+        $this->id = $sport->getId();
+    }
+}

+ 137 - 0
tests/Gedmo/Translatable/TranslatableEntityCollectionTest.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace Gedmo\Translatable;
+
+use Doctrine\Common\EventManager;
+use Tool\BaseTestCaseORM;
+use Translatable\Fixture\Article;
+use Translatable\Fixture\Comment;
+
+/**
+ * These are tests for translatable behavior
+ *
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Translatable
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class TranslatableEntityCollectionTest extends BaseTestCaseORM
+{
+    const ARTICLE = 'Translatable\\Fixture\\Article';
+    const COMMENT = 'Translatable\\Fixture\\Comment';
+    const TRANSLATION = 'Gedmo\\Translatable\\Entity\\Translation';
+
+    private $translatableListener;
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $evm = new EventManager;
+        $this->translatableListener = new TranslationListener();
+        $this->translatableListener->setTranslatableLocale('en_us');
+        $evm->addEventSubscriber($this->translatableListener);
+
+        $conn = array(
+            'driver' => 'pdo_mysql',
+            'host' => '127.0.0.1',
+            'dbname' => 'test',
+            'user' => 'root',
+            'password' => 'nimda'
+        );
+        //$this->getMockCustomEntityManager($conn, $evm);
+        $this->getMockSqliteEntityManager($evm);
+        $this->populate();
+    }
+
+    public function testMultipleTranslationPersistence()
+    {
+        $repo = $this->em->getRepository(self::TRANSLATION);
+        $sport = $this->em->getRepository(self::ARTICLE)->find(1);
+        $translations = $repo->findTranslations($sport);
+
+        $this->assertArrayHasKey('en_us', $translations);
+        $this->assertArrayHasKey('title', $translations['en_us']);
+        $this->assertArrayHasKey('content', $translations['en_us']);
+        $this->assertEquals('Sport', $translations['en_us']['title']);
+        $this->assertEquals('about sport', $translations['en_us']['content']);
+
+        $this->assertArrayHasKey('de_de', $translations);
+        $this->assertArrayHasKey('title', $translations['de_de']);
+        $this->assertArrayHasKey('content', $translations['de_de']);
+        $this->assertEquals('sport de', $translations['de_de']['title']);
+        $this->assertEquals('content de', $translations['de_de']['content']);
+
+        $this->assertArrayHasKey('ru_ru', $translations);
+        $this->assertArrayHasKey('title', $translations['ru_ru']);
+        $this->assertArrayHasKey('content', $translations['ru_ru']);
+        $this->assertEquals('sport ru', $translations['ru_ru']['title']);
+        $this->assertEquals('content ru', $translations['ru_ru']['content']);
+    }
+
+    public function testMultipleTranslationUpdates()
+    {
+        $repo = $this->em->getRepository(self::TRANSLATION);
+        $sport = $this->em->getRepository(self::ARTICLE)->find(1);
+        $sport->setTitle('Changed');
+        $repo
+            ->translate($sport, 'title', 'lt_lt', 'sport lt')
+            ->translate($sport, 'content', 'lt_lt', 'content lt')
+            ->translate($sport, 'title', 'ru_ru', 'sport ru change')
+            ->translate($sport, 'content', 'ru_ru', 'content ru change');
+
+        $this->em->persist($sport);
+        $this->em->flush();
+        $translations = $repo->findTranslations($sport);
+
+        $this->assertArrayHasKey('en_us', $translations);
+        $this->assertArrayHasKey('title', $translations['en_us']);
+        $this->assertArrayHasKey('content', $translations['en_us']);
+        $this->assertEquals('Changed', $translations['en_us']['title']);
+        $this->assertEquals('about sport', $translations['en_us']['content']);
+
+        $this->assertArrayHasKey('de_de', $translations);
+        $this->assertArrayHasKey('title', $translations['de_de']);
+        $this->assertArrayHasKey('content', $translations['de_de']);
+        $this->assertEquals('sport de', $translations['de_de']['title']);
+        $this->assertEquals('content de', $translations['de_de']['content']);
+
+        $this->assertArrayHasKey('ru_ru', $translations);
+        $this->assertArrayHasKey('title', $translations['ru_ru']);
+        $this->assertArrayHasKey('content', $translations['ru_ru']);
+        $this->assertEquals('sport ru change', $translations['ru_ru']['title']);
+        $this->assertEquals('content ru change', $translations['ru_ru']['content']);
+
+        $this->assertArrayHasKey('lt_lt', $translations);
+        $this->assertArrayHasKey('title', $translations['lt_lt']);
+        $this->assertArrayHasKey('content', $translations['lt_lt']);
+        $this->assertEquals('sport lt', $translations['lt_lt']['title']);
+        $this->assertEquals('content lt', $translations['lt_lt']['content']);
+    }
+
+    private function populate()
+    {
+        $repo = $this->em->getRepository(self::TRANSLATION);
+        $sport = new Article;
+        $sport->setTitle('Sport');
+        $sport->setContent('about sport');
+
+        $repo
+            ->translate($sport, 'title', 'de_de', 'sport de')
+            ->translate($sport, 'content', 'de_de', 'content de')
+            ->translate($sport, 'title', 'ru_ru', 'sport ru')
+            ->translate($sport, 'content', 'ru_ru', 'content ru');
+
+        $this->em->persist($sport);
+        $this->em->flush();
+    }
+
+    protected function getUsedEntityFixtures()
+    {
+        return array(
+            self::ARTICLE,
+            self::TRANSLATION,
+            self::COMMENT
+        );
+    }
+}