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

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