2012-04-17 15 views
19

Ho tre entità: Paese, stato e la città con le seguenti relazioni:Symfony2 incatenato selettori

Image http://i39.tinypic.com/15gc85u.png

Durante la creazione di una Città, voglio due selettori, uno per il Paese e una per lo stato in cui la città appartiene Questi due selettori devono essere concatenati in modo che la modifica del Paese "filtri" gli Stati mostrati nell'altro selettore.

Ho trovato un tutorial che mostra come farlo utilizzando Form Events ma il loro esempio non è proprio il mio caso. La mia entità Città non è direttamente correlata all'entità Paese (sono indirettamente correlati tramite Stato) quindi, quando si imposta il campo Paese nel modulo Città (all'interno della classe CityType), sono costretto a dichiarare tale campo come 'property_path'=>false come è possibile vedere nel seguente codice:

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'); 
    } 
} 

il problema è che quando si tenta di modificare una città esistente, il relativo Paese non è selezionata per impostazione predefinita nella forma. Se rimuovo la linea 'property_path'=>false ottengo (non sorprendentemente) il messaggio di errore:

Né proprietà "paese", né il metodo "getCountry()" o il metodo "isCountry()" esiste in classe "Test \ TestBundle \ Entity \ Città "

Qualche idea?

risposta

23

OK, ho finalmente capito come farlo correttamente:

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'); 
    } 
} 

Il selettore jQuery AJAX

$(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); 
       } 
      }); 
     } 
    }); 
}); 

Spero che questo sarà utile a qualcun altro di fronte alla stessa situazione.

+0

da dove proviene il 'Routing.generate ('state_list'),' '? dove si trova il controller che gestisce la route 'state_list'? – gondo

+0

Ciao David, ottima risposta, mi chiedo solo: hai qualche idea di come utilizzare questa città creata quando selezioni, ad esempio, un indirizzo per una persona? Quindi selezionare un paese, che aggiorna gli stati, selezionare uno stato, che aggiorna le città? So che ha qualcosa a che fare con i Form Events, ma mi sembra che tu debba annidarli in qualche modo? Ho postato una domanda su questo [qui] (http://stackoverflow.com/questions/20544442/add-event-listener-to-form-element-added-by-event-listener). Qualche idea? – iLikeBreakfast

+0

come farlo nella versione 'Symfony 2.3' –

0

È necessario un FieldType dedicato per la casella di selezione concatenata. E anche un controller xhr che può restituire opzioni figlio in base al parametro passato. Ofcourse property_path dovrebbe essere impostato su false.

+0

Potresti mostrarmi come creare il FieldType dedicato? –

+0

http://symfony.com/doc/master/cookbook/form/create_custom_field_type.html – ken

+0

Non so come ci possa essere un FieldType dedicato.Il problema è precisamente che l'impostazione 'property_path = false' per il campo entità Paese, lo isola dall'entità Città e il suo stato correlato. –

7

Dal momento che il link a questo approccio è giù ho deciso di completare la vostra risposta eccellente in modo che chiunque può usarlo:

Al fine di eseguire il seguente comando javascript:

url: Routing.generate('state_list'), 

È necessario installare FOSJsRoutingBundle che può essere trovato in here.

ATENTION: nella sezione di lettura del pacchetto ci sono le istruzioni di installazione ma manca qualcosa. Se si utilizzano le dipendenze con questo:

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

È necessario eseguire il php bin/vendors update prima che i prossimi passi.

Sto ancora cercando di scoprire quale percorso è necessario nel routing.yml per far funzionare questa soluzione. Non appena scoprirò, modificherò questa risposta.