自定义Symfony子类型自更新后表单数据丢失问题
嗨,我来帮你拆解下这个问题的根源,再给你一套可行的解决方案。
首先咱们先搞清楚为什么$form->getData()里的client字段会丢失:
你的ClientEntityType初始设置了空的choices,当表单提交时,Symfony的EntityType组件没办法把提交的ID字符串自动转换成Clients实体(因为它找不到对应的选项匹配)。这时候根表单在把表单数据映射到$data数组/对象时,会因为类型不匹配(比如你的业务逻辑期望Clients对象,但实际提交的是字符串ID)直接跳过这个字段,导致$form->getData()里没有client。
然后你在POST_SUBMIT事件里重新给父表单添加client字段,这时候表单的数据映射流程已经走完了——Symfony已经把表单数据处理完并同步到$data里了,新添加的字段不会反向更新根表单的$data,所以你还是拿不到它。而$form->get('client')->getData()能拿到值,是因为新字段手动设置了data属性,但这个新字段并没有参与根表单的数据映射流程。
那怎么解决呢?咱们换个思路,不用重建字段,而是用数据转换器+事件修改选项来处理:
解决方案:添加数据转换器,避免动态重建字段
步骤1:给ClientEntityType添加ID到实体的转换器
我们用Symfony自带的CallbackTransformer,手动把提交的ID字符串转换成Clients实体,这样不管有没有初始choices,都能正确解析提交的值。
步骤2:在POST_SUBMIT里修改当前字段的选项,而非重建字段
当验证失败需要重新渲染表单时,我们只需要给当前的client字段设置选中的选项,不用删除再重建字段,这样既保留了数据映射,又能正确显示选中项。
修改后的ClientEntityType代码如下:
use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvent; class ClientEntityType extends AbstractType { public function __construct( private readonly ClientsRepository $clientsRepository, private readonly RouterInterface $router, ) { } public function getParent(): string { return EntityType::class; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults($this->getOptions()); } public function buildForm(FormBuilderInterface $builder, array $options): void { // 1. 添加数据转换器:自动把提交的ID字符串转成Clients实体 $builder->addModelTransformer(new CallbackTransformer( // 正向转换:实体转表单渲染数据(EntityType自身会处理,直接返回实体即可) function (?Clients $client) { return $client; }, // 反向转换:提交的ID字符串转成实体 function ($clientId) { if (empty($clientId)) { return null; } return $this->clientsRepository->find($clientId); } )); // 2. 在POST_SUBMIT事件里修改当前字段的选项,用于验证失败后渲染选中项 $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void { $form = $event->getForm(); $client = $event->getData(); if ($client instanceof Clients) { // 直接修改当前字段的选项,设置选中的client为唯一选项 $newOptions = array_merge( $form->getConfig()->getOptions(), ['choices' => [(string) $client => $client]] ); $form->setConfig($form->getConfig()->setOptions($newOptions)); } }); } private function getOptions(): array { return [ 'class' => Clients::class, 'label' => 'Client', 'required' => false, 'attr' => [ 'class' => 'clientSelect', 'data-search-url' => $this->router->generate('admin_client_search'), ], 'placeholder' => 'Choisir une société', 'choices' => [], ]; } }
为什么这样能解决问题?
- 数据转换器会在表单提交时自动把ID字符串转换成
Clients实体,这样根表单在映射数据时,就能正确识别这个实体,把它放到$form->getData()的client字段里。 POST_SUBMIT事件里修改当前字段的choices,不会重建字段,只是更新选项,这样当验证失败需要重新渲染表单时,下拉框里会显示选中的client,同时不会影响已经完成的数据映射。- 彻底避免了动态修改父表单字段带来的数据不同步问题,逻辑更简洁也更符合Symfony表单的设计思路。
现在你再测试一下,$form->getData()里的client字段应该就能正常拿到Clients实体了,而且验证失败后表单也能正确显示选中的选项。
内容来源于stack exchange

