Manipulez des traductions avec Doctrine Gedmo Translatable (French)

2013-08-20

  symfony    doctrine    french    internationalization 

Lorsque l'on cherche à manipuler plusieurs traductions dans nos entités, nous arrivons rapidement sur l'extension Doctrine Gedmo Translatable, qui permet de manipuler très simplement et efficacement des traductions.

Nous allons donc voir comment utiliser cette extension.

Installation

Commencez par ajouter le package suivant dans votre fichier composer.json :


"require": {
    ...
    "gedmo/doctrine-extensions": "dev-master"
}

Lancez ensuite un php composer.phar update gedmo/doctrine-extensions pour compléter l'installation du bundle.

Configuration

Editez votre fichier app/config/config.yml afin d'y ajouter le listener translatable :


services:
    # Doctrine Extension listeners to handle behaviors
    gedmo.listener.translatable:
        class: Gedmo\\Translatable\\TranslatableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ @annotation_reader ] ]
            - [ setDefaultLocale, [ en ] ]
            - [ setTranslationFallback, [ true ] ]

La configuration est terminée. Nous allons désormais mettre en place une structure vous permettant de gérer les traductions de vos propriétés dans vos entités.

Modification des entités

Afin d'ajouter des champs "translatable", il faut définir quelques propriétés de mapping (nous utilisons ici les annotations) à certains éléments dans vos entités.

Prenons ici par exemple une entité Post dans laquelle nous souhaitons traduire le titre :


namespace Eko\\MyBundle\\Entity;

use Doctrine\\ORM\\Mapping as ORM;

use Gedmo\\Mapping\\Annotation as Gedmo;
use Gedmo\\Translatable\\Translatable;

/**
 * Class Post
 *
 * @ORM\\Entity
 * @ORM\\HasLifecycleCallbacks
 * @ORM\\Table(name="post")
 * @ORM\\Entity(repositoryClass="Eko\\MyBundle\\Repository\\PostRepository")
 *
 * @Gedmo\\TranslationEntity(class="Eko\\MyBundle\\Entity\\Translation\\PostTranslation")
 */
class Post implements Translatable {
    /**
     * Post title
     *
     * @Gedmo\\Translatable
     * @ORM\\Column(type="text", length=250, nullable=true)
     */
    protected $title;

    /**
     * Post locale
     * Used locale to override Translation listener's locale
     *
     * @Gedmo\\Locale
     */
    protected $locale;

    /**
     * Sets post title
     *
     * @param string $title
     */
    public function setTitle($title)
    {
        $this->title = $title;
    }
    
    /**
     * Returns post title
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Sets translatable locale
     *
     * @param string $locale
     */
    public function setTranslatableLocale($locale)
    {
        $this->locale = $locale;
    }
}

Comme vous pouvez le voir, nous définissons ici une annotation TranslationEntity nous permettant de définir une entité qui sera utilisée pour les traductions. Nous pouvons donc avoir des traductions stockées dans des tables séparées au lieu de stocker toutes les traductions de toutes les entités dans la même table.

Créons donc cette entité de traduction :


namespace Eko\\MyBundle\\Entity\\Translation;

use Doctrine\\ORM\\Mapping as ORM;
use Gedmo\\Translatable\\Entity\\MappedSuperclass\\AbstractTranslation;

/**
 * @ORM\\Table(name="ext_translations_post", indexes={
 *      @ORM\\Index(name="post_translation_idx", columns={"locale", "object_class", "field", "foreign_key"})
 * })
 * @ORM\\Entity(repositoryClass="Gedmo\\Translatable\\Entity\\Repository\\TranslationRepository")
 */
class PostTranslation extends AbstractTranslation
{

}

Vous disposez désormais d'entités et pouvez commencer à traduire vos champs en base en définissant une locale avant de persister vos entités. N'oubliez pas de mettre à jour votre schéma de base de données.

Comment récupérer les champ traduis dans nos repository ?

Pour récupérer les champs traduis dans la locale actuellement utilisée dans la request, je vous ai mis au point un petit système sympa.

Afin d'ajouter automatiquement une "hint" à vos requêtes effectuées dans vos repository, j'ai écris cette classe qui pourra être étendue par vos repository disposant de traductions :


namespace Eko\\MyBundle\\Repository;

use Doctrine\\ORM\\EntityRepository;
use Doctrine\\ORM\\QueryBuilder;
use Doctrine\\ORM\\AbstractQuery;
use Doctrine\\ORM\\Query;

use Gedmo\\Translatable\\TranslatableListener;

/**
 * Class TranslatableRepository
 *
 * This is my translatable repository that offers methods to retrieve results with translations
 */
class TranslatableRepository extends EntityRepository
{
    /**
     * @var string Default locale
     */
    protected $defaultLocale;

    /**
     * Sets default locale
     *
     * @param string $locale
     */
    public function setDefaultLocale($locale)
    {
        $this->defaultLocale = $locale;
    }

    /**
     * Returns translated one (or null if not found) result for given locale
     *
     * @param QueryBuilder $qb            A Doctrine query builder instance
     * @param string       $locale        A locale name
     * @param string       $hydrationMode A Doctrine results hydration mode
     *
     * @return QueryBuilder
     */
    public function getOneOrNullResult(QueryBuilder $qb, $locale = null, $hydrationMode = null)
    {
        return $this->getTranslatedQuery($qb, $locale)->getOneOrNullResult($hydrationMode);
    }

    /**
     * Returns translated results for given locale
     *
     * @param QueryBuilder $qb            A Doctrine query builder instance
     * @param string       $locale        A locale name
     * @param string       $hydrationMode A Doctrine results hydration mode
     *
     * @return QueryBuilder
     */
    public function getResult(QueryBuilder $qb, $locale = null, $hydrationMode = AbstractQuery::HYDRATE_OBJECT)
    {
        return $this->getTranslatedQuery($qb, $locale)->getResult($hydrationMode);
    }

    /**
     * Returns translated array results for given locale
     *
     * @param QueryBuilder $qb     A Doctrine query builder instance
     * @param string       $locale A locale name
     *
     * @return QueryBuilder
     */
    public function getArrayResult(QueryBuilder $qb, $locale = null)
    {
        return $this->getTranslatedQuery($qb, $locale)->getArrayResult();
    }

    /**
     * Returns translated single result for given locale
     *
     * @param QueryBuilder $qb            A Doctrine query builder instance
     * @param string       $locale        A locale name
     * @param string       $hydrationMode A Doctrine results hydration mode
     *
     * @return QueryBuilder
     */
    public function getSingleResult(QueryBuilder $qb, $locale = null, $hydrationMode = null)
    {
        return $this->getTranslatedQuery($qb, $locale)->getSingleResult($hydrationMode);
    }

    /**
     * Returns translated scalar result for given locale
     *
     * @param QueryBuilder $qb     A Doctrine query builder instance
     * @param string       $locale A locale name
     *
     * @return QueryBuilder
     */
    public function getScalarResult(QueryBuilder $qb, $locale = null)
    {
        return $this->getTranslatedQuery($qb, $locale)->getScalarResult();
    }

    /**
     * Returns translated single scalar result for given locale
     *
     * @param QueryBuilder $qb     A Doctrine query builder instance
     * @param string       $locale A locale name
     *
     * @return QueryBuilder
     */
    public function getSingleScalarResult(QueryBuilder $qb, $locale = null)
    {
        return $this->getTranslatedQuery($qb, $locale)->getSingleScalarResult();
    }

    /**
     * Returns translated Doctrine query instance
     *
     * @param QueryBuilder $qb     A Doctrine query builder instance
     * @param string       $locale A locale name
     *
     * @return Query
     */
    protected function getTranslatedQuery(QueryBuilder $qb, $locale = null)
    {
        $locale = null === $locale ? $this->defaultLocale : $locale;

        $query = $qb->getQuery();

        $query->setHint(
            Query::HINT_CUSTOM_OUTPUT_WALKER,
            'Gedmo\\\\Translatable\\\\Query\\\\TreeWalker\\\\TranslationWalker'
        );

        $query->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, $locale);

        return $query;
    }
}

Ainsi, vos repository peuvent étendre celui-ci :


namespace Eko\\MyBundle\\Repository;

use Doctrine\\ORM\\EntityRepository;
use Doctrine\\ORM\\QueryBuilder;

use Eko\\MyBundle\\Repository\\TranslatableRepository;

/**
 * Class PostRepository
 *
 * This is the Post entity repository class
 */
class PostRepository extends TranslatableRepository
{

Ainsi, au lieu d'appeler votre méthode getResult() (par exemple) depuis votre objet query obtenu depuis le QueryBuilder, vous appelerez $this->getResult($qb) en passant l'objet QueryBuilder.

Un petit exemple :


    /**
     * Returns all posts
     *
     * @return array
     */
    public function findAll()
    {
        $qb = $this->createQueryBuilder('post');

        return $this->getResult($qb, 'fr');
    }

Dans cet exemple, nous demandons donc les traductions françaises.

Passer automatiquement la locale de la request

Pour aller un peu plus loin, nous allons également définir un TranslatableManager qui sera étendu par nos managers afin de définir la locale du TranslatableRepository en fonction de la request courante.

Voici notre TranslatableManager :


namespace Eko\\MyBundle\\Manager;

use Symfony\\Component\\DependencyInjection\\ContainerInterface;

use Doctrine\\ORM\\EntityManager;

/**
 * Translatable entity manager
 */
class TranslatableManager
{
    /**
     * @var \\Doctrine\\ORM\\EntityManager
     */
    protected $em;

    /**
     * @var EntityRepository
     */
    protected $repository;

    /**
     * @var string Class name
     */
    protected $class;


    /**
     * Constructor
     *
     * @param EntityManager $em    Entity manager
     * @param string        $class Class name
     */
    public function __construct(EntityManager $em, $class)
    {
        $this->class      = $class;
        $this->em         = $em;
        $this->repository = $em->getRepository($this->class);
    }

    /**
     * Sets the repository request default locale
     *
     * @param ContainerInterface|null $container
     * 
     * @throws \\InvalidArgumentException if repository is not an instance of TranslatableRepository
     */
    public function setRepositoryLocale($container)
    {
        if (null !== $container) {
            if (!$this->repository instanceof TranslatableRepository) {
                throw new \\InvalidArgumentException('A TranslatableManager needs to be linked with a TranslatableRepository to sets default locale.');
            }

            if ($container->isScopeActive('request')) {
                $locale = $container->get('request')->getLocale();
                $this->repository->setDefaultLocale($locale);
            }
        }
    }
}

Faisons étendre nos managers de ce TranslatableManager :


namespace Eko\\MyBundle\\Manager;

use Eko\\MyBundle\\Manager\\TranslatableManager;

/**
 * Post entity manager
 */
class PostManager extends TranslatableManager
{

Voilà, il ne nous reste plus qu'à ajouter l'appel de la méthode setRepositoryLocale à l'initialisation de notre service, comme suit :


<service id="eko.mybundle.manager.post" class="Eko\\MyBundle\\Manager\\PostManager" public="true">
    <argument type="service" id="doctrine.orm.default_entity_manager" />
    <argument>Eko\\MyBundle\\Entity\\Post</argument>

    <call method="setRepositoryLocale"><argument type="service" id="service_container" /></call>
</service>

Voilà !

Vous savez désormais tout sur comment gérer plusieurs traductions de champs dans vos entités en base de données.

→ Documentation de Gedmo Translatable : https://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/translatable.md

Comments