Internacionalización de nuestra aplicación web o i18N en symfony2 con JMSTranslationBundle

El post de hoy tratará sobre como traducir nuestra aplicacion web hecha con Symfony 2 a distintos idiomas y no morir en el intento. (Que va, es mucho mas sencillo ya vereis…)

Lo primero, voy a usar un bundle muy famoso de los que más soporte tienen y que me gusta mucho porque aparte de funcionar muy bien, nos ofrece un panel donde poder añadir o editar traducciones que la verdad es que es muy cómodo y da mucha libertad si en nuestro equipo tuviéramos a personal dedicado a las traducciones, ya que les podríamos asignar un determinado ROL para que accedan al panel a trabajar todas las traducciones de nuestra aplicacion web.

Primeros pasos

1- Instalar bundle/s

>> composer require “jms/translation-bundle”

> composer require “jms/i18n-routing-bundle”

2- Configurar los bundles

2.1 Creamos un nuevo fichero jms_translations.yml, lo incluimos en nuestro config.yml y dentro añadimos todos los bundles que contendrán traducciones:

jms_translation:
    configs:
        app:
            dirs: ["%kernel.root_dir%", "%kernel.root_dir%/../src"]
            output_dir: "%kernel.root_dir%/Resources/translations"
            ignored_domains: ["routes"]
            output_format:  yml
            excluded_dirs: ["cache, data, logs"]
            extractors: ["jms_i18n_routing"]
        app_bundle:
            dirs: ["%kernel.root_dir%", "%kernel.root_dir%/../src"]
            output_dir: "%kernel.root_dir%/Resources/FOSUserBundle/translations/translations"
            extractors: ["jms_i18n_routing"]
        ...etc.

2.2 Creamos otro fichero llamado jms_i18n_routing.yml, lo incluimos en el config.yml y añadimos:

jms_i18n_routing:
    default_locale: "%locale%"
    locales: "%languages%"
    strategy: prefix_except_default

* Tendremos que definir en nuestro parameters.yml el ‘local’ y los lenguages que ofreceremos disponibles.

3- Habilitar dichos bundles en nuestro AppKernel.php

new JMS\TranslationBundle\JMSTranslationBundle(),
new JMS\I18nRoutingBundle\JMSI18nRoutingBundle(),

4- Crear una clase LocaleListener dentro de AppBundle/Listener/ que en cada petición revise si llega un parámetro llamado ‘_locale’ para que ajuste el locale en la session, tal como dice la documentación oficial de Symfony.

<?php
namespace efor\CoreBundle\Listener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class LocaleListener implements EventSubscriberInterface
{
    private $defaultLocale;

    public function __construct($defaultLocale = 'es')
    {
        $this->defaultLocale = $defaultLocale;
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        if (!$request->hasPreviousSession()) {
            return;
        }

        // try to see if the locale has been set as a _locale routing parameter
        if ($locale = $request->attributes->get('_locale')) {
            $request->getSession()->set('_locale', $locale);
        } else {
            // if no explicit locale has been set on this request, use one from the session
            $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            // must be registered after the default Locale listener
            KernelEvents::REQUEST => array(array('onKernelRequest', 15)),
        );
    }
}

5- Declaramos el servicio en nuestro app/config/services.yml

services:
    ...
    app.locale_listener:
        class: efor\CoreBundle\Listener\LocaleListener
        arguments: ['%kernel.default_locale%']
        tags:
            - { name: kernel.event_subscriber }

6- Ahora tendremos que crear las carpetas de traducciones dentro de cada bundle para añadir las traducciones de cada idioma tal que así(en mi caso tengo 3 idiomas, entonces un fichero por cada idioma):

estructura de carpetas traducciones

7- Dentro de cada fichero ‘.yml’ añadiremos las traducciones en el siguiente formato, agrupando las cosas comunes y que tengan sentido:

organization:
    title: 'Organizaciones'
    create: 'Crear organización'
    list: 'Lista de organizaciones'
    add_course: 'Añadir curso a organización'
logout: 'Cerrar sesión'

8- Ahora solo nos faltaría añadir el típico selector de idiomas en nuestro ‘header’ o ‘footer’ para que pueda cambiar el ‘locale’ de la aplicacion y listo(aun no del todo):

<a href="{{ url('dashboard_homepage', {'_locale': 'es'}) }}">Español</a> |
<a href="{{ url('dashboard_homepage', {'_locale': 'va'}) }}">Valenciano</a> |
<a href="{{ url('dashboard_homepage', {'_locale': 'en'}) }}">Ingles</a>

Añadimos un parámetro a la URL para que el LocaleListener lo recoja y lo setee en la session del usuario y asi, puedan funcionar las traducciones por arte de magia.

Y listo… ¿¡Espera no, aún no deberia funcionar…¡? Y de esto me dado cuenta después de 10 minutos de revisarlo todo de arriba a abajo¡¡¡

>> sudo rm -rf app/cache/* app/sessions/*

Si no borras la cache y cierras la session que pudieras tener abierta, no funcionaran las traducciones ni se añadirá el locale en la URL indicando que locale tienes ¡

Documentacion:

-http://jmsyst.com/bundles/JMSTranslationBundle

-http://jmsyst.com/bundles/JMSI18nRoutingBundle

Configurar el panel de traducciones

Como os he comentado antes, este magnifico bundle viene con un panel para administrar las traducciones que tenemos metidas en los ficheros. Para configurar el acceso a este panel, lo haremos de la siguiente forma:

1-  Instalamos el bundle que requiere dicho panel:

> composer require “jms/di-extra-bundle”

2- Activamos el bundle en AppKernel.php como nos dicen en la documentación del bundle:

new JMS\DiExtraBundle\JMSDiExtraBundle($this),
new JMS\AopBundle\JMSAopBundle(),

3- Importamos en el routing_dev.yml el bloque(solo se podrá acceder desde el entorno de desarrollo):

JMSTranslationBundle_ui:
    resource: "@JMSTranslationBundle/Controller/"
    type:     annotation
    prefix:   /_trans

*Si queremos añadirle seguridad de acceso, podemos añadir la siguiente configuración en el security.yml

4- Ahora podemos acceder a la ruta /_trans y veremos el panel que de forma automática nos ofrece el bundle JMS. Puede que nos aparezca un mensaje de error diciéndonos que los archivos no tienen permisos de escritura, por lo que tendremos que darles esos permisos a mano, o crearnos este pequeño shell-script para darle permisos a TODOS a la vez.

#!/bin/bash

find ../ -type f -name "*.es.yml" -exec chmod 777 {} \;
find ../ -type f -name "*.en.yml" -exec chmod 777 {} \;
find ../ -type f -name "*.va.yml" -exec chmod 777 {} \;

Si queréis mas informacion, podéis visitar los enlaces que he puesto arriba de documentación.

Si os ha gustado el post, no dudéis en compartirlo en redes sociales.

Hasta la próxima SymfonyDevs ¡

Doctrine Migrations Bundle o actualizaciones de datos incrementales

Uno de los problemas que surgen cuando usamos DataFixtures o datos de pruebas es que creamos muchas fixtures para que la base de datos tenga la informacion necesaria antes de lanzar la aplicacion a producción y que pueda funcionar todo a la perfección para aquellas tablas  que solo contienen informacion acotada para campos <select> por ejemplo. Hasta aquí todo bien, es el procedimiento normal.

Ahora bien, imagínate que al cabo del tiempo y una vez has desplegado tu aplicacion en producción, necesitas modificar un valor de un campo y no puedes borrar el esquema de la base de datos porque obviamente ya existen datos que no podemos perder de nuestros usuarios etc.

Aquí entra en juego el bundle de Doctrine para hacer “migraciones” incrementales y os voy a explicar brevemente como usarlo.

-Instalar bundle

>>php composer.phar require doctrine/doctrine-migrations-bundle

-Inicializar bundle(AppKernel.php)

new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),

-Crear migraciones

>> php app/console doctrine:migrations:generate

este comando lo que hace basicamente es crear una clase PHP dentro del directorio por defecto app/DoctrineMigrations/ cuyo nombre empieza por Version y añade la fecha y hora del momento de creacion. Ademas, la clase incluye 2 metodos up() y down() donde implementaremos el cambio(up) y la forma de deshacerlo(down) por si quisieramos volver atras. Podemos implementar la interfaz para hacer uso del contenedor de dependencias.

-Ejemplo:

class Version20160523164637 extends AbstractMigration implements ContainerAwareInterface
{
    /** @var ContainerInterface $container */
    private $container;

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

    /**
     * @param Schema $schema
     */
    public function up(Schema $schema)
    {
        // this up() migration is auto-generated, please modify it to your needs
        $documentationTypeManager = $this->container->get('documentation_type_manager');
        $docTypeDni = $documentationTypeManager->findOneByName('DNI');

        if($docTypeDni){
            $docTypeDni->setName('DNI/NIF');
            $documentationTypeManager->persistAndFlush($docTypeDni);
        }
    }

    /**
     * @param Schema $schema
     */
    public function down(Schema $schema)
    {
        // this down() migration is auto-generated, please modify it to your needs
        $documentationTypeManager = $this->container->get('documentation_type_manager');
        $docTypeDni = $documentationTypeManager->findOneByName('DNI/NIF');

        if($docTypeDni){
            $docTypeDni->setName('DNI');
            $documentationTypeManager->persistAndFlush($docTypeDni);
        }
    }
}

Aqui podemos hacer cambios de 3 formas distintas:

  • Usando servicios que trabajan con objetos directamente(Managers)
  • Usando el QueryBuilder del EntityManager
  • Usando el metodo $this->addSql(‘UPDATE table SET x=1’); . Esto vendra bien si tenemos procedimientos almacenados en nuestra BD y queremos llamarlos.

-Ejecutar una migración

>> php app/console doctrine:migrations:migrate

Si todo es correcto, a continuación nos aparecerá lo siguiente:

Ejecucion de una migracion

Si investigamos en la base de datos, nos habrá creado una nueva tabla donde aparecen las versiones que tenemos y que hemos aplicado en nuestra aplicación.

-Ver el estado de las migraciones

>> php app/console doctrine:migrations:status

Con este comando podemos ver la información de las migraciones en nuestra aplicación así:

estado

-Desahacer una migración

>> php app/console doctrine:migrations:execute YYYYMMDDHHIISS –down

De esta forma deshacemos una migración cambiando YYYYMMDDHHIISS por el valor que pone en CurrentVersion del comando status. Por eso es muy importante implementar el método down() de la clase, para que sepa como deshacer una migración y que cambios tendría que aplicar para que este como estaba antes de hacer la migración.

Resumen

Con esta herramienta tendremos un histórico de aquellos cambios en los datos de nuestra esquema que han sufrido cambios a posteriori de instalar nuestros fixtures al desplegar nuestra aplicacion. También sabremos quien a cambiado que, ya que al crearse una nueva clase en nuestro proyecto, sabremos quien la ha creado y el motivo.

Documentación

Si os ha gustado, no dudéis en compartir este post.

Saludos SymfoyDevs ¡

Enviar email desde SwiftMailer con Hotmail

Despues de buscar informacion por todos los lados no he encontrado apenas documentacion acerca de configurar SwiftMailer para el envio de emails desde nuestra cuenta de Hotmail.

La unica forma de hacerlo que he hecho funcionar es la siguiente:

– En el action del controlador donde queremos enviar el email debemos hacerlo así:

$transport = \Swift_SmtpTransport::newInstance(‘smtp.live.com’, 587, ‘tls’)
->setUsername(‘USUARIO@hotmail.com’)
->setPassword(‘PASSWORD’);

$mailer = \Swift_Mailer::newInstance($transport);
$message = \Swift_Message::newInstance()
->setSubject($subject)
->setFrom($sendFrom)
->setTo($sendTo)
->setBody($body);

$mailer->send($message);

Desconozco de que forma se puede meter esta configuración en el archivo config.yml ya que despues de hacer algunas pruebas no me ha llegado a funcionar. Si alguien lo prueba y lo consigue, que lo publique en los comentarios y lo añadire a esta entrada.

Saludos SymfonyDevs ¡

Compartid este articulo ¡

Formularios: Coleccion no mapeada en la entity usando un DTO

Si no has leido mi anterior “trick” acerca del uso de los DTOs en los formularios, deberias visitar antes este link.

En este ejemplo, necesitaba varios campos que no estan mapeados en la entity de la cual nace el formulario. Para ello, no he tenido mas remedio que añadir un nuevo campo “No mapeado”. Este campo es una coleccion, por lo que se puede añadir o eliminar elementos.

De acuerdo con esto, en mi formulario añado ese campo asi:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        //OTROS CAMPOS DE LA ENTIDAD
        //...
        ->add('enrolUserToOrg', CollectionType::class, array(
             'label' => false,
             'entry_type' => new EnrolUserToOrgType(),
             'mapped' => false,
             'allow_add' => true,
             'attr' => array(
                     'class' => 'enrolUserOrgWrapper'
             )
      ));
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'efor\AppBundle\Entity\Organization',
    ));
}

Os he marcado en color aquello que hay que tener en cuenta.

  • CollectionType::class: Definimos el campo de tipo coleccion para indicarle a Symfony que podremos añadir/quitar elementos
  • ‘entry_type’ => new EnrolUserToOrgType() : Instanciamos el sub-formulario que contendra los campos para cada fila de la coleccion
  • ‘mapped’ => false : Le indicamos que no esta mapeado en la entity para que no valide nada
  • ‘allow_add’ => true : Indicamos que podremos añadir elementos

Ahora lo que haremos sera crear un DTO(en otros sitios lo llaman ‘Domain Model’) que contendra los elementos del “subformulario” el cual hablaba antes y que representaran una fila de cada coleccion. Este DTO contendra 2 campos: usuario y rol, y quedaria asi:

<?php
namespace efor\AppBundle\Model;

use efor\AppBundle\Entity\Role;
use efor\UsuarioBundle\Entity\User;

class EnrolUserToOrg
{
    /** @var User $user */
    private $user;

    /** @var Role $role */
    private $role;

    /**
     * @return User
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * @param User $user
     */
    public function setUser($user)
    {
        $this->user = $user;
    }

    /**
     * @return Role
     */
    public function getRole()
    {
        return $this->role;
    }

    /**
     * @param Role $role
     */
    public function setRole($role)
    {
        $this->role = $role;
    }
}

Ahora creamos el subformulario y le indicamos que la clase en la que se tiene que basar es el anterior DTO:

public function buildForm(FormBuilderInterface $builder, array $options)
{
        $builder
            ->add('user', EntityType::class, array(
                'label' => false,
                'class' => 'UsuarioBundle:User',
                'empty_value' => 'Selecciona el/los responsable/s',
                'query_builder' => function (EntityRepository $er) {
                    return $er->createQueryBuilder('users');
                },
                'attr' => array(
                    'class' => 'col-md-5 select-user'
                )
            ))
            ->add('role', ChoiceType::class, array(
                'label' => false,
                'empty_value' => 'Seleccionar un Rol',
                'choices' => $this->roleChoices,
                'attr' => array(
                    'class' => 'col-md-5 select-role'
                )
            ))
            ->add('Quitar', ButtonType::class, array(
                'attr' => array(
                    'class' => 'col-md-2 btn-danger button-remove-enrol'
                )
            ))
       ;
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'efor\AppBundle\Model\EnrolUserToOrg'
    ));
}

Llegados a este punto, vamos a por la vista. Como es una coleccion he seguido la guia de Symfony para pintar esos campos en el formulario y la he ajustado a mi gusto, y quedaria asi:

<ul class="enrolUserToOrg" 
    data-prototype="{{ form_widget(form.enrolUserToOrg.vars.prototype) | e }}">
</ul>

Despues añadiriamos la logica necesaria en jQuery para poder añadir y eliminar filas de esa coleccion:

var $collectionHolder;

// setup an "add a enrolUserToOrg" link
var $buttonAdd = '<span class="input-group-btn">'+
    '<a class="btn btn-success btn-add add_enrol_link"><span class="glyphicon glyphicon-plus"></span> ' +
    '<span>Añadir nuevo responsable</span></a>'+
    '</span>';
var $addLink = $($buttonAdd);
var $newLinkLi = $('<li class="text-center"></li>').append($addLink);

jQuery(document).ready(function() {
    // Get the ul that holds the collection of authorized
    $collectionHolder = $('ul.enrolUserToOrg');

    // add the add li object to the tags ul
    $collectionHolder.append($newLinkLi);

    // count the current form inputs we have (e.g. 2), use that as the new
    // index when inserting a new item (e.g. 2)
    $collectionHolder.data('index', $collectionHolder.find(':input').length);

    $addLink.on('click', function(e) {
        // prevent the link from creating a "#" on the URL
        e.preventDefault();

        // add a new tag form (see next code block)
        addAuthorizedUserForm($collectionHolder, $newLinkLi);
    });

    //initialize the collection with one element
    if($collectionHolder.find('li').length == 1){
        $addLink.click();
    }

    $(document).on('click', '.button-remove-enrol', function () {
        var contElems = $collectionHolder.find('li').length - 1;

        if(contElems > 1 ){
            $(this).closest('li').remove();
        }
    });

});

function addAuthorizedUserForm($collectionHolder, $newLinkLi)
{
    // Get the data-prototype explained earlier
    var prototype = $collectionHolder.data('prototype');

    // get the new index
    var index = $collectionHolder.data('index');

    // Replace '__name__' in the prototype's HTML to
    // instead be a number based on how many items we have
    var newForm = prototype.replace(/__name__/g, index);

    // increase the index with one for the next item
    $collectionHolder.data('index', index + 1);

    // Display the form in the page in an li, before the "Add a tag" link li
    var $newFormLi = $('<li></li>').append(newForm);
    $newLinkLi.before($newFormLi);
}

Y para la parte del controlador, podremos recoger los datos de la coleccion del campo no mapeado de la siguiente forma:

/**
 * @Route("/create", name="organization_create")
 */
public function createOrganizationAction(Request $request)
{
    $organization = new Organization();
    $form = $this->createForm(new OrganizationType(), $organization, array(
        'action' => $this->generateUrl('organization_create'),
    ));

    $form
        ->add('ok', 'submit', array(
            'attr' => array('class' => 'btn btn-primary pull-right'),
            'label' => 'Crear',
        ))
        ->add('cancel', 'button', array(
            'attr' => array('class' => 'return-button pull-right'),
            'label' => 'Volver atrás',
        ));

    $form->handleRequest($request);
    if ($form->isValid()) {
        $usersEnroled = $form->get('enrolUserToOrg')->getData();

        //DO SOMETHING YOU NEED WITH THIS ARRAY
        //...example
        // /** @var EnrolUserToOrg $usersEnroled */
        //foreach($usersEnroled as $userEnroled){
             //$user = $userEnroled->getUser();
             //$role = $userEnroled->getRole();
             //...
        //}  
        
       $this->addFlashMessage(CoreController::FLASH_TYPE_SUCCESS, 'La organización ha sido creada con exito'); return $this->redirect($this->generateUrl('organization_list')); } return $this->render('AppBundle:Organization:createOrganization.html.twig', array( 'form' => $form->createView() )); }

De esta forma, nuestra accion sera la que inicialice y procese el formulario a la vez, y asi podremos recoger la coleccion no mapeada y hacer lo que necesitemos.

Os dejo un pequeño video de como quedaria y el efecto de añadir / quitar elementos de la coleccion.

Coleccion no mapeada en Symfony 2

Espero que os guste y por favor, compartid este “trick”.

Cualquier duda, podeis consultarme o mirar en la documentación que os he puesto en los links de arriba y aqui.

Saludos Sf-Devs¡¡

Formularios: Sobrescribir template de un widget – Personalización

En esta ocasión os voy a hablar sobre como sobrescribir un form_widget de los que se usa en los formularios para personalizarlo a nuestro gusto.

En mi caso, tengo configurado para que Symfony coja las plantillas definidas en sus Form Themes,las cuales meten elementos HTML definidos para que se ajusten a un estilo concreto.

Eso se puede configurar en el app/config/config.yml:

twig:
   //...
    form_themes:
        - 'bootstrap_3_layout.html.twig'

Aqui podemos elegir entre varios form_themes que Symfony trae pre-definidos:

Symfony comes with some built-in form themes that define each and every fragment needed to render every part of a form:

form_div_layout.html.twig, wraps each form field inside a element.

form_table_layout.html.twig, wraps the entire form inside a element and each form field inside a element.

bootstrap_3_layout.html.twig, wraps each form field inside a element with the appropriate CSS classes to apply the default Bootstrap 3 CSS framework styles.

bootstrap_3_horizontal_layout.html.twig, it’s similar to the previous theme, but the CSS classes applied are the ones used to display the forms horizontally (i.e. the label and the widget in the same row).

foundation_5_layout.html.twig, wraps each form field inside a element with the appropriate CSS classes to apply the default Foundation CSS framework styles.

En mi aplicacion me ocurre que esto me inserta una etiqueta ‘div class=form-control’ al insertar un elemento de mi FormType y justamente necesito que ese “div” no exista para que se alinee todo horizontalmente, sino la clase form-control me coge todo el ancho y no permite elementos contiguos.

Para ello, no me queda mas remedio que buscar el form_theme y en concreto el widget que “pinta” ese div y “sobrescribirlo”.

vendor/symfony/symfony/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig

{% block form_row -%}
    {{- form_label(form) -}} 
    {{- form_widget(form) -}} 
    {{- form_errors(form) -}}
{%- endblock form_row %}
 Ese es el bloque que mete el div que necesito quitar. Justamente el widget que pinta un row del formulario.

1- Creamos un template Twig en nuestro proyecto y lo llamamos ‘formRowCustom.html.twig’ y en el metemos lo siguiente:

{% block form_row -%}
        {{- form_label(form) -}}
        {{- form_widget(form) -}}
        {{- form_errors(form) -}}
{%- endblock form_row %}

2- A continuacion para poder usar nuestro “widget theme”, nos vamos al formulario donde lo queremos y lo usaremos asi:

{% form_theme form.user 'AppBundle:Form:formRowCustom.html.twig' %}
{{ form_widget(form.user) }}

De esta forma, cuando se vaya a ejecutar la funcion de Twig llamada “form_widget()”, en lugar de “pintar” el estilo del Form Theme, nos mostrara  nuestro estilo personalizado.

Con este “trick” podemos personalizar todos los elementos de los formularios para que se ajusten a nuestro diseño.

Para más información os dejo el enlace de la documentación oficial para que veais mas ejemplos o formas de sobrescribir los elementos de los form themes.

Si te a gustado y lo encuentras util, no dudes en compartir este truco en las redes sociales.

Hasta la proxima Sf dev¡¡

 

Uso del operador IN en consultas DQL de Doctrine y otros operadores SQL

A menudo tengo la necesidad de usar el operador IN dentro del WHERE de una consulta DQL con Doctrine y casi siempre se me olvida como usarlo.

Para ello voy a explicar como usarlo a continuación con un ejemplo:

– Imaginamos que tenemos un array con los posibles valores que queremos seleccionar en la parte del where de la consulta tal que asi:

$filters = array(1, 2, 3, 4, 10, 15, 25);

-Ahora, hacemos nuestra consulta de la siguiente forma:

$users = $qb
    ->addSelect('user')
    ->from('UsuarioBundle:User', 'user')
    ->join('user.userGroup', 'userGroup')
    ->where($qb->expr()->in('userGroup.id', $filters))
    ->getQuery()
    ->getResult();

 De esta forma, usando el expr() del QueryBuilder podemos usar los operadores IN, NOT IN, BETWEEN, MAX, MIN, etc. Podeis ver toda la documentacion y los operadores disponibles en la documentacion oficial de Doctrine sobre el querybuilder:

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/query-builder.html#high-level-api-methods

Ale, espero que os sirva de ayuda.

Saludos Devs¡

Herencia entre entidades y consultas DQL relacionadas

herencia entre entidades y consultas DQL
Herencia entre entidades

Una de las características que tiene Doctrine es que se puede usar la herencia de clases para representar aquellas tablas que heredan de una tabla “base”.

Pongamos un ejemplo:

  • Tenemos una tabla Rol (“padre”) y de ella heredan 3 tipos de roles distintos(RolPlataforma, RolOrganizacion, RolEdicion), pero que comparten los mismos atributos que la tabla Rol “padre”, excepto que se cada “entidad” hija se relacionan con entidades diferentes. Aqui el diagrama UML que representa el caso:
  • umlAhora la parte de las entidades:
    • Entity Padre Role
  • /**
     * @ORM\Table(name="role")
     * @ORM\InheritanceType("JOINED")
     * @ORM\DiscriminatorColumn(name="discr", type="string")
     * @ORM\DiscriminatorMap({
     *     "role" = "Role",
     *     "role_edition" = "RoleEdition",
     *     "role_org" = "RoleOrganization",
     *     "role_plat" = "RolePlatform"
     * })
     */
    class Role 
    {
     const ROLE_EDITION = 'RoleEdition';
     const ROLE_ORGANIZATION = 'RoleOrganization';
     const ROLE_PLATFORM = 'RolePlatform';
     const ROLE = 'Role';
    
     /**
     * @ORM\Id
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\GeneratedValue()
     */
     protected $id;
    
     /**
     * @ORM\Column(name="rol_borrado", type="boolean", nullable=false)
     */
     private $deleted = false;
    
     /**
     * @ORM\ManyToMany(targetEntity="efor\UsuarioBundle\Entity\User", mappedBy="roleUser")
     */
     private $user;
    
    //...
    
    //OTROS CAMPOS NO RELEVANTES PARA EL EJEMPLO
    //SETTERS Y GETTERS Y OTROS METODOS.
  • Con la anotacion estamos indicando que se va a hacer herencia de tablas y que los tipos de discriminador van a ser los que pone en el “DiscriminatorMap”. De esta entidad nunca se van a instanciar objetos, siempre se instanciaran objetos de las entidades hijas, por lo que podriamos definirla como Abstracta.
  • Entity descendiente RolePlatform
    • /**
       * @ORM\Table(name="rol_plataforma")
       * @ORM\Entity
       */
      class RolePlatform extends Role
      {
          protected $discr = self::ROLE_PLATFORM;
      
          /**
           * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Platform", inversedBy="rolePlatform")
           */
          private $platform;
      
          //otros campos
      }
  • Entity descendiente RoleOrganization
  • /**
     * @ORM\Table(name="rol_organizacion")
     * @ORM\Entity
     */
    class RoleOrganization extends Role
    {
        protected $discr = self::ROLE_ORGANIZATION;
    
        /**
         * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Organization", inversedBy="roleOrganization", cascade={"persist"})
         */
        private $organization;
    
        //otros campos
    }
  • Entity descendiente RoleEdition
  • /**
     * @ORM\Table(name="rol_edicion")
     * @ORM\Entity
     */
    class RoleEdition extends Role
    {
        protected $discr = self::ROLE_EDITION;
    
        /**
         * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Edition", inversedBy="roleEdition")
         */
        private $edition;
    
        //otros campos
    }

Si os fijas cada entidad hija tiene una relacion distinta a otra entidad, por eso necesitabamos usar la herencia entre entidades, porque podemos crear objetos que se relacionan con entidades distintas en funcion del tipo de relacion que queramos tener.

En la documentación de Doctrine explica los tipos de herencia entre entidades y algunas consideraciones a tener en cuenta cuando se usa la herencia. Os dejo aqui la documentacion:

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#class-table-inheritance

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#single-table-inheritance

-http://stackoverflow.com/questions/7504680/doctrine-2-how-to-write-a-dql-select-statement-to-search-some-but-not-all-the

Consultas DQL filtradas por el campo discriminador:

Una de las dudas que surgen al trabajar con la herencia en Doctrine es saber como puedes filtrar las consultas por el campo discriminatorio. En mi caso, necesitaba filtrar los resultados por aquellos objetos que eran de una determinada clase del discriminador(RoleOrganization) y despues de buscar, encontre que se puede hacer algo asi:

public function findUsersFilteredByRoleOrganization(User $loggedUser, Role $selectedRole)
{
    $qb = $this->getEntityManager()->createQueryBuilder();
    $usersResult = $qb
    ->select('user')
    ->from('UsuarioBundle:User', 'user')
    ->join('user.roleUser', 'roleUser')
    ->where('roleUser INSTANCE OF AppBundle:RoleOrganization')
    ->getQuery();

    return $usersResult->getResult();
}

Si os dais cuenta, hemos hecho uso del INSTANCE OF dentro del Where de la consulta DQL, la cual se traduce por esto en SQL:

SELECT t0_.*
INNER JOIN rol_usuario t2_ ON t0_.id = t2_.user_id
INNER JOIN rol t1_ ON t1_.id = t2_.role_id
LEFT JOIN rol_edition t3_ ON t1_.id = t3_.id
LEFT JOIN rol_org t4_ ON t1_.id = t4_.id
LEFT JOIN rol_plataforma t5_ ON t1_.id = t5_.id
WHERE t1_.discr IN (‘role_org’)

De esta forma solo obtenemos los objetos del tipo “RoleOrganization” que queremos.

Espero que os sirve de ayuda, ya que la documentacion de Doctrine no es muy explicita en este aspecto y no lo explica muy bien que digamos.

Recordad compartir este articulo para que llegue al maximo de amigos de Symfony.

Saludos ¡¡