Как создать зависимый выбор (зависимый выпадающий список) в Symfony 3

Работать с Symfony Forms довольно просто, круто и функционально. Они уже решают многие проблемы, которые теоретически можно игнорировать, потому что вам нужно только позаботиться о их создании. Для таких ленивых, как я, есть что-то удивительное — создание CRUD-форм (создание, обновление и удаление) из сущности. Это построить целый модуль с контроллером и правильно просмотреть, что готово к использованию. Хотя автоматическая сборка решает много проблем, к сожалению, в Symfony по умолчанию не существует функции автоматического зависимого выбора, что означает, что вам придется реализовать ее самостоятельно.

В соответствии с дизайном базы данных вам потребуется зависимый выбор в форме, чтобы показать пользователю только те строки, которые связаны с другим выбором в той же форме. Наиболее типичным примером в реальной жизни (и легче понять) является Person, City а также Neighborhood отношения. Рассмотрим следующие объекты, первый Person.php:

Заметка

Мы пропустим методы получения и установки в классе, чтобы сделать статью короче, однако они, очевидно, должны существовать.

Каждый человек, который может быть зарегистрирован в системе, должен проживать в городе из нашей базы данных, а также жить в определенном районе нашей базы данных, поэтому City.php юридическое лицо:

И Neighborhood.php юридическое лицо:

Субъекты полностью действительны и нам не нужно изменять их вообще во время этого урока, однако они полезны для понимания того, что мы собираемся делать. Если вы автоматически создадите Форму из этих объектов, в поле Соседство в Форме Персона вы найдете все окрестности без фильтрации только тех Соседств, которые принадлежат выбранному городу:

Реализация зависимого выбора в Symfony 3

Вот почему нам нужно реализовать зависимый отбор, поэтому, когда пользователь выбирает, например, Сан-Франциско в качестве своего города, при выборе Соседства он должен найти только 2 района, которые принадлежат Сан-Франциско (Остров сокровищ и Президио Сан-Франциско). Фильтрация запроса в FormType проста, однако это должно быть сделано динамически и с помощью JavaScript, так что этого легко достичь, выполнив следующие шаги:

1. Настройте FormType правильно

Логика для создания зависимой выборки заключается в следующем, изначально выбор соседей (зависимых) будет пустым, пока пользователь не выберет город, используя идентификатор выбранного города, вы должны загрузить новые опции в выбор соседства. Однако, если вы редактируете форму Person, выбранная окрестность должна отображаться автоматически, без необходимости использования JavaScript в представлении редактирования. Вот почему вам нужно изменить FormType вашей формы, в данном случае PersonType. Для начала вам необходимо прикрепить 2 слушателя событий к форме, которые выполняются при событиях. PRE_SET_DATA а также PRE_SUBMIT формы срабатывают. Внутри событий вы проверите, есть ли выбранный Город в форме или нет, если вы его отправите в качестве аргумента методу addElements.

addElements В качестве второго аргумента метод ожидает, что объект City (или ноль) решит, какие данные будут отображаться в выбранном Соседстве:

Заметка

FormType должен получить Entity Manager в конструкторе, так как вам нужно будет сделать несколько запросов внутри. В зависимости от вашей версии Symfony, это автоматически делается автоматическое связывание, если это не сделано автоматически, вам может потребоваться передать его в качестве аргумента в конструкторе контроллеров, в которые преобразуется класс.

em = $em;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// 2. Remove the dependent select from the original buildForm as this will be
// dinamically added later and the trigger as well
$builder->add('name')
->add('lastName');
// 3. Add 2 event listeners for the form
$builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData'));
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'onPreSubmit'));
}
protected function addElements(FormInterface $form, City $city = null) {
// 4. Add the province element
$form->add('city', EntityType::class, array(
'required' => true,
'data' => $city,
'placeholder' => 'Select a City...',
'class' => 'AppBundle:City'
));
// Neighborhoods empty, unless there is a selected City (Edit View)
$neighborhoods = array();
// If there is a city stored in the Person entity, load the neighborhoods of it
if ($city) {
// Fetch Neighborhoods of the City if there's a selected city
$repoNeighborhood = $this->em->getRepository('AppBundle:Neighborhood');
$neighborhoods = $repoNeighborhood->createQueryBuilder("q")
->where("q.city = :cityid")
->setParameter("cityid", $city->getId())
->getQuery()
->getResult();
}
// Add the Neighborhoods field with the properly data
$form->add('neighborhood', EntityType::class, array(
'required' => true,
'placeholder' => 'Select a City first ...',
'class' => 'AppBundle:Neighborhood',
'choices' => $neighborhoods
));
}
function onPreSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
// Search for selected City and convert it into an Entity
$city = $this->em->getRepository('AppBundle:City')->find($data['city']);
$this->addElements($form, $city);
}
function onPreSetData(FormEvent $event) {
$person = $event->getData();
$form = $event->getForm();
// When you create a new person, the City is always empty
$city = $person->getCity() ? $person->getCity() : null;
$this->addElements($form, $city);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Person'
));
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_person';
}
}

Просто правильно настроив FormType, если вы попытаетесь создать нового персонажа, поле Соседство будет пустым, и вы пока не сможете ничего сохранять, а если вы попытаетесь отредактировать человека, вы увидите, что поле Соседство только загружает окрестности, связанные с выбранным городом.

2. Создайте конечную точку для динамического отображения окрестностей города.

В качестве следующего шага вам нужно создать доступную конечную точку ajax, которая будет возвращать окрестности города (идентификатор города отправляется через параметр get, а именно cityid), так что вы можете создавать его где угодно и как хотите. В этом примере мы решили написать его в том же контроллере Person:

getDoctrine()->getManager();
$neighborhoodsRepository = $em->getRepository("AppBundle:Neighborhood");
// Search the neighborhoods that belongs to the city with the given id as GET parameter "cityid"
$neighborhoods = $neighborhoodsRepository->createQueryBuilder("q")
->where("q.city = :cityid")
->setParameter("cityid", $request->query->get("cityid"))
->getQuery()
->getResult();
// Serialize into an array the data that we need, in this case only name and id
// Note: you can use a serializer as well, for explanation purposes, we'll do it manually
$responseArray = array();
foreach($neighborhoods as $neighborhood){
$responseArray[] = array(
"id" => $neighborhood->getId(),
"name" => $neighborhood->getName()
);
}
// Return array with structure of the neighborhoods of the providen city id
return new JsonResponse($responseArray);
// e.g
// [{"id":"3","name":"Treasure Island"},{"id":"4","name":"Presidio of San Francisco"}]
}
}

В этом проекте наши маршруты определяются через файл yml (routing.yml), и маршрут будет выглядеть так:

# AppBundle/Resources/config/routing/person.yml
person_list_neighborhoods:
path:     /get-neighborhoods-from-city
defaults: { _controller: "AppBundle:Person:listNeighborhoodsOfCity" }
methods:  GET

Как только конечная точка станет доступной, вы можете проверить ее вручную, получив доступ к маршруту. Важно то, что контроллер должен возвращать ответ JSON с массивом, который содержит окрестности, которые принадлежат нужному городу.

3. Напишите JavaScript для обработки изменения города

В качестве последнего шага нам нужно сделать так, чтобы при изменении пользователем City окрестности обновлялись данными ранее созданного контроллера. Для этого вам нужно написать свой собственный JavaScript и сделать AJAX-запрос к ранее созданной конечной точке. Эта часть полностью зависит от JS-фреймворков, которые вы используете, или от того, как вам нравится работать с JavaScript. Чтобы сделать наш пример универсальным, мы будем использовать jQuery.

Логика должна быть помещена в оба вида формы (новый и редактировать), например, в нашем new.html.twig код будет:

{# views/new.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
Person creation
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}

{% endblock %}
{% block javascripts %}
$('#appbundle_person_city').change(function () {
var citySelector = $(this);
// Request the neighborhoods of the selected city.
$.ajax({
url: "{{ path('person_list_neighborhoods') }}",
type: "GET",
dataType: "JSON",
data: {
cityid: citySelector.val()
},
success: function (neighborhoods) {
var neighborhoodSelect = $("#appbundle_person_neighborhood");
// Remove current options
neighborhoodSelect.html('');
// Empty value ...
neighborhoodSelect.append('');
$.each(neighborhoods, function (key, neighborhood) {
neighborhoodSelect.append('');
});
},
error: function (err) {
alert("An error ocurred while loading data ...");
}
});
});
{% endblock %}

Если все было правильно реализовано, когда пользователь пытается создать новый регистр с формой, окрестности выбранного города будут загружены при изменении выбора. Кроме того, благодаря событиям формы Symfony, окрестности будут автоматически загружаться в поле на стороне сервера, когда пользователь редактирует форму:

Зависимый выбор Symfony 3

Ссылка на основную публикацию