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:
<?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);
}
// ...
}
{# … #}
{{ 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) }}
{# … #}