Нередка ситуация, когда в классе формы требуется создать несколько элементов <select>, значения которых зависят друг от друга. Типичный пример — страны-города.
На гитхабе есть решения, но они в составе бандлов. Предлагаю своё решение.
Сразу, как будет выглядеть класс нашей формы:
| 1 2 3 4 5 6 7 8 9 10 11 12 | $builder     ->add('country', 'entity', array(         'class' => 'AppBundle:Country',         'empty_value'=> '== Choose country ==',         'required' => false,     ))     ->add('city', 'my_dependent_entity', array(         'class' => 'AppBundle:City',         'property' => 'name',         'parent_field' => 'country',     )) ; | 
Два поля — страны и города — список городов будет формироваться в зависимости от выбранной страны. Поле city имеет тип my_dependent_entity, который нам предстоит ещё создать.
Полный листинг класса DependentEntityType
| 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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | namespace MyBundle\Form\Type; use MyBundle\Form\DataTransformer\EntityToIdTransformer; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Exception\InvalidConfigurationException; use Symfony\Component\OptionsResolver\OptionsResolver; use Doctrine\Common\Persistence\ObjectManager; class DependentEntityType extends AbstractType {     private $om;     public function __construct( ObjectManager $om )     {         $this->om = $om;     }     public function buildForm(FormBuilderInterface $builder, array $options)     {         if (null === $options['class']) {             throw new InvalidConfigurationException('Option "class" is empty');         }         if (null === $options['parent_field']) {             throw new InvalidConfigurationException('Option "parent_field" is empty');         }         if (null === $options['property']) {             throw new InvalidConfigurationException('Option "property" is empty');         }         $builder->addViewTransformer(new EntityToIdTransformer(             $this->om,             $options['class']         ), true);         $query = $options['query'];         if ($query instanceof \Closure)         {             $queryBuilder = $query($this->om->getRepository($options['class']));             $query = $queryBuilder->getQuery()->getDql();         }         $builder->setAttribute('class',           $options['class']);         $builder->setAttribute("parent_field",    $options['parent_field']);         $builder->setAttribute("no_result_msg",   $options['no_result_msg']);         $builder->setAttribute("empty_value",     $options['empty_value']);         $builder->setAttribute("property",        $options['property']);         $builder->setAttribute("em_name",         $options['em_name']);         $builder->setAttribute('query',           $query);         $builder->setAttribute("order_direction", $options['order_direction']);         $builder->setAttribute("order_property",  $options['order_property']);     }     public function buildView(FormView $view, FormInterface $form, array $options)     {         $view->vars['class']           = $form->getConfig()->getAttribute('class');         $view->vars['parent_field']    = $form->getConfig()->getAttribute('parent_field');         $view->vars['no_result_msg']   = $form->getConfig()->getAttribute('no_result_msg');         $view->vars['empty_value']     = $form->getConfig()->getAttribute('empty_value');         $view->vars['property']        = $form->getConfig()->getAttribute('property');         $view->vars['em_name']         = $form->getConfig()->getAttribute('em_name');         $view->vars['query']           = $form->getConfig()->getAttribute('query');         $view->vars['order_direction'] = $form->getConfig()->getAttribute('order_direction');         $view->vars['order_property']  = $form->getConfig()->getAttribute('order_property');     }     public function configureOptions(OptionsResolver $resolver)     {         $resolver->setDefaults(array(             'class'             => null,             'empty_value'       => '',             'parent_field'      => null,             'property'          => null,             'compound'          => false,             'em_name'           => 'default',             'query'             => null,             'no_result_msg'     => 'No result',             'order_direction'   => 'ASC',             'order_property'    => 'id',         ));     }     public function getParent()     {         return 'form';     }     public function getName()     {         return 'my_dependent_entity';     } } | 
Опции $resolver->setDefaults говорят сами за себя, обязательные из них — это class, property, parent_field. Так-же класс использует трансформер EntityToIdTransformer, трансформирующий коллекцию сущностей в массив, его листинг:
| 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 | namespace AppBundle\Form\DataTransformer; use Symfony\Component\Form\DataTransformerInterface; use Doctrine\ORM\EntityManager; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Exception\InvalidConfigurationException; class EntityToIdTransformer implements DataTransformerInterface {     protected         $em,         $class,         $unitOfWork     ;     public function __construct(EntityManager $em, $class)     {         $this->em = $em;         $this->unitOfWork = $this->em->getUnitOfWork();         $this->class = $class;     }     public function transform($entity)     {         if (null === $entity || '' === $entity){             return 'null';         }         if (!is_object($entity)) {             throw new UnexpectedTypeException($entity, 'object');         }         if (!$this->unitOfWork->isInIdentityMap($entity)) {             throw new InvalidConfigurationException('Entities passed to the choice field must be managed');         }         return $entity->getId();     }     public function reverseTransform($id)     {         if ('' === $id || null === $id) {             return null;         }         if (!is_numeric($id)) {             throw new UnexpectedTypeException($id, 'numeric ' . $id);         }         $entity = $this->em->getRepository($this->class)->findOneById($id);         if ($entity === null) {             throw new TransformationFailedException(sprintf('The entity with key "%s" could not be found', $id));         }         return $entity;     } } | 
Следующим шагом будет создание виджета-отображения нашего типа my_dependent_entity.
| 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 | {# MyBundle/Resources/views/form_vidget.html.twig #} {% block my_dependent_entity_widget %}     {% set attr = attr|merge({ 'class': (attr.class|default('') ~ ' form-control')|trim }) %}     <select {{ block('widget_attributes') }}></select>     <script type="text/javascript">         $(function(){             $("#{{ form.parent.offsetGet( parent_field ).vars.id }}").change( function() {                 var selected_index = {{ value ? value : 0 }};                 $.ajax({                     type: "POST",                     data: {                         'class': "{{ class|raw }}",                         'parent_id': $(this).val(),                         'empty_value': "{{ empty_value }}",                         'parent_field': "{{ parent_field }}",                         'property': "{{ property }}",                         'em_name': "{{ em_name }}",                         'query': "{{ query|url_encode }}",                          'order_property': "{{ order_property }}",                         'order_direction': "{{ order_direction }}",                         'no_result_msg': "{{ no_result_msg }}",                     },                     url:"{{ path('my_dependent_entity') }}",                     success: function(msg){                         if (msg != ''){                             $("#{{ form.vars.id }}").html(msg).show();                             $.each($("#{{ form.vars.id }} option"), function (index, option){                                 if ($(option).val() == selected_index)                                     $(option).prop('selected', true);                             })                             $("#{{ form.vars.id }}").removeClass('zk2_loader');                         } else {                             $("#{{ form.vars.id }}").html('<em>{{ no_result_msg|trans() }}</em>');                             $("#{{ form.vars.id }}").removeClass('zk2_loader');                         }                     },                     error: function(xhr, ajaxOptions, thrownError){                     $('html').html(xhr.responseText);                     }                 });             });             $("#{{ form.parent.offsetGet( parent_field ).vars.id }}").trigger('change');         });     </script> {% endblock %} | 
Чтобы ядро Symfony «подхватило» этот файл, надо ему о нём сообщить. Например в файле app/config/config.yml:
| 1 2 3 4 5 | twig:     .........     form:         resources:             - "AppBundle::form_widget.html.twig" | 
Для выполнения ajax запросов необходим контроллер:
| 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 | namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; class DependentEntityController extends Controller {     public function dependentEntityAction(Request $request)     {         $translator = $this->get('translator');         $em_name    = $request->get('em_name');         $em = $this->get('doctrine')->getManager( $em_name );         $parent_id        = $request->get('parent_id');         $empty_value      = $request->get('empty_value');         $class            = $request->get('class');         $parent_field     = $request->get('parent_field');         $property         = $request->get('property');         $order_property   = $request->get('order_property');         $order_direction  = $request->get('order_direction');         $no_result_msg    = $request->get('no_result_msg');         if( !$query = $request->get('query') )         {             $query = sprintf(                 "SELECT e.id,e.%s FROM %s e WHERE e.id<>0 ",                 $property, $class             );             $rootAlias = 'e';         }         else         {             $query = urldecode( $query );             $rootAlias = substr($query, 7, 1);         }         $query .= sprintf(             " AND %s.%s='%s' ORDER BY %s.%s %s",             $rootAlias, $parent_field, $parent_id, $rootAlias, $order_property, $order_direction         );         $results = $em->createQuery( $query )             ->getScalarResult()         ;         $html = '';         if (empty($results))         {             return new Response('<option value="">' . $translator->trans($no_result_msg) . '</option>');         }         if ($empty_value)         {             $html .= '<option value="">' . $translator->trans($empty_value) . '</option>';         }         foreach($results as $result)         {             $html .= sprintf("<option value=\"%d\">%s</option>",$result['id'], $result[$property]);         }         return new Response($html);     } } | 
Регистрируем его в routing.yml
| 1 2 3 | my_dependent_entity:     pattern:  /my_dependent_entity     defaults: { _controller: AppBundle:DependentEntity:dependentEntity } | 
Осталось только зарегистрировать созданную нами форму, как сервис с тэгом form.type, и можно пользоваться
| 1 2 3 4 5 6 7 | # servises.yml     app.type.dependent_entity:         class: AppBundle\Form\Type\DependentEntityType         arguments: [@doctrine.orm.entity_manager]         tags:             - { name: form.type, alias: my_dependent_entity } | 





