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

Registro con ventana modal con FOSUserBundle en Symfony 3

Comumente las aplicaciones web desarrolladas ya sea con Symfony o no, suelen tener ciertas caracteristicas similares(no en todos los casos, obviamente), pero muchas de ellas se parecen en algunas funcionalidades. En este caso, y como este blog trata sobre Symfony, voy a explicar un caso comun sobre la gestion de usuarios, y mas concretamente el registro de usuario, y su solucion usando el FOSUserBundle para la gestion de usuarios en Symfony 3.

Generalmente las aplicaciones web, pueden ofrecer al usuario una de estas 2 formas para que se registren en el web.

Una de esas opciones y la mas “sencilla”, seria la de una pagina o vista con un formulario donde el usuario rellena dichos campos, y al enviar dicho formulario, se le envia a otra pagina distinta(ya sea de bienvenida, de procesar compra, etc.). Digo sencilla porque el FOSUserBundle de Symfony 3 ya ofrece esta funcionalidad de esa forma.

La segunda forma, seria la que podriamos ver cuando pulsamos un boton de la cabecera o header, y nos aparece una ventana modal en forma de capa con un fondo oscuro, en la que nos aparece el formulario con los campos, y que nos guia a traves de ese modal a otra parte de la aplicacion. (He tenido que ocultar ciertos detalles de esta captura por temas legales y de privacidad)

Registro con ventana modal
Registro con ventana modal

A efectos logicos, podria ser practicamente igual una forma que la otra, pero a efectos practicos no es igual, y su solucion es algo mas compleja que reescribir las vistas del FOSUserBundle y alguna que otra cosa mas. Tenemos que hacer varias cosas mas para que todo esto funcione y sea correcto usando las buenas practicas en Symfony 3. Dicho esto, comenzamos.

NOTA: en el proyecto estoy usando Symfony 3 y la ultima version del FOSUSerBundle, aunque supongo que para Symfony 2 sera practicamente igual.

1- Instalamos FOSUserBundle(composer.json)

“friendsofsymfony/user-bundle”: “dev-master”

2- Inicializamos en bundle en la aplicacion(app/AppKernel.php):

new FOS\UserBundle\FOSUserBundle(),

3- Importamos las rutas del FOSUserBundle para que esten accesibles:

#fos_user:
#    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"

fos_user_profile:
    resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
    prefix: /profile

fos_user_register:
    resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
    prefix: /register

fos_user_resetting:
    resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
    prefix: /resetting

fos_user_change_password:
    resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
    prefix: /profile

NOTA: Atentos a que la primera ruta esta comentada, pues importa todas las rutas del FOS, pero puede que no las queramos tener todas disponibles, por eso las importamos una a una a mano, mas abajo.

4- Configuramos el bundle(app/config/config.yml)

Una buena practica es crear un nuevo archivo por ejemplo “fos.yml” y añadir la configuracion especifica ahi, y despues importarlo en el config.yml.

fos_user:
    db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
    firewall_name: main
    user_class: UserBundle\Entity\User
    registration:
        form:
            type: UserBundle\Form\RegistrationType
    use_username_form_type: false

NOTA: Por una parte, configuramos para que el formulario de registro use nuestro formulario(ya que lo tendremos que sobrescribir) y despues desactivamos que el formulario haga uso de la validacion por el campo username. Mas adelante, lo explicare.

5- Definimos la seguridad, roles, y providers(app/config/security.yml)

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:

    encoders:
        UserBundle\Entity\User: sha512
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_token_generator: security.csrf.token_manager
                login_path: fos_user_security_login
                check_path: fos_user_security_check
                failure_path: fos_user_security_login
                always_use_default_target_path: true
                default_target_path: homepage
                # if you are using Symfony < 2.8, use the following config instead:
                # csrf_provider: form.csrf_provider

            logout:       true
            anonymous:    true
            # activate different ways to authenticate

            # http_basic: ~
            # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # http://symfony.com/doc/current/cookbook/security/form_login_setup.html

    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/, role: ROLE_ADMIN }

NOTA: En nuestro caso, tenemos un par de roles muy sencillos y poco mas.

6- Creamos la entity User que extenderá la que lleva el FOSUserBundle, con algunos campos más:

use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="ecommerce_user")
 */
class User extends BaseUser
{
 /**
 * @ORM\Id
 * @ORM\Column(type="integer")
 * @ORM\GeneratedValue(strategy="AUTO")
 */
 protected $id;

 /**
 * @ORM\Column(type="string", length=250, nullable=true)
 */
 protected $name;

 /**
 * @ORM\Column(type="string", length=250, nullable=true)
 */
 protected $surname;

 public function __construct()
 {
     parent::__construct();
     // your own logic

     $this->enabled = true;
 }

 /**
 * @return string
 */
 public function getName()
 {
 return $this->name;
 }

 /**
 * @param string $name
 */
 public function setName($name)
 {
 $this->name = $name;
 }

 /**
 * @return string
 */
 public function getSurname()
 {
 return $this->surname;
 }

 /**
 * @param string $surname
 */
 public function setSurname($surname)
 {
 $this->surname = $surname;
 }

 /**
 * @param string $email
 *
 * @return $this|\FOS\UserBundle\Model\UserInterface
 */
 public function setEmail($email)
 {
     $this->setUsername($email);
     return parent::setEmail($email);
 }
}

NOTA: Prestad especial atencion al metodo setEmail(). Este metodo esta implementado en la clase BaseUser del FOS, pero nosotros necesitamos “sobrescribirlo” porque en nuestro caso el email sera nuestro “Identificador” del usuario con el que se podra “logear” en la aplicacion web.

Por supuesto, hay que aplicar los nuevas cambios en la base de datos usando el comando:

$ php bin/console doctrine:schema:update –force –dump-sql

7- Sobrescribir el formulario de registro del FOS por el nuestro personalizado:

class RegistrationType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, array(
                'label' => 'Nombre',
                'required' => true,
                'constraints' => array(
                    new NotBlank(),
                ),
            ))
            ->add('surname', TextType::class, array(
                'label' => 'Apellidos',
                'required' => true,
                'constraints' => array(
                  new NotBlank(),
                ),
            ))
            ->add('email', EmailType::class, array(
                'label' => 'Email',
                'required' => true,
                'constraints' => array(
                    new NotBlank(),
                ),
            ))
            ->add('plainPassword', TextType::class, array(
                'label' => 'Contraseña',
                'required' => true,
                'constraints' => array(
                    new NotBlank(),
                )
            )
        );
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'UserBundle\Entity\User',
            'csrf_token_id' => 'registration'
        ));
    }

    public function getBlockPrefix()
    {
        return 'user_registration';
    }
}

En  nuestro caso, no necesitamos el campo “username” o “password” con validacion que “impone” el formulario base del FOSUserBundle. Es por ello, que lo sobrescribimos de esta forma. Si nos acordamos bien, definimos en fos.yml que el formulario de registro iba a ser uno nuestro. Pues bien, este es el que se usara.

Uno de los problemas que me encontre al usar el formulario base del FOS fue que esos campos(username, o password con validacion) debian mostrarse si o si, y aunque se oculten para no mostrarse, el formulario daba error, sobretodo por Javascript al decir que dichos campos no eran “focusable” , es decir, no podia situarse el foco(raton, tabulador teclado, etc.) en ellos.

8- Sobrescribir el controlador del registro

Aqui os explicare porque, ya que en mi caso, el controller base del FOS redirige a una pagina nueva, y en nuestro caso no es asi, sino la misma pagina donde estabamos pero mostrando la ventana modal. Por lo tanto, necesitamos sobrescribir dicha funcionalidad.

El bundle nos dice en su documentacion, que para poder “sobrescribir” un Controller, necesitamos crear un bundle “hijo” que herede del FOS.

8.1- Creamos un nuevo UserBundle

$ php bin/console generate:bundle –namespace=UserBundle –dir=src

8.2- Le decimos que nuestro nuevo bundle herede del FOSUserBundle (src/UserBundle/UserBundle.php)

class UserBundle extends Bundle
{
    public function getParent()
    {
        return 'FOSUserBundle';
    }
}

8.3- Creamos un nuevo RegisterController.php que sustituira el del FOS

<?php

/*
 * This file overrides the FOSUserBundle registration controller.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace UserBundle\Controller;

use AppBundle\Controller\BaseController;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\Form\Factory\FactoryInterface;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * Controller managing the registration.
 *
 * @author Thibault Duplessis <thibault.duplessis@gmail.com>
 * @author Christophe Coevoet <stof@notk.org>
 */
class RegistrationController extends BaseController
{
    /**
     * @param Request $request
     *
     * @return Response
     */
    public function registerAction(Request $request)
    {
        /** @var $formFactory FactoryInterface */
        $formFactory = $this->get('fos_user.registration.form.factory');
        /** @var $userManager UserManagerInterface */
        $userManager = $this->get('fos_user.user_manager');
        /** @var $dispatcher EventDispatcherInterface */
        $dispatcher = $this->get('event_dispatcher');

        $user = $userManager->createUser();

        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_INITIALIZE, $event);

        if (null !== $event->getResponse()) {
            return $event->getResponse();
        }

        $form = $formFactory->createForm();
        $form->setData($user);

        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            if ($form->isValid()) {
                $event = new FormEvent($form, $request);
                $dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event);

                $userManager->updateUser($user);

                if (null === $response = $event->getResponse()) {
                    $url = $this->generateUrl('fos_user_registration_confirmed');
                    $response = new RedirectResponse($url);
                }

                $dispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($user, $request, $response));

                return $response;
            }

            $event = new FormEvent($form, $request);
            $dispatcher->dispatch(FOSUserEvents::REGISTRATION_FAILURE, $event);

            if (null !== $response = $event->getResponse()) {
                return $response;
            }
        }

        return $this->redirectToReferer($request);

//        return $this->render('FOSUserBundle:Registration:register.html.twig', array(
//            'form' => $form->createView(),
//        ));
    }

    /**
     * Tell the user to check his email provider.
     */
    public function checkEmailAction()
    {
        $email = $this->get('session')->get('fos_user_send_confirmation_email/email');

        if (empty($email)) {
            return new RedirectResponse($this->get('router')->generate('fos_user_registration_register'));
        }

        $this->get('session')->remove('fos_user_send_confirmation_email/email');
        $user = $this->get('fos_user.user_manager')->findUserByEmail($email);

        if (null === $user) {
            throw new NotFoundHttpException(sprintf('The user with email "%s" does not exist', $email));
        }

        return $this->render('FOSUserBundle:Registration:check_email.html.twig', array(
            'user' => $user,
        ));
    }

    /**
     * Receive the confirmation token from user email provider, login the user.
     *
     * @param Request $request
     * @param string  $token
     *
     * @return Response
     */
    public function confirmAction(Request $request, $token)
    {
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->get('fos_user.user_manager');

        $user = $userManager->findUserByConfirmationToken($token);

        if (null === $user) {
            throw new NotFoundHttpException(sprintf('The user with confirmation token "%s" does not exist', $token));
        }

        /** @var $dispatcher EventDispatcherInterface */
        $dispatcher = $this->get('event_dispatcher');

        $user->setConfirmationToken(null);
        $user->setEnabled(true);

        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRM, $event);

        $userManager->updateUser($user);

        if (null === $response = $event->getResponse()) {
            $url = $this->generateUrl('fos_user_registration_confirmed');
            $response = new RedirectResponse($url);
        }

        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRMED, new FilterUserResponseEvent($user, $request, $response));

        return $response;
    }

    /**
     * Tell the user his account is now confirmed.
     */
    public function confirmedAction()
    {
        $user = $this->getUser();
        if (!is_object($user) || !$user instanceof UserInterface) {
            throw new AccessDeniedException('This user does not have access to this section.');
        }

        return $this->render('FOSUserBundle:Registration:confirmed.html.twig', array(
            'user' => $user,
            'targetUrl' => $this->getTargetUrlFromSession(),
        ));
    }

    /**
     * @return mixed
     */
    private function getTargetUrlFromSession()
    {
        $key = sprintf('_security.%s.target_path', $this->get('security.token_storage')->getToken()->getProviderKey());

        if ($this->get('session')->has($key)) {
            return $this->get('session')->get($key);
        }
    }
}

Si observais en rojo he comentado el render de su propia vista, y en verde mi codigo, que devuelve al usuario a la pagina de donde a venido. Podriamos pasarle algun parametro o flag para que cuando se cargue esa vista, abra automaticamente la ventana modal y muestre algo, por ejemplo. Eso lo dejo a vuestro a vuestra eleccion. Ademas, tambien tenemos que implementar el resto de metodos que hay en dicho controlador, en mi caso los he dejado igual que estaban.

SUGERENCIA: Al comenzar el proyecto solo tenia un unico bundle (AppBundle) donde iba metiendo la logica de negocio de la aplicacion web. Al necesitar “sobrescribir” el FOSUserBundle, he necesitado crear un nuevo UserBundle por lo que “todo” lo relacionado con los usuarios lo he migrado al UserBundle(Controllers, vistas, js, Entity, etc.) para no mezclarlo con el resto de cosas que iran en ApBundle.

El resto ya es cosa vuestra, es decir debereis usar algun plugin de jQuery o lo que useis para implementar vuestro “overlay” o dialogo modal y pintar alli el formulario. Os dejo un ejemplo de como quedaria el mio:

REGISTER-MODAL.HTML.TWIG

El action del controller quedaria de la siguiente forma:

/**
 * @Route("/", name="homepage")
 */
public function indexAction(Request $request)
{
    /** @var $formFactory FactoryInterface */
    $formFactory = $this->get('fos_user.registration.form.factory');
    $user = new User();
    $form = $formFactory->createForm();
    $form->setData($user);

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

El siguiente problema al que me enfrento es el siguiente: Imaginad que mi aplicacion web tiene 10 paginas en el front. En la cabecera(header) es donde se llama a la ventana modal para mostrar el formulario de registro, es decir, que por logica en cada Action del Controller deberia crear el formulario cada vez y enviarselo como parametro a cada vista( 10 veces al menos ).

Para evitar duplicar el mismo bloque cada vez, me ha pasado un buen amigo una idea que me parece bastante acertada, y consiste en usar la funcion “render” de twig para llamar a un controller y que este se encargue de crear el formulario. Esto se haria asi:

En nuestro template(footer.html.twig) usamos la funcion twig asi:

{{ render(controller('AppBundle:Default:registerModal')) }}

Y en nuestro controller, creamos el formulario de registro y pintamos la vista “parcial”, y hacemos lo siguiente:

/**
 * @Route("/register-modal", name="register_modal")
 */
public function registerModalAction()
{
    /** @var $formFactory FactoryInterface */
    $formFactory = $this->get('fos_user.registration.form.factory');
    $user = new User();
    $form = $formFactory->createForm();
    $form->setData($user);

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

De esta forma, todas las vistas que muestren el footer, construiran el formulario de registro y mostraran la ventana modal con el registro de usuario, asi no “duplicamos” la misma logica en todos los action del controlador.

 

Espero que os haya gustado, y si teneis otra forma distinta de hacerlo o teneis alguna pregunta, no dudeis en preguntar y compartirlo en las redes sociales.

Saludos SymfonyDevs ¡¡

Obtener versiones de los vendors instalados con Composer

Una de las dudas que he tenido durante mucho tiempo al trabajar con composer es averiguar que versiones de los vendors se han instalado al ejecutar composer install o composer update.

Esta duda me surgia cada vez que al actualizar me daba algun error o “warning” por que algun vendor necesitaba una version superior o distinta de alguna otra libreria que tuviera instalada, y en un momento dado, en lugar de tener puestas que se instalen las que esten en la rama “dev-master”, quizas me interesaba instalar las que estuvieran en un tag o rama concreta para evitar “bugs” que hubieran sido detectados o cualquier otro motivo, es por eso que necesitaba saber que version tenia antes de actualizar mediante el comando “composer update”.

Para ello solamente tenemos que ejecutar el siguiente comando y nos sacara un listado de todos los vendors instalados y de la version de cada uno:

$ composer show -i

Captura de pantalla de 2016-07-13 12:41:25

Si ademas queremos ver para cada vendor, que dependencias tiene y que versiones requieren en forma de arbol solamente tendremos que añadir el parametro -t , tal que asi:

$ composer show -i -t

Captura de pantalla de 2016-07-13 12:40:46

Si en el listado que nos aparece, queremos concretar mas aun, y ver que version hay de un vendor en concreto, podemos ejecutar esto y nos dara la informacion mas detallada:

$ composer show — doctrine/doctrine-fixtures-bundle

composer how vendor

Documentacion:

Si os ha gustado este post, por favor compartidlo y dar “Me gusta” en las redes sociales.

Nos vemos en el siguiente post, symfonyDevs ¡¡

jQuery: diferencias entre usar los eventos .click() y .on(‘click’)

Acabo de encontrar un articulo que me a parecido muy útil y claro, a la hora de usar estos eventos de jQuery y el porqué de usar uno en cada momento ya que en la documentacion oficial no es que este bien explicado del todo.

Cuando usar click

$("button.alert").click(function() {
    alert(1);
});

Con lo anterior, se crea de forma individual un manejador para cada elemento que coincide con el selector. Esto significa que:

  1. Muchos elementos que coincidan con el selector, crearan manejadores idénticos y esto incrementara el consumo de memoria
  2. Los elementos que se añaden de forma dinámica no tendrán manejador. Si nosotros crearemos nuevos ‘button’ con clase ‘alert’ en el evento click() estos no tendrán un manejador asociado, y por lo tanto no funcionara el evento asociado a esa acción sobre el elemento HTML button.

Cuando usar on

$("div#container").on('click', 'button.alert', function() {
    alert(1);
});

Con lo anterior, se asocia un manejador único para cada elemento que coincidan con el selector incluyendo los que se crean de forma dinámica.

Otro de los motivos es con el uso de eventos con namespace. Si añades un manejador con .on(‘click’, handler), normalmente lo eliminaras con .off(‘click’, handler), y obviamente esto solo funcionara si tienes una referencia a la función, para ello has de usar namespaces.

$("#element").on("click.someNamespace", function() { console.log("anonymous!"); });

Eliminar manejador con:

$("#element").off("click.someNamespace");

Me a parecido muy útil esta explicación, así que la comparto para todos.

Documentacion de referencia:

 

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 ¡