Сложная (многоуровневая) динамическая форма в Symfony
Мне нужно создать форму добавления факультета в базу данных. Сначала пользователь выбирает регион из списка (<select> / ChoiceType), затем город этого региона из следующего списка, выбирает университет и, наконец, вводит название факультета. Значения по умолчанию - это первый регион из базы данных, его первый город и первый университет.
Отправка страницы с данными по умолчанию работает, выбор региона работает, но выбор города возвращает статус 500. Из описания ошибки следует, что на изменение города реагирует слушатель $builder->get('region')->addEventListener(), а не $builder->get('city')->addEventListener(). Как это исправить?
Форма:
Ошибка:
Twig и ajax:
{% extends 'admin/insert/insert.html.twig' %}
{% block title %}Add Faculty{% endblock %}
{% block body %}
<div class="insert">
<h1 class="insert__title">Add Faculty</h1>
{{ form_start(insert_faculty, { 'attr' : {'class' : 'insert__form'} }) }}
{% for message in app.flashes('success') %}
<div class="insert__success">
{{ message }}
</div>
{% endfor %}
<div class="insert__errors">
{{ form_errors(insert_faculty) }}
</div>
{{ form_label(insert_faculty.region, 'Region:', { 'label_attr' : {'class' : 'insert__label'} }) }}
{{ form_widget(insert_faculty.region, { 'attr' : {'class' : 'insert__input'} }) }}
{{ form_label(insert_faculty.city, 'City:', { 'label_attr' : {'class' : 'insert__label'} }) }}
{{ form_widget(insert_faculty.city, { 'attr' : {'class' : 'insert__input'} }) }}
{{ form_label(insert_faculty.university, 'University:', { 'label_attr' : {'class' : 'insert__label'} }) }}
{{ form_widget(insert_faculty.university, { 'attr' : {'class' : 'insert__input'} }) }}
{{ form_label(insert_faculty.name, 'Name:', { 'label_attr' : {'class' : 'insert__label'} }) }}
{{ form_widget(insert_faculty.name, { 'attr' : {'class' : 'insert__input insert__input_name'} }) }}
<button type="submit" class="insert__button">Save</button>
{{ form_end(insert_faculty) }}
<div class="insert__buttons">
<a href="{{ path('insert') }}" class="insert__button">Back</a>
</div>
</div>
{% block javascripts_footer %}
{{ parent() }}
<script>
let $region = $('#insert_faculty_region');
$region.change(function() {
let $form = $(this).closest('form');
let data = {};
data[$region.attr('name')] = $region.val();
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
success: function(get) {
$('#insert_faculty_city').html(
$(get).find('#insert_faculty_city').html()
);
$('#insert_faculty_university').html(
$(get).find('#insert_faculty_university').html()
);
}
});
});
let $city = $('#insert_faculty_city');
$city.change(function() {
let $form = $(this).closest('form');
let data = {};
data[$city.attr('name')] = $city.val();
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
success: function(get) {
$('#insert_faculty_university').html(
$(get).find('#insert_faculty_university').html()
);
}
});
});
</script>
{% endblock %}
{% endblock %}
Класс формы:
class InsertFacultyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Список регионов
->add('region', ChoiceType::class, [
'choices' => $options['regions_array'],
'mapped' => false,
])
// Список городов
->add('city', ChoiceType::class, [
'choices' => null,
'mapped' => false,
])
// Список вузов
->add('university', ChoiceType::class, [
'choices' => null,
])
// Название факультета
->add('name')
;
// Функция изменения формы
$formModifier = function (FormInterface $form, $entity) {
// Если изменён регион
if (get_class($entity) === 'App\Entity\Region') {
// Если у него нет городов, отправляем null
if (!$entity->getCities()->count()) {
$form->add('city', ChoiceType::class, [
'choices' => null,
'mapped' => false,
]);
}
else {
// Формируем массив, где ключ (<option>) - имя города, значение - сам город
$cities_in_database = $entity->getCities();
foreach ($cities_in_database as $city) {
$cities[$city->getName()] = $city;
}
// Обновляем поле формы
$form->add('city', ChoiceType::class, [
'choices' => $cities,
'mapped' => false,
]);
}
}
// Если изменён регион
else if (get_class($entity) === 'App\Entity\City') {
// Если у него нет вузов, отправляем null
if (!$entity->getUniversities()->count()) {
$form->add('university', ChoiceType::class, [
'choices' => null,
]);
}
else {
// Формируем массив, где ключ (<option>) - имя вуза, значение - сам вуз
$university_in_database = $entity->getUniversities();
foreach ($university_in_database as $university) {
$universities[$university->getName()] = $university;
}
// Обновляем поле формы
$form->add('university', ChoiceType::class, [
'choices' => $universities,
]);
}
}
};
// Начальные данные
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($options, $formModifier, $builder) {
$region = $options['regions_array'][array_key_first($options['regions_array'])];
$city = $region->getCities()[0];
$formModifier($event->getForm(), $region);
$formModifier($event->getForm(), $city);
}
);
// Изменение региона
$builder->get('region')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$region = $event->getForm()->getData();
$city = $region->getCities()[0];
$formModifier($event->getForm()->getParent(), $region);
$formModifier($event->getForm()->getParent(), $city);
}
);
// Изменение города
$builder->get('city')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$city = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $city);
}
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Faculty::class,
'regions_array' => null,
]);
$resolver->setAllowedTypes('regions_array', 'array');
}
}
Контроллер:
/**
* @Route("/admin/insert/faculty", name="faculty")
*/
public function faculty(Request $request)
{
// Получаем список регионов
$regions_in_database = $this->getDoctrine()->getRepository(Region::class)->findAll();
// Записываем их в массив, где индекс - имя региона
$regions = [];
foreach ($regions_in_database as $region) {
$regions[(string)$region->getName()] = $region;
}
// Создаём пустой класс факультета
$faculty = new Faculty();
// и форму под него
$insert_form = $this->createForm(InsertFacultyType::class, $faculty, [
'regions_array' => $regions,
]);
// Проверяем наличие регионов
if (!$regions_in_database) {
$insert_form->addError(new FormError("Отсутствуют регионы!
Пожалуйста, перейдите по ссылке /insert/region и добавьте соответствующий регион"));
}
// Отправка формы
$insert_form->handleRequest($request);
if ($insert_form->isSubmitted() && $insert_form->isValid()) {
// Проверяем, есть ли уже такой факультет в базе
$repository = $this->getDoctrine()->getRepository(University::class);
$faculty_in_database = $repository->findOneBy(
[
'name' => $faculty->getName(),
'university' => $faculty->getUniversity(),
]
);
// Если есть, выдаём ошибку
if ($faculty_in_database) {
$insert_form->addError(new FormError('Такой факультет уже есть в базе!'));
}
else {
$faculty->setRating(0);
// Проверяем, был ли отправлен университет
if(!$faculty->getUniversity()) {
$insert_form->addError(new FormError("Укажите университет!"));
}
// Сохраняем факультет в базу
else {
$entity_manager = $this->getDoctrine()->getManager();
$entity_manager->persist($faculty);
$entity_manager->flush();
$this->addFlash(
'success',
'Факультет "' . $faculty->getName() . '" успешно сохранён!'
);
}
}
}
return $this->render('admin/insert/faculty/faculty.html.twig', [
'insert_form' => $insert_form->createView(),
]);
}


