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

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s