Configurando DoctrineExtensions en Symfony 2

Este artículo forma parte de la serie Symfony2

Antes de continuar se recomienda leer : Instalando y configurando Symfony2 en un ambiente compartido

Doctrine ORM es una plataforma que permite trabajar base de datos relacionales de forma orientada a objetos de ahi su nombre (Object Relational Mapping). La version 2.x ha sido rediseñada y reescrita desde cero inspirándose en plataformas Java como Hibernate y Spring e implementando varios de los patrones de diseño descrito por el reconocido ingeniero del software Martín Fowler.

La version 1.x incluye en su núcleo los llamados comportamientos (behaviors: translatable o i18n, slug, timestampable, …) los cuales fueron removidos en la versión 2.x por lo que si queremos usar algunos de ellos debemos usar DoctrineExtensions

Este artículo presupone que tiene configurado un proyecto Symfony2 (2.0.12) y Doctrine ORM (2.2.1) aunque el procedimiento que se describe puede usarse con otras versiones.

Descargar DoctrineExtensions

– Ir a DoctrineExtensions v2.3.0
– Descompactar
– Ir la carpeta lib del directorio resultante del paso anterior
– Si deseas que estas extension sea usada por varios proyectos entonces puedes copiar la carpeta Gedmo a /usr/share/php

$ sudo sudo rsync -avz --no-p --no-o --no-g Gedmo /usr/share/php/

sino debes copiar la Carpeta en vendor de tu proyecto.

Crear DoctrineExtensions Listener

Ponga la siguiente clase dentro del DIR DependencyInjection de su Bundle

<?php
// Ajuste su Namespace
namespace CompanyName\BundleName\DependencyInjection;
 
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class DoctrineExtensionListener implements ContainerAwareInterface
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    public function onLateKernelRequest(GetResponseEvent $event)
    {
        $translatable = $this->container->get('gedmo.listener.translatable');
        $translatable->setTranslatableLocale($event->getRequest()->getLocale());
        $translatable->setDefaultLocale('es_es');
        $translatable->setTranslationFallback(true);
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        $securityContext = $this->container->get('security.context', ContainerInterface::NULL_ON_INVALID_REFERENCE);
        if (null !== $securityContext && null !== $securityContext->getToken() && $securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
            $loggable = $this->container->get('gedmo.listener.loggable');
            $loggable->setUsername($securityContext->getToken()->getUsername());
        }
    }
}

Crear doctrine_extensions yml

Ponga este fichero en app/config, se han habilitados todos los comportamientos habilite solo lo que Ud necesite

# services to handle doctrine extensions
# import it in config.yml
services:
    # KernelRequest listener
    extension.listener:
        # Ajustar Namespace
        class: CompanyBundleNameDependencyInjectionDoctrineExtensionListener
        calls:
            - [ setContainer, [ @service_container ] ]
        tags:
            # translatable sets locale after router processing
            - { name: kernel.event_listener, event: kernel.request, method: onLateKernelRequest, priority: -10 }
            # loggable hooks user username if one is in security context
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

    # Doctrine Extension listeners to handle behaviors
    gedmo.listener.tree:
        class: GedmoTreeTreeListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ @annotation_reader ] ]

    gedmo.listener.translatable:
        class: GedmoTranslatableTranslatableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ @annotation_reader ] ]
            - [ setDefaultLocale, [ %locale% ] ]
            - [ setTranslationFallback, [ false ] ]

    gedmo.listener.timestampable:
        class: GedmoTimestampableTimestampableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ @annotation_reader ] ]

    gedmo.listener.sluggable:
        class: GedmoSluggableSluggableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
           - [ setAnnotationReader, [ @annotation_reader ] ]

    gedmo.listener.sortable:
        class: GedmoSortableSortableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ @annotation_reader ] ]

    gedmo.listener.loggable:
        class: GedmoLoggableLoggableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ @annotation_reader ] ]

Importar doctrine_extensions.yml en el config.yml

...
imports:
    - { resource: parameters.ini }
    - { resource: security.yml }
    - { resource: doctrine_extensions.yml }
...

Agregar la sección translatable a la sección doctrine en el config.yml

Usamos traducciones personales o es lo que lo mismo cada tabla que tienen campos internacionalizados tendrá una tabla de traducciones

...
doctrine:
    dbal:
        driver:   pdo_mysql
        host:     localhost
        port:     3306
        dbname:   DB_NAME
        user:     root
        password: 
        charset:  UTF8

    orm:
        auto_mapping: true
        mappings:
            translatable:
                type: annotation
                # Camino absoluto
                dir: /usr/share/php/Gedmo/Translatable/Entity/MappedSuperclass
                # Configuración por proyectos
                # dir: %kernel.root_dir%/../vendor/Gedmo/Translatable/Entity/MappedSuperclass
                prefix: GedmoTranslatableEntity
                alias: Gedmo
...

Comprobar que el comportamiento translatable esté habilitado

$ php app/console doctrine:mapping:info
...
[OK]   Gedmo/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation
[OK]   Gedmo/Translatable/Entity/MappedSuperclass/AbstractTranslation
...

Ponemos las siguientes clases en el DIR Entity

Nótese el uso de las anotaciones Gedmo
Entidad Category

<?php
namespace CompanyName\BundleName\Entity;
 
use Doctrine\Common\Collections\ArrayCollection;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="category")
 * @Gedmo\TranslationEntity(class="Acme\DemoBundle\Entity\CategoryTranslation")
 */
class Category
{
    /**
     * @ORMColumn(type="integer")
     * @ORMId
     * @ORMGeneratedValue
     */
    private $id;

    /**
     * @GedmoTranslatable
     * @ORMColumn(length=64)
     */
    private $title;

    /**
     * @GedmoTranslatable
     * @ORMColumn(type="text", nullable=true)
     */
    private $description;

    /**
     * @ORMOneToMany(
     *   targetEntity="CategoryTranslation",
     *   mappedBy="object",
     *   cascade={"persist", "remove"}
     * )
     */
    private $translations;

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

    public function getTranslations()
    {
        return $this->translations;
    }

    public function addTranslation(CategoryTranslation $t)
    {
        if (!$this->translations->contains($t)) {
            $this->translations[] = $t;
            $t->setObject($this);
        }
    }

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

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }

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

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

    public function __toString()
    {
        return $this->getTitle();
    }
}

Entidad CategoryTranslations

<?php
namespace CompanyName\BundleName\Entity;
 
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="category_translations",
 *     uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={
 *         "locale", "object_id", "field"
 *     })}
 * )
 */
class CategoryTranslation extends AbstractPersonalTranslation
{
    /**
     * Convinient constructor
     *
     * @param string $locale
     * @param string $field
     * @param string $value
     */
    public function __construct($locale, $field, $value)
    {
        $this->setLocale($locale);
        $this->setField($field);
        $this->setContent($value);
    }

    /**
     * @ORMManyToOne(targetEntity="Category", inversedBy="translations")
     * @ORMJoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $object;
}

Ver las entidades mapeadas

$ php app/console doctrine:mapping:info
...
[OK]   Gedmo/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation
[OK]   Gedmo/Translatable/Entity/MappedSuperclass/AbstractTranslation
[OK]   Acme/DemoBundle/EntityCategoryTranslation
[OK]   Acme/DemoBundle/EntityCategory

Crear BD y generar el schema

$ php app/console doctrine:database:create

Estructura de las tablas

Si inspeccionamos las estructuras de las tablas veriamos lo siguiente
category

+-------------+-------------+------+-----+---------+----------------+
| Field       | Type        | Null | Key | Default | Extra          |
+-------------+-------------+------+-----+---------+----------------+
| id          | int(11)     | NO   | PRI | NULL    | auto_increment |
| title       | varchar(64) | NO   |     | NULL    |                |
| description | longtext    | YES  |     | NULL    |                |
+-------------+-------------+------+-----+---------+----------------+

category_translations
object_id = id correspondiente de la tabla category
field = campos que ha sido declarados como translatable en category (title, description)
content = valor de los campos translatable

+-----------+-------------+------+-----+---------+----------------+
| Field     | Type        | Null | Key | Default | Extra          |
+-----------+-------------+------+-----+---------+----------------+
| id        | int(11)     | NO   | PRI | NULL    | auto_increment |
| object_id | int(11)     | YES  | MUL | NULL    |                |
| locale    | varchar(8)  | NO   | MUL | NULL    |                |
| field     | varchar(32) | NO   |     | NULL    |                |
| content   | longtext    | YES  |     | NULL    |                |
+-----------+-------------+------+-----+---------+----------------+

Consultas de tablas internacionalizadas

$dql = 'SELECT c.title, c.descriptioon from BundleName:Category c';
 return $this->_em->createQuery($dql)
                    ->setHint(
                            DoctrineORMQuery::HINT_CUSTOM_OUTPUT_WALKER,
                            'GedmoTranslatableQueryTreeWalkerTranslationWalker'
                    )
                    ->getResult();

Lecturas recomendadas
Proyecto Symfony
Proyecto Doctrine
DoctrineExtensions

2 comentarios en “Configurando DoctrineExtensions en Symfony 2”

  1. Jorge Romeo Salazar

    Hola amigo, lo primero de todo, muchas gracias por compartir tus conocimientos. Lo siguiente, hay alguna manera de indicarle a través de la web que traduzca al idioma elegido? Gracias

  2. Sip, en el Routing de tu applicacion le puedes pasar el paramtero locale algo asi como:
    Mi_routing:
    pattern: /{_locale}/traducir
    # Locale por defecto: ES
    defaults: { _controller: MiBundle:MiControlador:miActio, _locale: es}

    Luego puedes llamar a tu app usando una url como: http://midominio/en/traduccir

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.