Blog


Select dependientes y autocompletados en Symfony2

SELECT DEPENDIENTES Y AUTOCOMPLETADOS EN SYMFONY2

24 / 09 / 2015 Symfony

En este artículo queremos repasar la forma de implementar dos casos que se repiten muy a menudo al crear formularios con Symfony2: los combos o select dependientes o anidados y los campos autocompletados. Además, lo haremos integrando ambos, es decir, crearemos dos campos autocompletados y que dependa uno del otro. Para ello, contaremos con dos entidades, Cliente y Mascota, teniendo la entidad Mascota una relación uno a muchos con Cliente:


<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

class Cliente
{
    // …

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     */
    protected $nombre;

    // …
}


<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

class Mascota
{
    // …

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     */
    protected $nombre;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Cliente", inversedBy="mascotas")
     * @ORM\JoinColumn(name="cliente_id", referencedColumnName="id")
     * @Assert\NotBlank
     */
    protected $cliente;

    // …
}

Trabajaremos con un formulario que corresponderá a un modelo llamado Cita que tendrá un atributo fecha (tipo date) y un atributo mascota que será un objeto de tipo Mascota:


<?php
 
namespace AppBundle\Entity;
 
use Symfony\Component\Validator\Constraints as Assert;
 
class Cita
{
    /**
     * @Assert\NotBlank()
     */
    public $fecha;
 
    /**
     * @Assert\Type("AppBundle\Entity\Mascota")
     * @Assert\NotNull()
     */
    public $mascota;
}

Empezaremos creando el formulario asociado al modelo Cita:


<?php

namespace AppBundle\Form;

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

class CitaType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
       $builder
           ->add('fecha', 'date', array('widget' => 'single_text'))
           ->add('mascota');
    }

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

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

 

Campo autocompletado

Incluir campos autocompletados en los formularios que creamos en nuestros proyectos Symfony es extremadamente sencillo gracias a bundles de terceros como GenemuFormBundle, que implementa Form types para librerías como Select2, ReCaptcha, JQueryUi, etc. En el ejemplo que estamos tratando usaremos Select2, veamos cómo hacerlo.

Empezaremos instalando el bundle en nuestro proyecto, para ello añadimos la dependencia en composer.json:


{
    …
    "require": {
        …
        "genemu/form-bundle": “2.2.*”,
        …
    },
    …
}

actualizamos composer para que se instalen las nuevas dependencias:


$ composer update

habilitamos el bundle en el kernel:


<?php
// app/AppKernel.php

public function registerBundles()
{
    $bundles = array(
        // ...
        new Genemu\Bundle\FormBundle\GenemuFormBundle(),
    );
}

e inicializamos los assets:


$ php app/console assets:install --symlink web/

En nuestro caso, como ya hemos dicho, vamos a usar Select2 para los campos autocompletados, por lo tanto, tendremos que configurarlo en el fichero config.yml de nuestro proyecto:


# app/config/config.yml
genemu_form:
    select2: ~

Llegados a este punto, modificaremos nuestra entidad CitaType para usar el autocompletado en el campo mascota (‘genemu_jqueryselect2_entity'):


<?php

// ...

class CitaType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
       $builder
           ->add('fecha', 'date', array('widget' => 'single_text'))
           ->add('mascota', 'genemu_jqueryselect2_entity', array(
                'class' => 'AppBundle\Entity\Mascota',
                'property' => 'nombre',
            )));
    }

    // ...
}

Para que todo funcione correctamente, solo faltará incluir en la plantilla del formulario lo siguiente:


{# … #}

{{ form_row(form.fecha) }}
{{ form_row(form.mascota) }}

{{ form_stylesheet(form) }}
{{ form_javascript(form) }}

{# … #}

Con esto ya tenemos listo el campo autocompletado en nuestro formulario, fácil ¿verdad?

Select dependientes

La otra parte que nos habíamos propuesto en este artículo era ver cómo incluir campos dependientes en nuestro formulario, para lo que tendremos que realizar algunas modificaciones en nuestro CitaType.

Lo primero será añadir los campos cliente y mascota mediante dos listener que veremos a continuación, de la siguiente manera:


<?php

// ...

class CitaType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
       $builder
           ->add('fecha', 'date', array('widget' => 'single_text'))
           ->addEventSubscriber(new AddClienteFieldSubscriber())
           ->addEventSubscriber(new AddMascotaFieldSubscriber());
    }

    // ...
}

Nuestros listener se lanzarán cuando se produzca uno de estos dos eventos de formulario:

PRE_SET_DATA: se produce al inicio del método Form::setData() que carga el formulario con los datos.

PRE_SUBMIT: se produce al inicio del método Form::submit() que envía los datos del formulario, los transforma y valida.

Nuestra clase AddClienteFieldSubscriber quedará de la siguiente manera:


<?php

namespace AppBundle\Form\EventListener;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;

class AddClienteFieldSubscriber implements EventSubscriberInterface
{
   public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData',
            FormEvents::PRE_SUBMIT => 'preSubmit'
        );
    }

    private function addClienteForm($form, $cliente = null)
    {
        $formOptions = array(
            'class' => 'AppBundle:Cliente',
            'placeholder' => 'Selecciona...',
            'mapped' => false,
            'attr' => array(
                'class' => 'cliente_selector',
            )
        );

        if ($cliente) {
            $formOptions['data'] = $cliente;
        }

        $form->add('cliente', 'genemu_jqueryselect2_entity', $formOptions);
    }

    public function preSetData(FormEvent $event)
    {
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

        $accessor = PropertyAccess::createPropertyAccessor();

        $mascota = $accessor->getValue($data, 'mascota');
        $cliente = ($mascota) ? $mascota->getCliente() : null;

        $this->addClienteForm($form, $cliente);
    }

    public function preSubmit(FormEvent $event)
    {
        $form = $event->getForm();

        $this->addClienteForm($form);
    }
}

y la clase AddMascotaFieldSubscriber:


<?php

namespace AppBundle\Form\EventListener;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Doctrine\ORM\EntityRepository;

class AddMascotaFieldSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData',
            FormEvents::PRE_SUBMIT => 'preSubmit'
        );
    }

    private function addMascotaForm($form, $cliente_id)
    {
        $formOptions = array(
            'placeholder' => 'Selecciona...',
            'class' => 'AppBundle:Mascota',
            'property' => 'nombre',
            'attr' => array(
                'class' => 'mascota_selector',
            ),
            'query_builder' => function (EntityRepository $repository) use ($cliente_id) {
                $qb = $repository->createQueryBuilder('m')
                    ->innerJoin('m.cliente', 'c')
                    ->where('c.id = :cliente')
                    ->setParameter('cliente', $cliente_id)
                ;

                return $qb;
            }
        );

        $form->add(‘mascota’, 'genemu_jqueryselect2_entity', $formOptions);
    }

    public function preSetData(FormEvent $event)
    {
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

        $accessor = PropertyAccess::createPropertyAccessor();

        $mascota = $accessor->getValue($data, 'mascota');
        $cliente_id = ($mascota) ? $mascota->getCliente()->getId() : null;

        $this->addMascotaForm($form, $cliente_id);
    }

    public function preSubmit(FormEvent $event)
    {
        $data = $event->getData();
        $form = $event->getForm();

        $cliente_id = array_key_exists('cliente', $data) ? $data['cliente'] : null;

        $this->addMascotaForm($form, $cliente_id);
    }
}

Nos faltan dos cosas para rematar nuestro formulario en Symfony2 con campos dependientes y autocompletados:

  • añadir una acción en un controlador de nuestro proyecto para recuperar las mascotas de un determinado cliente:

<?php

namespace AppBundle\Controller;

// ...
use Symfony\Component\HttpFoundation\JsonResponse;


class MascotaController extends Controller
{
    // ...
    
    /**
     * @Route("/mascotas", name="select_mascotas", condition="request.headers.get('X-Requested-With') == 'XMLHttpRequest'")
     */
     public function mascotasAction(Request $request)
     {
         $em = $this->getDoctrine()->getManager();

         $cliente_id = $request->request->get('cliente_id');

         $mascotas = $em->getRepository(‘AppBundle:Mascota’)->findByClienteId($cliente_id);

         return new JsonResponse($mascotas);
     }

     // ...
}

  • incluir un código javascript en la plantilla de nuestro formulario que realice una llamada AJAX a la acción anterior y cargue los valores devueltos en el campo mascota:

{# … #}

{{ form_row(form.fecha) }}
{{ form_row(form.cliente) }}
{{ form_row(form.mascota) }}

<script>
    $("#cita_cliente").change(function() {
        var data = {
            cliente_id: $(this).val()
        };

        $.ajax({
            type: 'post',
            url: '{{ path("select_mascotas") }}',
            data: data,
            success: function(data) {
                var $mascota_selector = $('#cita_mascota');

                $mascota_selector.html('<option>Selecciona...</option>');

                for (var i = 0, total = data.length; i < total; i++) {
                    $mascota_selector.append('<option value="' + data[i].id + '">' + data[i].nombre + '</option>');
                }
            }
        });
    });
</script>

{{ form_stylesheet(form) }}
{{ form_javascript(form) }}

{# … #}



ARTÍCULOS RELACIONADOS