Propagar parametros custom de un FormType a un FormType Embebido

Al hilo del mi ultimo post acerca de pasar parametros custom a un FormType desde un controlador, para por ejemplo rellenar un select con valores que vienen de otro sitio, me a surgido la duda siguiente:

¿Es posible propagar ese campo custom a un formulario embebido dentro del mismo FormType? Es decir, imaginad que tenemos un FormType con una serie de campos y dentro de ese FormType tenemos otro FormType con otros campos el cual un de ellos tambien se rellena con información externa.

Ejemplo:

CreateIncidentType:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $incidentType = $options['incident_type'];
    $incidentTypesOptions = array('' => '');
    /** @var IncidentType $item */
    foreach ($incidentType as $item) {
     $incidentTypesOptions[$item->getName()] = $item->getCode();
    }

    $builder
         ...OTROS CAMPOS
         ->add('visit_delivery', VisitType::class, array(
              'label' => 'Visita de Entrega',
              'incident_type' => $incidentType,
              'attr' => array(
                     'class' => 'form-control visit-delivery hide'
               ),
              'label_attr' => array(
                    'class' => 'hide'
              )
     ));
}
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => CreateIncident::class,
        'incident_type' => null
    ));
}

He marcado en color las cosas a tener en cuenta. En mi caso VisitType es un formulario embebido dentro del CreateIncidentType y que tb recibe un parametro custom. Para ello, hacemos exactamente lo mismo que antes:

VisitType:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $incidentType = $options['incident_type']; 
    $builder
        ->add('visit_type_code', ChoiceType::class, array(
             'label' => 'Tipo',
             'choices' => $incidentType,
             'attr' => array(
                  'class' => 'form-control visit-type-code',
             ),
             'required' => true
         ));
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => Visit::class,
        'incident_type' => null
    ));
}

De esa forma, hemos “propagado” el parametro que recibe el formtype inicial hacia el formulario embebido para usarlo dentro de el.

Por cierto, recordad que en el controller cuando creais el Form teneis que hacerlo asi:

$incidentModel = new CreateIncident();
$incidentForm = $this->createForm(CreateIncidentType::class, $incidentModel, array(
 'incident_type' => $incidentTypes
));

Espero que os sirva de ayuda o os de una idea de como enfocarlo.

Si te a gustado, ya sabes retuit, comparte y comenta ¡¡

Saludos Devs ¡¡¡

Enviar parametros a un FormType desde el Controller

El truco de hoy consiste en enviar información a un formulario desde un controlador. En mi caso el problema que tengo es el siguiente:

Tengo un formulario en el que los valores de un campo select (ChoiceType) los recojo de un servicio de una API Rest y se los tengo que enviar al formType para rellenar los valores(choices) de un campo de tipo ChoiceType.

La forma de hacerlo seria la siguiente:

Controller
$form = $this->createForm(ResumeCartType::class, $resumeCart, array(
    'pickupTime' => $timetablePickup->getPickup(),
    'deliveryTime' => $timetableDelivery->getDelivery()
));

En mi caso le envio 2 parametros (pickupTime y deliveryTime) cuyos valores son de tipo ArrayCollection con la informacion. Despues habria que recogerlos en el formulario de la siguiente forma:

FormType(ResumeCartType)
class ResumeCartType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
 $resolver->setDefaults(array(
 'data_class' => 'Appbundle\Model\ResumeCart',
 'pickupTime' => null,
 'deliveryTime' => null
 ));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
    /** @var ArrayCollection $pickupRanges */
    $pickupRanges = $options['pickupTime'];

    //Operaciones para formar el array asociativo para 
    //el campo ChoiceType
    $pickupValues = array();
    /** @var Range $range */
    foreach ($pickupRanges as $range) {
        array_push($pickupValues, array($range->getTimeSlotCode() => $range->getTimeSlotCode()));
    }

    /** @var ArrayCollection $deliveryRanges */
    $deliveryRanges = $options['deliveryTime'];    
    $builder
            ...//Otros campos de tu modelo
            ->add('time_pickup', ChoiceType::class, array(
                'choices' => $pickupValues,
                'expanded' => true,
                'multiple' => false,
                'required' => true
            ))
}

Lo primero es definir esos parametros en el metodo configureOptions() y despues podemos recogerlos usando el parametro $options[‘clave’], hacer lo que necesitemos y despues pasarselo al campo que necesitemos. En mi caso recibo un ArrayCollection que tengo que transformar a un array simple asociativo(clave => valor) para pasarselo al ChoiceType ya que este objeto necesita ese formato para formar correctamente el elemento <select> en la vista.

De esta forma tan simple, podemos enviarle los parametros que necesitemos a nuestros formularios. Existen muchas mas formas, incluso recuperando datos de una Entidad usando traits, pero eso ya viene muy bien explicado en la documentacion oficial de Symfony.

Espero que os sirva y si teneis sugerencias, no dudeis en postearlas. Seguro que hay muchas mas formas de hacer estas cositas, y incluso mejores supongo, pero esta es la mas sencilla que he encontrado.

Un saludo SymfonyDevs ¡¡

Edito:

Existe otra opcion oficial en Symfony que desconocia hasta ayer que soluciona esto sin recorrer la coleccion manualmente. En nuestro FormType debemos indicarle el choice_label y choice_value y Symfony lo hace solo por nosotros.

->add('incident_type_code', ChoiceType::class, array(
    'label' => 'Tipo de Incidencia',
    'choices' => $options['incident_types'],
    'choice_label' => 'name',
    'choice_value' => 'code',
    'placeholder' => '',
    'attr' => array(
        'class' => 'form-control incident-type-code',
    ),
    'required' => true
))

De esa forma nos ahorramos el bucle y seguimos fieles a Symfomy.

Saludos ‘¡

Cambiar idioma/locale de la aplicación según las preferencias del usuario

Preferencias de idioma

Este post es la segunda parte del anterior que escribí relacionado con la internacionalización de nuestra aplicacion web y tratará de como traducir nuestra aplicación según el idioma que el usuario haya seleccionado en su configuración personal.

Para ello asumiremos que tenemos una entidad UserPreferences relacionada con nuestra entidad User con una relacion OneToOne, y otra entidad Language relacionada con UserPreferences con una relación ManyToOne, donde guardaremos los posibles “locales” de la aplicación, de la siguiente forma:

User:

/**
 * @ORM\OneToOne(targetEntity="efor\AppBundle\Entity\UserPreference", inversedBy="user", cascade={"persist"})
 */
protected $userPreference;

UserPreference:

/**
 * @ORM\Id
 * @ORM\Column(type="integer", nullable=false)
 * @ORM\GeneratedValue()
 */
private $id;

/**
 * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Language", inversedBy="userPreference", fetch="EAGER")
 */
private $language;

/**
 * @ORM\OneToOne(targetEntity="efor\UsuarioBundle\Entity\User", mappedBy="userPreference")
 */
private $user;

Language:

/**
 * @ORM\Id
 * @ORM\Column(type="integer", nullable=false)
 * @ORM\GeneratedValue()
 */
private $id;

/**
 * @ORM\Column(name="idioma_nombre", type="string", length=50, nullable=false)
 */
private $name;

/**
 * @ORM\Column(name="idioma_abreviatura", type="string", length=4, nullable=true)
 */
private $slug;

/**
 * @ORM\Column(name="idioma_codigo", type="integer", nullable=true)
 */
private $code;

/**
 * @ORM\Column(name="idioma_activo", type="boolean", nullable=false)
 */
private $active = true;

/**
 * @ORM\Column(name="idioma_borrado", type="boolean", nullable=false)
 */
private $deleted = false;

/**
 * @ORM\OneToMany(targetEntity="efor\AppBundle\Entity\UserPreference", mappedBy="language")
 */
private $userPreference;

Todas estas entidades van con sus getters/setters correspondientes.

Montamos el formulario para mostrar los idiomas disponibles que en nuestro caso los tendremos guardados mediante fixtures o migración de datos en la entidad Language. Quedaría así:

class UserPreference extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('language', EntityType::class,array(
                'label' => 'Idioma'
            ));
    }

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

    public function getName()
    {
        return 'usuario_bundle_user_preference';
    }
}

Y este formulario lo añadiremos a nuestro formulario de configuración del perfil del usuario, tal que así:

class ProfileType extends AbstractType
{
    /** @var ObjectManager $manager */
    private $manager;

    /** @var Language $lang */
    private $lang;

    public function __construct(ObjectManager $manager, Language $lang)
    {
        $this->manager = $manager;
        $this->lang = $lang;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, array('label' => 'name'))
            ->add('surnameFirst', TextType::class, array('label' => 'first_name'))
            ->add('surnameSecond', TextType::class, array('label' => 'last_name'))
            ->add('email', EmailType::class, array('label' => 'Email'))
            ->add('plainPassword', 'password', array(
                'label' => 'Nuevo Password',
                'required' => false,
            ))
            ->add('salt', HiddenType::class)
            ->add('photo', new UploadFileType(), array(
                'label' => 'Foto',
                'required' => false,
            ))
            ->add('city', TextType::class, array(
                'label' => 'city',
            ))
            ->add('postalCode', TextType::class, array(
                'label' => 'postal_code',
            ))
            ->add('country', TextType::class, array(
                'label' => 'country',
            ))
            ->add('birthdate', DateType::class, array(
                'label' => 'birthdate',
                'widget' => 'single_text',
                'format' => 'dd/MM/yyyy',
                'attr' => array(
                    'class' => 'form-control input-inline datepicker',
                    'data-provide' => 'datepicker',
                    'data-date-format' => 'dd/mm/yyyy',
                    ),
                )
            )
->add('language', EntityType::class, array(
                'class' => 'AppBundle:Language',
                'data' => $this->lang,
                'label' => 'Idioma',
                'mapped' => false,
                'required' => true,
                'multiple' => false,
                'expanded' => true
            ))
        ;
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'efor\UsuarioBundle\Entity\User'
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'efor_usuariobundle_User';
    }
}

En nuestro caso, este formulario recibe un objeto Language que se encargara de hidratar el campo “no mapeado” de los idiomas que lo pasaremos desde nuestro Controller al formulario de la siguiente forma:

public function editProfileAction(Request $request)
{
    /** @var User $user */
    $user = $this->getUser();
    $em = $this->getDoctrine()->getManager();
    $langUser = $user->getUserPreference()->getLanguage();
    $editProfileForm = $this->createForm(new ProfileType($em, $langUser), $user, array(
        'action' => $this->generateUrl('profile_edit'),
        'method' => 'POST',
    ));

    $editProfileForm
        ->add('update', 'submit', array(
            'attr' => array(
                'class' => 'boton pull-right'
            ),
            'label' => 'update'
        ));

    $editProfileForm->handleRequest($request);
    if ($editProfileForm->isValid()) {
        try {
            // Si el usuario no ha cambiado el password, su valor es null después
            // de hacer el ->bindRequest(), por lo que hay que recuperar el valor original
            if (null == $user->getPlainPassword()) {
                $user->setPassword($passwordOriginalEncripted);
            }

            /** @var Language $language */
            $language = $editProfileForm->get('language')->getData();
            $userPreference = new UserPreference();
            $userPreference->setLanguage($language);
            $userPreference->setUser($user);
            $user->setUserPreference($userPreference);

            $userManager = $this->getUserManager();
            $userManager->updateUser($user);

            $this->addFlashMessage(CoreController::FLASH_TYPE_SUCCES

            return $this->redirect($this->generateUrl('profile_show', array(
                '_locale' => $language->getSlug()
            )));


        } catch (\Exception $e) {
            $editProfileForm->addError(new FormError($e->getMessage()));

            return $this->render('UsuarioBundle:Profile:editProfile.html.twig', array(
                'form' => $editProfileForm->createView(),
            ));
        }
    }

    return $this->render('UsuarioBundle:Profile:editProfile.html.twig', array(
        'form' => $editProfileForm->createView(),
        'form_errors_validation_document' => $editProfileForm->getErrorsAsString(),
    ));
}

Para que funcione el cambio del “locale” de la aplicación, tendremos que pasarle el parámetro “_locale” en la URL.

Siguiendo la receta del libro de Symfony, tendremos que crear un LocaleListener que se encargara de atender cada petición en el evento ‘onKernelRequest’ y comprobar si existe un parámetro que sea ‘_locale’ para cambiar en la session el locale y que la aplicacion se traduzca. Para ello, creamos una carpeta EventListener dentro de nuestro bundle, y dentro creamos la clase LocaleListener, tal que asi:

<?php
namespace efor\CoreBundle\EventListener;

use Stof\DoctrineExtensionsBundle\DependencyInjection\Compiler\SecurityContextPass;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;

class LocaleListener implements EventSubscriberInterface
{
    private $defaultLocale;

    /**
     * LocaleListener constructor.
     *
     * @param string $defaultLocale
     */
    public function __construct($defaultLocale = 'es')
    {
        $this->defaultLocale = $defaultLocale;
    }

    /**
     * @param GetResponseEvent $event
     */
    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', 150)),
        );
    }
}

Después añadimos el listener como un servicio dentro del archivo app/config/services.yml :

services:

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

Ademas, si queremos que cuando el usuario haga login se cambie el idioma de la aplicación, según el idioma que tenga guardado en sus preferencias, debemos hacerlo en otro listener. En mi aplicación, dispongo de un listener que se ejecuta después del login satisfactorio del usuario y es allí donde busco el locale en las preferencias y lo envió.  La documentación para este listener la tenéis aquí.

<?php

namespace efor\UsuarioBundle\Redirection;

use efor\AppBundle\Entity\Role;
use efor\UsuarioBundle\Entity\User;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;

class AfterLoginRedirection implements AuthenticationSuccessHandlerInterface
{
    const DEFAULT_LOCALE = 'es';

    /** @var RouterInterface */
    private $router;

    /**
     * AfterLoginRedirection constructor.
     *
     * @param RouterInterface $router
     */
    public function __construct(RouterInterface $router)
    {
        $this->router = $router;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        //Default redirect
        $response = new RedirectResponse($this->router->generate('fos_user_security_login'));

        if ($token->isAuthenticated()) {
                //Go home with user locale preferences
                /** @var User $user */
                $user = $token->getUser();
                $userPreference = $user->getUserPreference();
                $lang = $userPreference ? $userPreference->getLanguage()->getSlug() : self::DEFAULT_LOCALE;

                $response = new RedirectResponse($this->router->generate('dashboard_homepage', array(
                    '_locale' => $lang
                )));
        }
        // Go login
        return $response;
    }
}

Y tenemos este listener definido como servicio dentro de nuestro bundle, tal que así:

<service id="redirect.after.login" class="efor\UsuarioBundle\Redirection\AfterLoginRedirection">
    <argument type="service" id="router" />
</service>

En resumen, en mi caso solo cambiando el locale en la session, request y translator, no me funcionaba, por lo que he tenido que recurrir a la alternativa de pasar el locale por GET en la url y que el listener fuera el que hiciera el trabajo sucio.

No se si sera mejor o peor forma de hacerlo, el caso es que funciona y muy bien.

Si alguien tiene dudas, o quiere comentar algo, no dudéis en escribirme y lo comentamos.

Por favor, comparte si te a sido útil.

Hasta la vista SymfonyDevs ¡

 

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 ¡

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¡¡

Uso de Data Transfer Objects (DTO) en formularios

Uno de los problemas que a veces nos encontramos en los formularios a menudo, es que necesitamos mostrar campos que no existen en nuestra entidad directamente ya que puede que necesitemos esos campos para construir un determinado objeto en funcion de las opciones que haya elegido.

En mi caso, tengo un formulario en el que aparecen 4 campos de tipo <select> y que son dependientes entre sí, y en funcion de las opciones el usuario tendra asignado unas cosas o otras, y 3 de estos campos no estan mapeados en la Entity.

Para ello, debemos hacer uso de los DTO (Data Transfer Object) que no son mas que “Objetos de Transferencia de Datos” o objetos “pasarela” para recoger la informacion del formulario y hacer a posteriori nuestras cosas.

Enctonces, imaginamos que tenemos una entidad Organizacion pero necesitamos 4 campos mas que no estan en nuestra entity pero que los necesitamos para el formulario:

<?php
namespace myApp\AppBundle\Model;

use Symfony\Component\Validator\Constraints as Assert;

class Enrol
{
    protected $organization;

    protected $plan;

    protected $edition;

    protected $group;

    /**
     * @Assert\NotBlank()
     */
    protected $roleUser;

    /**
     * Enrol constructor.
     */
    public function __construct()
    {
    }

    /**
     * @return mixed
     */
    public function getOrganization()
    {
        return $this->organization;
    }

    /**
     * @param mixed $organization
     */
    public function setOrganization($organization)
    {
        $this->organization = $organization;
    }

    /**
     * @return mixed
     */
    public function getPlan()
    {
        return $this->plan;
    }

    /**
     * @param mixed $plan
     */
    public function setPlan($plan)
    {
        $this->plan = $plan;
    }

    /**
     * @return mixed
     */
    public function getEdition()
    {
        return $this->edition;
    }

    /**
     * @param mixed $edition
     */
    public function setEdition($edition)
    {
        $this->edition = $edition;
    }

    /**
     * @return mixed
     */
    public function getGroup()
    {
        return $this->group;
    }

    /**
     * @param mixed $group
     */
    public function setGroup($group)
    {
        $this->group = $group;
    }

    /**
     * @return mixed
     */
    public function getRoleUser()
    {
        return $this->roleUser;
    }

    /**
     * @param mixed $roleUser
     */
    public function setRoleUser($roleUser)
    {
        $this->roleUser = $roleUser;
    }
}

Este es el modelo en el que se basara nuestro formulario para “hidratar” los campos del objeto que le pasemos.

Nuestro formulario quedaría así:

<?php

namespace myApp\AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class EnrolType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('organization', 'entity', array(
                'label' => 'Seleccionar una Organización',
                'class' => 'AppBundle:Organization',
                'required' => false
            ))
            ->add('plan', 'entity', array(
                'label' => 'Seleccionar un Plan',
                'class' => 'AppBundle:Plan',
                'required' => false
            ))
            ->add('edition', 'entity', array(
                'label' => 'Seleccionar una Edición',
                'class' => 'AppBundle:Edition',
                'required' => false
            ))
            ->add('group', 'entity', array(
                'label' => 'Seleccionar un Grupo',
                'class' => 'AppBundle:Group',
                'required' => false
            ))
            ->add('roleUser', 'entity', array(
                'label' => 'Seleccionar un Rol',
                'class' => 'AppBundle:Role',
                'required' => false
            ));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'myApp\AppBundle\Model\Enrol'
        ));
    }

    public function getName()
    {
        return 'myapp_bundle_enrol_type';
    }
}

Creamos el action:

/**
 * @Route("/organizations-form", name="org_form")
 */
public function orgFormAction(Request $request)
{
    $enrol = new Enrol();
    $form = $this->createForm(new EnrolType(), $enrol, array());
    $form->add('submit', 'submit');

    $form->handleRequest($request);
    if($form->isValid()){
        //Aqui harás lo que necesites con los datos del formulario
        //ya que tienes el DTO $enroll y con sus métodos puedes
        //recoger la informacion seleccionada en el formulario.
    }

    return $this->render('UserBundle::orgForm.html.twig', array(
        'form' => $form->createView()
    ));
}

Ya tenemos nuestro formulario, ahora la vista(puedes hacerla mas bonita si quieres):

{{ form_start(form) }}
{{ form_row(form.organization) }}
{{ form_row(form.plan) }}
{{ form_row(form.edition) }}
{{ form_row(form.group) }}
{{ form_row(form.roleUser) }}
{{ form_end(form) }}

Y con esto ya tendrias un formulario que no se a basado en una Entity y que contiene los campos que a ti te interesan y que despues procesaras en el Action de la forma que creas conveniente.

Espero que te haya servido, y si es asi, puedes compartir este articulo en redes sociales pulsando en ellas.

En otro articulo, explicare como hacer selects dependientes o anidados a traves de AJAX, otra de las necesidades que todo tenemos hoy en dia para los formularios. Pero eso sera en otro capitulo 🙂

Gracias y saludos a todos.