Drupal9 hook_form_alter修改Ajax表单提交报无效选择错误如何修复
报错原因
这个报错是Drupal 8/9版本相比Drupal7新增的表单安全校验机制导致的:
- Drupal 9基于Symfony表单组件,对所有select下拉元素默认开启选项白名单校验:表单提交时,服务端会校验用户提交的选项值是否存在于当前表单构建阶段生成的
#options列表中,不存在就直接抛出The value you selected is not a valid choice和Illegal choice错误,用来防止恶意用户篡改表单选项提交非法值,Drupal7没有这层强制校验,所以旧代码可以正常运行。 - 现有代码存在4个具体问题导致校验失败:
- AJAX回调存在选择器笔误:给
field_offices字段设置的包裹层id是ajax-wrapper-field-offices(复数),但回调里替换的选择器写的是#ajax-wrapper-field-office(单数,少了末尾的s),导致该字段的选项更新完全不生效。 #ajax配置格式错误:Drupal Form API的wrapper参数只支持传入单个字符串id,代码里传入了数组,会干扰核心AJAX流程的正常运行。- 动态选项设置时机错误:直接在
hook_form_alter里设置字段#options,会被后续字段widget自身的#process构建回调覆盖,导致提交校验阶段实际生效的选项列表被重置为初始空值/仅含_none的列表,用户通过AJAX选到的值不在白名单内。 - 初始选项生成逻辑错误:进入管理员角色判断分支后,直接将
$district_id覆盖为$form_state中读取的值,首次加载表单时$form_state还没有提交的district值,$district_id被设为false,导致两个下拉框初始选项为空/仅含提示项,和AJAX加载后的选项列表不一致。
补充:field_offices字段没报错是因为替换失效后一直保持初始空选项状态,若字段非必填,提交空值可以通过校验;field_user的AJAX替换正常,前端能看到加载后的选项,但提交阶段选项被重置,所以触发报错。
- AJAX回调存在选择器笔误:给
修复方法
按以下步骤调整代码即可:
- 修正AJAX回调的选择器笔误,保证两个字段的DOM替换都能命中目标元素。
- 移除非法的
wrapper数组配置,因为已经自定义AjaxResponse处理多元素替换,不需要核心自动替换逻辑。 - 将动态设置下拉选项的逻辑移到
#after_build回调中执行,该回调在所有#process流程跑完后触发,设置的#options不会被字段默认逻辑覆盖,保证初始加载、AJAX重建、提交校验三个阶段生成的选项列表完全一致。 - 修正
$district_id的取值逻辑,首次加载时默认取用户自身的所属片区id,只有$form_state中存在用户提交的片区值时才覆盖,保证初始选项正常生成。 - 移除
field_district字段重复、错误层级的默认值设置,options字段默认值只需设置在widget层级即可,避免值读取异常。
修正后的关键代码
/** * after_build回调处理动态选项,避免#options被字段默认逻辑覆盖 */ function bncreports_form_after_build(array $form, FormStateInterface $form_state) { $user = User::load(\Drupal::currentUser()->id()); if ($user->hasRole('administrator') || $user->hasRole('bnc_operator') || $user->hasRole('ao_user')) { // 优先读取form_state提交值,不存在则取用户自身默认片区值 $district_id = $form_state->hasValue('field_district') ? $form_state->getValue('field_district')[0]['value'] : $user->get('field_district')->value; $form['field_offices']['widget']['#options'] = $district_id ? Functions::officeNames($district_id) : []; $form['field_user']['widget']['#options'] = $district_id ? ['_none' => '- Select a value -'] + Functions::districtUserLastandFirstNames($district_id) : ['_none' => '- Select a value -']; } return $form; } function bncreports_form_node_bnc_message_form_alter(&$form, FormStateInterface $form_state, $form_id) { $form['actions']['submit']['#submit'][] = 'bncreports_node_bnc_message_form_submit'; $form['#title'] = t('Create Contact Us'); // 注册after_build回调 $form['#after_build'][] = 'bncreports_form_after_build'; $user = User::load(\Drupal::currentUser()->id()); $district_id = $user->get('field_district')->value; $form['field_first_name']['widget'][0]['value']['#default_value'] = $user->get('field_first_name')->value; $form['field_last_name']['widget'][0]['value']['#default_value'] = $user->get('field_last_name')->value; $form['field_email']['widget'][0]['value']['#default_value'] = $user->getEmail(); $form['field_email']['widget'][0]['value']['#attributes'] = ['onblur' => 'this.value=this.value.toLowerCase();']; $form['field_phone']['widget'][0]['value']['#default_value'] = $user->get('field_phone')->value; // 移除错误层级的default_value设置,只保留widget层级配置 $form['field_district']['widget']['#default_value'] = $district_id; if ($user->hasRole('administrator') || $user->hasRole('bnc_operator') || $user->hasRole('ao_user')) { $form['field_office']['#access'] = false; $form['field_office_name']['#access'] = false; $form['field_office_names']['#access'] = false; $form['field_site_codes']['#access'] = false; $form['field_district']['widget']['#ajax'] = [ 'event' => 'change', 'callback' => 'bncreports_ajax_offices_and_user', // 移除非法的wrapper数组配置,由自定义AjaxResponse处理替换 ]; $form['field_offices']['#prefix'] = '<div id="ajax-wrapper-field-offices">'; $form['field_offices']['#suffix'] = '</div>'; $form['field_user']['#prefix'] = '<div id="ajax-wrapper-field-user">'; $form['field_user']['#suffix'] = '</div>'; // 移除直接写在alter里的选项设置逻辑,统一交给after_build处理 } else { $form['bnc_prefix']['#markup'] = '<p>BNC User Support: ' . Constants::BNC_PHONE_NO . ' <a href="mailto:' . Constants::BNC_EMAIL . '">' . Constants::BNC_EMAIL . '</a></p>' . '<p><a href="' . Constants::AO_CONTACTS_URL . '">AO Program and CM/ECF Contacts</a></p>' . '<h4>Unable to find what you need? Send us a message.</h4>'; $form['bnc_prefix']['#weight'] = -1; $form['field_offices']['#access'] = false; $form['field_office_name']['#access'] = false; $form['field_office_names']['#access'] = false; $form['field_site_codes']['#access'] = false; $form['field_user']['#access'] = false; $form['field_non_user_name']['#access'] = false; $form['field_non_user_phone']['#access'] = false; $form['field_assigned_to']['#access'] = false; $form['revision_information']['#access'] = false; $office = $user->get('field_office')->value; $form['field_office']['widget']['#options'] = Functions::officeNames($district_id); $form['field_office']['widget']['#default_value'] = $office; $form['field_office_name']['widget'][0]['value']['#default_value'] = Functions::officeName($district_id, $office); $form['#attached']['library'][] = 'bncreports/restricted_contact_log'; } } function bncreports_ajax_offices_and_user(array $form, FormStateInterface $form_state): AjaxResponse { $response = new AjaxResponse(); // 修正选择器笔误,和包裹层id保持一致(offices为复数) $response->addCommand(new ReplaceCommand('#ajax-wrapper-field-offices', $form['field_offices'])); $response->addCommand(new ReplaceCommand('#ajax-wrapper-field-user', $form['field_user'])); return $response; }
内容的提问来源于stack exchange,提问作者user4202824
相关产品推荐
相关产品推荐

