Symfony动态下拉表单提交验证失败:所选选项无效
Symfony动态表单提交验证错误:The selected choice is invalid
问题描述
开发Symfony项目时,实现了基于用户选择动态填充下拉框的功能:选择品牌后更新对应型号下拉选项。采用AJAX处理动态更新,并用PRE_SUBMIT事件在提交前修改表单,但提交表单时Symfony验证失败,抛出错误:The selected choice is invalid。
代码详情
AnnonceFormType
public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('category', ChoiceType::class, [ 'label' => $this->translator->trans('Sélectioner la catégorie *'), 'choices' => [ $this->translator->trans('Auto') => $this->translator->trans('auto'), $this->translator->trans('Moto') => $this->translator->trans('moto'), ], 'expanded' => true, 'multiple' => false, 'attr' => [ 'class' => 'form-check-input', 'name' => 'category', ], ]) ->add('marques', ChoiceType::class, [ 'label' => false, 'choices' => [], 'placeholder' => $this->translator->trans('Sélectionner la marque'), ]) ->add('modeles', ChoiceType::class, [ 'label' => false, 'choices' => [], 'placeholder' => $this->translator->trans('Sélectionner le modéle'), ]); $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); if (isset($data['category'])) { $marques = $this->getMarques($data['category']); $form->add('marques', ChoiceType::class, [ 'choices' => array_combine(array_map(function($marque) { return $marque; }, $marques), array_map(function($marque) { return $marque; }, $marques)), 'choice_label' => function ($marque) { return $marque; // Afficher le titre }, 'choice_value' => function ($marque) { return $marque; // Persister le titre }, 'placeholder' => $this->translator->trans('Sélectionner une marque'), 'label' => false, ]); } if (isset($data['marques'])) { dump($data['marques']); $marqueId = array_search($data['marques'], array_flip($this->getMarques($data['category']))); dump($marqueId); if($marqueId !== false){ $modeles = $this->getModeles($data['marques']); $form->add('modeles', ChoiceType::class, [ 'choices' => array_combine(array_map(function($modele) { return $modele; }, $modeles), array_map(function($modele) { return $modele; }, $modeles)), 'choice_label' => function ($modele) { return $modele; }, 'choice_value' => function ($modele) { return $modele; }, 'placeholder' => $this->translator->trans('Sélectionner un modèle'), 'label' => false, ]); } } }); } private function getMarques(?string $category): array { if (!$category) { return []; } $marques = $this->marqueRepository->findMarques($category); $choices = []; foreach ($marques as $marque) { $choices[$marque->getId()] = $marque->getTitle(); } return $choices; } private function getModeles(?int $marqueId): array { if (!$marqueId) { return []; } $modeles = $this->modeleRepository->findModeles($marqueId); $choices = []; foreach ($modeles as $modele) { $choices[$modele->getId()] = $modele->getTitleModele(); } return $choices; }
控制器
#[Route('/{_locale}/getmarques/{type}', name: 'app_getmarque')] public function getMarques(string $type) { $marques = $this->marqueRepository->findMarques($type); dump($marques); return $this->render('addannonce/getmarques.html.twig', [ 'marques' => $marques, ]); } #[Route('/{_locale}/getmodeles/{marque}', name: 'app_getmodele')] public function getModeles(int $marque) { dump($marque); $modeles = $this->modeleRepository->findModeles($marque); dump($modeles); return $this->render('addannonce/getmodeles.html.twig', [ 'modeles' => $modeles, ]); }
AJAX代码
if (jQuery) { // Fonction pour récupérer les marques en fonction du type sélectionné function getMarques(type) { let path = $("div.s-submit").data("path-getmarque"); if (path) { path = path.replace("__type__", type); $.ajax({ url: path, dataType: "html", type: "get", cache: false, beforeSend: function () { // Affiche un indicateur de chargement $("#AnnonceType1_marques").html("<option>Chargement des données...</option>"); }, success: function (data) { console.log("getMarques", data); $("#AnnonceType1_marques").html(data); }, error: function (xhr, status, error) { console.log("Erreur AJAX:", status, error); alert( "Une erreur est survenue lors de la récupération des marques. Veuillez réessayer." ); }, }); } else { console.log("Erreur: attribut de données non trouvé"); } } // Lorsque la sélection de marque change, récupérer les modèles correspondants $("#AnnonceType1_marques").on("change", function () { let marqueId = $(this).val(); let marqueText = $(this).find("option:selected").text(); console.log("Marque sélectionnée:", marqueText); console.log("ID de la marque sélectionnée:", marqueId); if (marqueId) { let path = $("div.s-submit").data("path-getmodele"); if (path) { path = path.replace("__id__", marqueId); $.ajax({ url: path, data: { marque: marqueId }, dataType: "html", type: "get", cache: false, beforeSend: function () { // Affiche un indicateur de chargement $("#AnnonceType1_modeles").html("<option>Chargement des données...</option>"); }, success: function (data) { console.log("getModeles", data); $("#AnnonceType1_modeles").html(data); }, error: function (xhr, status, error) { console.log("Erreur AJAX:", status, error); alert( "Une erreur est survenue lors de la récupération des modèles. Veuillez réessayer." ); }, }); } } else { $("#AnnonceType1_modeles").html( '<option value="">Sélectionner la marque</option>' ); } }); // Log du modèle sélectionné $("#AnnonceType1_modeles").on("change", function () { let modeleId = $(this).val(); let modeleTitle = $(this).find("option:selected").text(); console.log("ID du modèle sélectionné:", modeleId); console.log("Texte du modèle sélectionné:", modeleTitle); }); } else { console.log("Erreur: jQuery n'est pas chargé"); }
模板文件
{# getmarques.html.twig #} {% if(marques) %} <option value="">--{{"Sélectionner"|trans }}--</option> {% for marque in marques %} <option value="{{ marque.id}}">{{ marque.title }}</option> {% endfor %} {% else %} {{"pas de marque"|trans }} {% endif %} {# getmodeles.html.twig #} {% if(modeles) %} <option value="">--{{"Sélectionner"|trans }}--</option> {% for modele in modeles %} <option value="{{ modele.id }}"> {{ modele.titleModele }} </option> {% endfor %} {% else %} {{"pas de modéle"|trans }} {% endif %}
已尝试方案
- 使用AJAX在用户选择类型/品牌时动态获取对应品牌和型号
- 添加
PRE_SUBMIT事件监听器,在提交前根据提交数据更新表单选项 - 在
PRE_SUBMIT事件中重新配置ChoiceType字段的选项
预期效果
表单可根据所选品牌动态更新型号,提交品牌和型号的标题(而非ID),数据能正确持久化到数据库且无验证错误。
解决方案
问题根源
前端提交的是品牌/型号的ID,但PRE_SUBMIT事件中配置的ChoiceType选项把标题作为choice_value,导致Symfony验证时无法匹配提交的ID,抛出"The selected choice is invalid"错误。同时,代码中对choices的处理(array_combine将标题同时作为键和值)完全偏离了getMarques/getModeles返回的[ID => 标题]结构,进一步加剧了不匹配。
修正步骤
1. 修正PRE_SUBMIT事件中的表单配置
保持choices为getMarques/getModeles返回的[ID => 标题]结构,让choice_value默认使用ID(与前端提交值一致),choice_label默认使用标题,同时将提交的ID转换为标题:
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); // 更新品牌选项 if (isset($data['category'])) { $marques = $this->getMarques($data['category']); $form->add('marques', ChoiceType::class, [ 'choices' => $marques, 'placeholder' => $this->translator->trans('Sélectionner une marque'), 'label' => false, ]); } // 更新型号选项,并将提交的ID转换为标题 if (isset($data['marques']) && isset($data['category'])) { $marqueId = (int)$data['marques']; $marques = $this->getMarques($data['category']); // 验证品牌ID是否有效 if (isset($marques[$marqueId])) { // 将提交的品牌ID替换为标题 $data['marques'] = $marques[$marqueId]; $event->setData($data); // 获取对应型号并更新表单选项 $modeles = $this->getModeles($marqueId); $form->add('modeles', ChoiceType::class, [ 'choices' => $modeles, 'placeholder' => $this->translator->trans('Sélectionner un modèle'), 'label' => false, ]); // 如果有型号提交,同样将ID转换为标题 if (isset($data['modeles'])) { $modeleId = (int)$data['modeles']; if (isset($modeles[$modeleId])) { $data['modeles'] = $modeles[$modeleId]; $event->setData($data); } } } } });
2. 保留初始表单字段配置
初始的marques和modeles字段无需额外修改,保持空choices即可:
->add('marques', ChoiceType::class, [ 'label' => false, 'choices' => [], 'placeholder' => $this->translator->trans('Sélectionner la marque'), ]) ->add('modeles', ChoiceType::class, [ 'label' => false, 'choices' => [], 'placeholder' => $this->translator->trans('Sélectionner le modéle'), ])
修正逻辑说明
- 前端提交的ID与表单选项的
choice_value(ID)匹配,验证通过 - 在
PRE_SUBMIT事件中将提交的ID转换为对应标题,更新提交数据,满足持久化标题的需求 - 直接复用
getMarques/getModeles返回的结构,避免不必要的数组操作,减少出错概率
内容的提问来源于stack exchange,提问作者kasali
相关产品推荐
相关产品推荐

