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 objectTranslated
. - $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!