Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

symfony2 chained selectors

Tags:

symfony

I have three entities: Country, State and City with the following relationships:

Image

When creating a City, I want two selectors, one for the Country and one for the State where the city belongs. These two selectors need to be chained so changing the Country will "filter" the States shown in the other selector.

I found a tutorial showing how to do this using Form Events but their example it's not quite my case. My entity City it's not directly related to the Country entity (they are indirectly related through State) so, when setting the country field in the City form (inside the class CityType), I'm forced to declare that field as 'property_path'=>false as you can see in the code below:

class CityType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('country', 'entity', array(
            'class'=>'TestBundle:Country', 
            'property'=>'name', 
            'property_path'=>false //Country is not directly related to City
        ));
        $builder->add('name');

        $factory = $builder->getFormFactory();

        $refreshStates = function ($form, $country) use ($factory) 
        {
            $form->add($factory->createNamed('entity', 'state', null, array(
                'class'         => 'Test\TestBundle\Entity\State',
                'property'      => 'name',
                'query_builder' => function (EntityRepository $repository) use ($country)
                                   {
                                       $qb = $repository->createQueryBuilder('state')
                                                        ->innerJoin('state.country', 'country');

                                        if($country instanceof Country) {
                                            $qb->where('state.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', "Venezuela");;
                                        }        
                                        return $qb;
                                    }
            )));
        };

        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (DataEvent $event) use ($refreshStates)
        {
            $form = $event->getForm();
            $data = $event->getData();

            if($data == null)
                return;               

            if($data instanceof City){
                if($data->getId()) { //An existing City
                    $refreshStates($form, $data->getState()->getCountry());
                }else{               //A new City
                    $refreshStates($form, null);
                }
            }
        });

        $builder->addEventListener(FormEvents::PRE_BIND, function (DataEvent $event) use ($refreshStates)
        {
            $form = $event->getForm();
            $data = $event->getData();

            if(array_key_exists('country', $data)) {
                $refreshStates($form, $data['country']);
            }
        });
    }

    public function getName()
    {
        return 'city';
    }

    public function getDefaultOptions(array $options)
    {
        return array('data_class' => 'Test\TestBundle\Entity\City');
    }
}

The problem is that when I try to edit an existing City, the related Country is not selected by default in the form. If I remove the line 'property_path'=>false I get (not surprisingly) the error message:

Neither property "country" nor method "getCountry()" nor method "isCountry()" exists in class "Test\TestBundle\Entity\City"

Any ideas?

like image 292
David Barreto Avatar asked Apr 17 '12 06:04

David Barreto


2 Answers

OK, I finally figured out how to do it properly:

namespace Test\TestBundle\Form;  use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder;  use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\Event\DataEvent;  use Test\TestBundle\Entity\Country; use Test\TestBundle\Entity\State; use Test\TestBundle\Entity\City;   class CityType extends AbstractType {     public function buildForm(FormBuilder $builder, array $options)     {         $builder->add('name');          $factory = $builder->getFormFactory();          $refreshStates = function ($form, $country) use ($factory) {             $form->add($factory->createNamed('entity','state', null, array(                 'class'         => 'Test\TestBundle\Entity\State',                 'property'      => 'name',                 'empty_value'   => '-- Select a state --',                 'query_builder' => function (EntityRepository $repository) use ($country) {                     $qb = $repository->createQueryBuilder('state')                         ->innerJoin('state.country', 'country');                      if ($country instanceof Country) {                         $qb->where('state.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;                })            ));         };          $setCountry = function ($form, $country) use ($factory) {             $form->add($factory->createNamed('entity', 'country', null, array(                 'class'         => 'TestBundle:Country',                  'property'      => 'name',                  'property_path' => false,                 'empty_value'   => '-- Select a country --',                 'data'          => $country,             )));         };          $builder->addEventListener(FormEvents::PRE_SET_DATA, function (DataEvent $event) use ($refreshStates, $setCountry) {             $form = $event->getForm();             $data = $event->getData();              if ($data == null) {                 return;             }              if ($data instanceof City) {                 $country = ($data->getId()) ? $data->getState()->getCountry() : null ;                 $refreshStates($form, $country);                 $setCountry($form, $country);             }         });          $builder->addEventListener(FormEvents::PRE_BIND, function (DataEvent $event) use ($refreshStates) {             $form = $event->getForm();             $data = $event->getData();              if(array_key_exists('country', $data)) {                 $refreshStates($form, $data['country']);             }         });     }      public function getName()     {         return 'city';     }      public function getDefaultOptions(array $options)     {         return array('data_class' => 'Test\TestBundle\Entity\City');     } } 

The jQuery AJAX selector

$(document).ready(function () {     $('#city_country').change(function(){         $('#city_state option:gt(0)').remove();         if($(this).val()){             $.ajax({                 type: "GET",                 data: "country_id=" + $(this).val(),                 url: Routing.generate('state_list'),                 success: function(data){                     $('#city_state').append(data);                 }             });         }     }); }); 

I hope this will be helpful to somebody else facing the same situation.

like image 61
David Barreto Avatar answered Sep 21 '22 12:09

David Barreto


Since your link to this approach is down i decided to complement your excelent answer so anyone can use it:

In order to execute the following javascript command:

url: Routing.generate('state_list'), 

You need to install FOSJsRoutingBundle that can be found in here.

ATENTION: in the read me section of the bundle there are instalation instructions but there is something missing. If you use the deps with this:

[FOSJsRoutingBundle] git=git://github.com/FriendsOfSymfony/FOSJsRoutingBundle.git target=/bundles/FOS/JsRoutingBundle 

You must run the php bin/vendors update before the next steps.

I'm still trying to find out what route is needed in the routing.yml for this solution to work. As soon as i discover i will edit this answer.

like image 25
Fonsini Avatar answered Sep 18 '22 12:09

Fonsini