I(blah...blah...blah)n

Published on

Nov 17, 2011

knp

Nov 18, 2011 − You love Doctrine2 and its explicit way of doing things, but miss i18n from Doctrine1 and Propel? Want to make rich i18n forms or retrieve all your entity translations in one simple query? New DoctrineExtensionsTranslator comes to rescue!

You love Doctrine2 and its explicit way of doing things, but miss i18n from Doctrine1 and Propel? Want to make rich i18n forms or retrieve all your entity translations in one simple query?

New DoctrineExtensionsTranslator comes to rescue!

There's already a Translatable extension in Gedmo DoctrineExtensions repo. It's super easy to setup and to use, when you're talking bout single translation persist:

$article = $em->find('EntityArticle', 1 /*article id*/);
$article->setTitle('my title in de');
$article->setContent('my content in de');
$article->setTranslatableLocale('de_de'); // change locale
$em->persist($article);
$em->flush();

but becomes incredibly hard, when you're talking bout translation outputting:

$article = $em->find('EntityArticle', 1 /*article id*/);
$repository = $em->getRepository('GedmoTranslatableEntityTranslation');
$translations = $repository->findTranslations($article);

or multiple translations writing:

$repository = $em->getRepository('Gedmo\Translatable\Entity\Translation');
$article = new Article;
$article->setTitle('My article en');
$article->setContent('content en');

$repository->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();

And if you start to think about something like multilingual forms - your work could simply become a hell.

So, today we're presenting another way to do i18n inside your Doctrine2-enabled project - Translator. It's little bit more explicit to setup inside your entities, but much more easier to use and extend later.

Let's start with simple Person entity:

<?php

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 */
class Person
{
    /**
     * @ORMId
     * @ORMGeneratedValue
     * @ORMColumn(type="integer")
     */
    private $id;
    /**
     * @ORMColumn(name="name", type="string", length=128)
     */
    private $name;
    /**
     * @ORMColumn(name="desc", type="string", length=128)
     */
    private $description;

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setDescription($description)
    {
        $this->description = $description;
    }

    public function getDescription()
    {
        return $this->description;
    }
}

Lets say we want to make our name property translatable. First, we need a some place to store our translation values. This place is a special PersonTranslation entity, which you should create by hands:

<?php

use DoctrineORMMapping as ORM;
use GedmoTranslatorEntityTranslation;

/**
 * @ORMTable(
 *         indexes={@ORMindex(name="person_translations_lookup_idx", columns={
 *             "locale", "translatable_id"
 *         })},
 *         uniqueConstraints={@ORMUniqueConstraint(name="person_lookup_unique_idx", columns={
 *             "locale", "translatable_id", "property"
 *         })}
 * )
 * @ORMEntity
 */
class PersonTranslation extends Translation
{
    /**
     * @ORMManyToOne(targetEntity="Person", inversedBy="translations")
     */
    protected $translatable;
}

and map it to the Person entity with one-to-many relation:

/**
 * @ORMOneToMany(
 *     targetEntity="PersonTranslation",
 *     mappedBy="translatable",
 *     cascade={"persist"}
 * )
 */
private $translations;

public function __construct()
{
    $this->translations = new DoctrineCommonCollectionsArrayCollection();
}

Now you have an entity itself and the translations collection in it, which will store and persist all your translations automatically.

Now, how to actually add/use translations of your entity? With additional translate() method inside your entity:

public function translate($locale = null)
{
    if (null === $locale) {
        return $this;
    }

    return new GedmoTranslatorTranslationProxy($this,
    /* Locale                            */ $locale,
    /* List of translatable properties:  */ array('name'),
    /* Translation entity class:         */ 'PersonTranslation',
    /* Translations collection property: */ $this->translations
    );
}

Now you could simply write:

$person = new Person();

$person->translate()->setName('Konstantin');
$person->translate('ru')->setName('Константин');

// persist and flush entity with made translations:
$em->persist($person);
$em->flush();

Now, to retrieve and use your entity translations, just do:

$person = $em->getRepository('Person')->findOneByName('Konstantin');

// 'Konstantin' == $person->getName() == $person->translate()->getName()
// 'Константин' != $person->getName()
// 'Константин' == $person->translate('ru')->getName()

The first time you'll require non-default translation - Doctrine2 will automatically make an additional request to database to reach it. But what if you want to get entity with transaltions in one query? Use joins:

$persons = $em->getRepository('Person')
    ->createQueryBuilder('p')
    ->select('p, t')
    ->join('p.translations', 't')
    ->getQuery()
    ->execute();

// no additional query:
$ruName = $persons[0]->translate('ru')->getName();

It's that simple! But what if i want to create a i18n-enabled form or define validators on the translatable fields? Use custom TranslationProxy:

The Symfony forms are not able to deal with non-existent [gs]etters as the ones simulated by the use of “magic” method in the generic translation proxy so you must create your own proxy class extending it:

class CustomProxy extends TranslationProxy
{
    public function setName($name)
    {
        return $this->setTranslatedValue('name', $name);
    }

    public function getName()
    {
        return $this->getTranslatedValue('name');
    }
}

And update the ->translate() method of the Person entity and add specific getters for the wanted translations:

public function translate($locale = null)
{
    if (null === $locale) {
        return $this;
    }

    return new CustomProxy($this,
                           $locale,
                           array('name'),
                           'PersonTranslation',
                           $this->translations
    );
}

public function getEnTranslation()
{
    return $this->translate('en');
}

public function getFrTranslation()
{
    return $this->translate('fr');
}

Now, you can create the form type associated to the Person entity:

class PersonType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('enTranslation.name', 'text')
            ->add('frTranslation.name', 'text')
        ;
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Person'
        );
    }
}

For the validation, you need to specify each translation must be valid on the Person entity:

/**
 * @AssertValid
 */
public function getEnTranslation()
{
    return $this->translate('en');
}

/**
 * @AssertValid
 */
public function getFrTranslation()
{
    return $this->translate('fr');
}

And map the constraints you want to your custom proxy's getters:

/**
 * @AssertNotBlank
 * @AssertMaxLength(255)
 */
public function getName()
{
    return $this->getTranslatedValue('name');
}

Written by

KNP Labs
KNP Labs

Comments