Нередка ситуация, когда в классе формы требуется создать несколько элементов <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 } |