1
votes

I'm playing with Doctrine ODM, trying to make some i18n-able field in my Mongo document. This is what I want to achieve in Mongo:

{
    "title": {
      "en": "Car",
      "eu": "Autoa"
    }
}

And the PHP API I want for the Document would be something like this:

$doc->getTitle()->setDefaultLocale('en');
$doc->getTitle(); // "Car"
$doc->getTitle()->get('eu'); // "Autoa"
$doc->getTitle()->set('es', 'Coche');
$doc->getTitle()->setTranslations([
  'fr' => 'Voiture',
  'eu' => 'Kotxea',
]);
$doc->getTitle()->getTranslations(); // ["en" => "Car", ...]

I have tried two aproaches, both of them with it's own pitfalls. I don't like none of them.

Custom Annotation

I've created a class which will be the middleman between the document and mongo. This class will be placed in the field, in this case in $title.

class Translation
{
    protected $default;
    protected $translations;

    public function __construct(array $translations = array()) { /* ... */ }
    public function get($locale) { /* ... */ }
    public function getTranslations() { /* ... */ }
    public function set($locale, $value) { /* ... */ }
    public function setDefaultLocale($default) { /* ... */ }
    public function setTranslations(array $translations = array()) { /* ... */ }
}

Then, I've created a custom FieldType, which converts the Mongo array to the Translation middleman object and viceversa (convertTo* methods seem to be ignored by Doctrine and are equal to the closureTo* methods, so I'll omit them):

class TranslationType extends \Doctrine\ODM\MongoDB\Types\Type
{
    public function convertToDatabaseValue($value) { /* ... */ }
    public function convertToPHPValue($value) { /* ... */ }

    public function closureToMongo()
    {
        return '$return = $value->getTranslations();';
    }

    public function closureToPHP()
    {
        return '$return = new \App\TransBundle\MongoDB\Translation($value);';
    }
}

Then, I have my annotation:

/** @Annotation */
class Translation extends \Doctrine\ODM\MongoDB\Mapping\Annotations\AbstractField
{
    public $type = 'translation';
}

And the document:

use App\TransBundle\MongoDB\Annotations\Translation;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/** @MongoDB\Document */
class Translated
{
    /** @MongoDB\Id */
    protected $id;

    /** @Translation */
    protected $title;

    public function getId() { /* ... */ }
    public function getTitle() { /* ... */ }
}

The GOOD parts:

  • Easy usage: one use, property declaration, annotation and the getter.
  • Reads OK from Mongo => Doctrine.
  • Meets API requirements.

The BAD parts:

  • Doesn't save to DB, I suppose that it's because the Translation object doesn't dirty the title property on the parent object Translated.
  • $title doesn't get initialized to the middleman Translation object on object creation.
    • This would be fixed by initializing the object in the constructor, but if possible I'd like to try to avoid this to keep the usage as lean as possible. I'll have to find a workaround.

EmbedOne

The second approach consists of using an embedded document, this works perfectly, but has it's own small issues. :-)

First, my base Translation class for the embedded document, this class will work directly on the class properties instead of an array property:

class BaseTranslation
{    
    public function __construct(array $translations = array()) { /* ... */ }
    public function get($locale) { /* ... */ }
    public function getTranslations() { /* ... */ }
    public function set($locale, $value) { /* ... */ }
    public function setDefaultLocale($default) { /* ... */ }
    public function setTranslations(array $translations = array()) { /* ... */ }
}

Then, the Translation class to be used in my projects, this will be the actual embbeded document:

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/** @MongoDB\EmbeddedDocument */
class Translation extends BaseTranslation
{
    /** @MongoDB\String */
    protected $en;
    
    /** @MongoDB\String */
    protected $es;
    
    /** @MongoDB\String */
    protected $fr;
}

Finally the Document

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/** @MongoDB\Document */
class Translated
{
    /** @MongoDB\Id */
    protected $id;

    /** @MongoDB\EmbedOne(targetDocument="Translation") */
    protected $title;

    public function getId() { /* ... */ }
    public function getTitle() { /* ... */ }
}

The GOOD parts:

  • It just works, reads and writes nicely.
  • Easy setup.
  • Can be used with any data type, so it would be easy to add an isTranslated i18n boolean field, just add a new TranslationBoolean class.

The BAD parts:

  • Not a big problem but the locales are hardcoded in the Translation class, it would be nice to be able to work directly on an array but this would add another level in the schema and type coercion might be lost.
  • Like in the other approach, the property doesn't get initialized, but it's easy as initializing it in the constructor (like any One2Many relation).

Conclusions

I'm liking more the second approach, which works how it's now. Do you have any idea how to overcome the BAD parts on both approaches?

Thanks!

1

1 Answers

0
votes

Regarding the type methods:

  • closureToMongo() is actually not used.
  • closureToPHP() is used by the HydratorFactory class.
  • convertToPHPValue() is used by ClassMetadataInfo, but only for identifiers.
  • convertToDatabaseValue() is used in a few places, such as UnitOfWork and PersistenceBuilder.

The missing convertToDatabaseValue() implementation could be related to why you're seeing TranslationType fail to persist to the database.

The issue of "hardcoded locales in the Translation class" could be alleviated by storing an object (Hash annotation) within this field, although that would add another layer to the model. An EmbedMany array of objects containing a field each for the language code and value would consume even more space, although the model could make it easier to devise a form to edit the data.

Have you looked into the DoctrineExtensions library? It contains a component for having translatable documents/entities (supports both ODM and ORM). The author has slowed down his own development of it, but there are a healthy amount of pull requests being merged on the project and it is widely used.