Symfony - Select o combos dependientes

 
Vista:
Imágen de perfil de Carlos Alberto

Select o combos dependientes

Publicado por Carlos Alberto (1 intervención) el 17/03/2015 20:09:52
Hola a todos soy nuevo con esto de la programacion usando el framework symfony2, y queria saber si alguien tiene implementado en un proyecto symfony2 los select dependientes o combos dependientes. La idea es que ya en la base de datos este la relacion entre dos tablas, ejemplo paises y provincias, por lo que si yo selecciono un pais, me deberian salir solo las provincias para ese pais. Gracias de ante mano a todos. Saludos y espero que me respondan...
Valora esta pregunta
Me gusta: Está pregunta es útil y esta claraNo me gusta: Está pregunta no esta clara o no es útil
0
Responder
Imágen de perfil de gilberto

Select o combos dependientes

Publicado por gilberto (5 intervenciones) el 25/05/2016 20:31:58
Selects dependientes, selects relacionados, selects anidados, combos o combobox dependientes, hay muchas formas de llamarlos y siempre surgen dudas de cómo implementarlos, en este artículo vamos a ver una forma de implementarlo con formularios y mediante eventos en Symfony2.



Para este ejemplo tenemos 3 entities típicas, Country, Province y City. City tiene una relación de muchos a uno con Province mediante el atributo $province y Province tiene una relación de muchos a uno con Country mediante el atributo $country. Creo que no es necesario pegar aquí el código de las entities ya que se haría demasiado largo y se puede ver en Github.

Nos creamos un modelo llamado Location que será sobre el que haremos el formulario:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
namespace SMTC\MainBundle\Form\Model;
 
use Symfony\Component\Validator\Constraints as Assert;
 
class Location
{
    /**
     * @Assert\NotBlank()
     */
    public $address;
 
    /**
     * @Assert\Type("SMTC\MainBundle\Entity\City")
     * @Assert\NotNull()
     */
    public $city;
}

Como se puede ver tiene un atributo $address que será un string y $city será un objeto de tipo City. Creamos el formulario asociado:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace SMTC\MainBundle\Form\Type;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use SMTC\MainBundle\Form\EventListener\AddCityFieldSubscriber;
use SMTC\MainBundle\Form\EventListener\AddProvinceFieldSubscriber;
use SMTC\MainBundle\Form\EventListener\AddCountryFieldSubscriber;
 
class LocationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder,array $options)
    {
        $factory=$builder->getFormFactory();
        $citySubscriber=new AddCityFieldSubscriber($factory);
        $builder->addEventSubscriber($citySubscriber);
        $provinceSubscriber=new AddProvinceFieldSubscriber($factory);
        $builder->addEventSubscriber($provinceSubscriber);
        $countrySubscriber=new AddCountryFieldSubscriber($factory);
        $builder->addEventSubscriber($countrySubscriber);
 
        $builder->add('address');
    }
 
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'=>'SMTC\MainBundle\Form\Model\Location'
        ));
    }
 
    public function getName()
    {
        return 'location';
    }
}

Si no has visto nada de eventos en los formularios se recomienda leer la receta de Dynamic Form Generation, nosotros vamos a usar dos eventos, PRE_SET_DATA al que se le llama justo antes de poblar el formulario con los datos del modelo y el evento PRE_BIND al que se le llama justo antes de poblar el formulario con los datos que llegan de la vista.

En el código anterior vemos que se crean 3 suscriptores y el campo address, vamos a ver los suscriptores.

AddCityFieldSubscriber:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
namespace SMTC\MainBundle\Form\EventListener;
 
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityRepository;
use SMTC\MainBundle\Entity\Province;
 
class AddCityFieldSubscriber implements EventSubscriberInterface
{
    private $factory;
 
    public function __construct(FormFactoryInterface $factory)
    {
        $this->factory = $factory;
    }
 
    public static function getSubscribedEvents()
    {
        returnarray(
            FormEvents::PRE_SET_DATA=>'preSetData',
            FormEvents::PRE_BIND     =>'preBind'
        );
    }
 
    private function addCityForm($form, $province)
    {
        $form->add($this->factory->createNamed('city','entity',null,array(
            'class'         =>'MainBundle:City',
            'empty_value'   =>'Ciudad',
            'query_builder'=>function(EntityRepository$repository)use($province){
                $qb=$repository->createQueryBuilder('city')
                    ->innerJoin('city.province','province');
                if($provinceinstanceofProvince){
                    $qb->where('city.province = :province')
                    ->setParameter('province',$province);
                }elseif(is_numeric($province)){
                    $qb->where('province.id = :province')
                    ->setParameter('province',$province);
                }else{
                    $qb->where('province.name = :province')
                    ->setParameter('province',null);
                }
 
                return $qb;
            }
        )));
    }
 
    public function preSetData(FormEvent $event)
    {
        $data=$event->getData();
        $form=$event->getForm();
 
        if(null===$data){
            return;
        }
 
        $province=($data->city)?$data->city->getProvince():null;
        $this->addCityForm($form,$province);
    }
 
    public function preBind(FormEvent $event)
    {
        $data=$event->getData();
        $form=$event->getForm();
 
        if(null===$data){
            return;
      }
 
        $province=array_key_exists('province',$data) ? $data['province'] : null;
        $this->addCityForm($form,$provi nce);
    }
}
Cuando se le llama a preSetData lo que se hace es comprobar si $data, que en este caso será un objeto de tipo Location, tiene asociado una ciudad para poder obtener la provincia, si tiene asociada una provincia entonces las ciudades que deben aparecer en el select son sólo las de esa provincia. En el métodopreBind se hace lo mismo, pero los datos llegan en forma de array.

Estos dos métodos llaman a addCityForm que se encarga de añadir al formulario el campo city y las ciudades que aparecerán serán en base a la provincia.

AddProvinceFieldSubscriber:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
namespace SMTC\MainBundle\Form\EventListener;
 
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityRepository;
use SMTC\MainBundle\Form\Model\User;
use SMTC\MainBundle\Entity\Country;
 
class AddProvinceFieldSubscriber implements EventSubscriberInterface
{
    private $factory;
 
    public function __construct(FormFactoryInterface$factory)
    {
        $this->factory=$factory;
    }
 
    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData',
            FormEvents::PRE_BIND     => 'preBind'
        );
    }
 
    private function addProvinceForm($form,$province,$country)
    {
        $form->add($this->factory->createNamed('province','entity',$province,array(
            'class'         =>'MainBundle:Province',
            'empty_value'   =>'Provincia',
            'mapped'        =>false,
            'query_builder'=>function(EntityRepository$repository)use($country){
                $qb=$repository->createQueryBuilder('province')
                    ->innerJoin('province.country','country');
                if($countryinstanceofCountry){
                    $qb->where('province.country = :country')
                    ->setParameter('country',$country);
                }elseif(is_numeric($country)){
                    $qb->where('country.id = :country')
                    ->setParameter('country',$country);
                }else{
                    $qb->where('country.name = :country')
                    ->setParameter('country',null);
                }
 
                return $qb;
            }
        )));
    }
 
    public function preSetData(FormEvent $event)
    {
        $data=$event->getData();
        $form=$event->getForm();
 
        if(null===$data){
            return;
        }
 
        $province=($data->city)?$data->city->getProvince():null;
        $country=($province)?$province->getCountry():null;
        $this->addProvinceForm($form,$province,$country);
    }
 
    public function preBind(FormEvent $event)
    {
        $data=$event->getData();
        $form=$event->getForm();
 
        if(null===$data){
            return;
        }
 
        $province=array_key_exists('province',$data)?$data['province']:null;
        $country=array_key_exists('country',$data)?$data['country']:null;
        $this->addProvinceForm($form,$province,$country);
    }
}

Para añadir el campo province es similar a city, en preSetData comprobamos que $data tiene el atributo city y recuperamos la provincia a la que pertenece la ciudad y el país al que pertenece la provincia. En preBind hacemos lo mismo que antes, $data es un array y obtenemos province y country. Finalmente en estos dos métodos llamamos a addProvinceForm que añade el campo province al formulario. El campo province tiene dentro de sus opciones mapped => false que significa que no está asociado al objeto que maneja el formulario, es decir al objeto de tipo Location, por esto mismo el formulario no sabe cuál es la provincia seleccionada y hay que pasársela como parámetro cuando se crea (tercer parámetro del createNamed).

AddCountryFieldSubscriber:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
namespace SMTC\MainBundle\Form\EventListener;
 
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityRepository;
 
class AddCountryFieldSubscriber implements EventSubscriberInterface
{
    private $factory;
 
    public function __construct(FormFactoryInterface$factory)
    {
        $this->factory=$factory;
    }
 
    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA=>'preSetData',
            FormEvents::PRE_BIND     =>'preBind'
        );
    }
 
    private function addCountryForm($form,$country)
    {
        $form->add($this->factory->createNamed('country','entity',$country,array(
            'class'         =>'MainBundle:Country',
            'mapped'        =>false,
            'empty_value'   =>'País',
            'query_builder'=>function(EntityRepository$repository){
                $qb=$repository->createQueryBuilder('country');
 
                return $qb;
            }
        )));
    }
 
    public function preSetData(FormEvent $event)
    {
        $data=$event->getData();
        $form=$event->getForm();
 
        if(null===$data){
            return;
        }
 
        $country=($data->city) ? $data->city->getProvince()->getCountry() : null;
        $this->addCountryForm($form,$country);
    }
 
    public function preBind(FormEvent $event)
    {
        $data=$event->getData();
        $form=$event->getForm();
 
        if(null===$data){
            return;
        }
 
        $country=array_key_exists('country',$data)?$data['country']:null;
        $this->addCountryForm($form,$country);
    }
}


En este caso hacemos lo mismo que en provincia, pero al no tener un campo que limita la lista de países que aparecen sólo nos hace falta el $country seleccionado o que viene dado por la ciudad seleccionada.

Bernhard Schussek el lead developer del componente de formularios y validación comentaba que están trabajando en hacerlo más sencillo, así que está previsto. Parte del código de este artículo está basado en una pregunta en StackOverflow.

Faltaría la parte del controller y la vista (ajax para que al seleccionar un país aparezcan sólo las provincias de ese país, etc), pero para eso hemos creado unos ejemplos y se puede ver ahí:

Código del controlador

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
     * @Route("/selects-dependientes/location/new", name="examples_dependent_selects_location_new")
     * @Template("MainBundle:Example\Location:new_location.html.twig")
     */
    public function newLocationAction()
    {
        $location = new Location();
        $form = $this->createForm(new LocationType(), $location);
 
        $request = $this->getRequest();
 
        if ($request->isMethod('POST')) {
            $form->bind($request);
 
            if ($form->isValid()) {
 
                // do amazing things
 
                $flashBag = $this->get('session')->getFlashBag();
                $flashBag->add('smtc_success', 'Se ha creado una localización:');
                $flashBag->add('smtc_success', sprintf('Dirección: %s', $location->address));
                $flashBag->add('smtc_success', sprintf('Ciudad: %s', $location->city->getName()));
 
                return $this->redirect($this->generateUrl('examples_dependent_selects'));
            }
        }
 
        return array(
            'form' => $form->createView()
        );
    }

Código de la vista

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{% extends 'MainBundle::layout.html.twig' %}
 
{% set section = 'examples' %}
 
{% block javascripts %}
    {{ parent() }}
    <script>
        $(function(){
            $("#location_country").change(function(){
                var data = {
                    country_id: $(this).val()
                };
 
                $.ajax({
                    type: 'post',
                    url: '{{ path("select_provinces") }}',
                    data: data,
                    success: function(data) {
                        $('#location_province').html(data);
                        $('#location_city').html("<option>Ciudad</option>");
                    }
                });
            });
 
            $("#location_province").change(function(){
                var data = {
                    province_id: $(this).val()
                };
 
                $.ajax({
                    type: 'post',
                    url: '{{ path("select_cities") }}',
                    data: data,
                    success: function(data) {
                        $('#location_city').html(data);
                    }
                });
            });
        });
    </script>
{% endblock %}
 
{% block content %}
    <div class="row">
        <div class="span12">
            <form action="{{ path('examples_dependent_selects_location_new') }}" method="POST" novalidate>
                {{ form_row(form.address) }}
                {{ form_row(form.country) }}
                {{ form_row(form.province) }}
                {{ form_row(form.city) }}
                {{ form_rest(form) }}
                <button type="submit" class="btn btn-success">
                    Guardar
                </button>
            </form>
        </div>
    </div>
{% endblock
Valora esta respuesta
Me gusta: Está respuesta es útil y esta claraNo me gusta: Está respuesta no esta clara o no es útil
0
Comentar