我是一名 Symfony 新手,正在开发一个应用程序(使用 Doctrine)来管理心理学实践的日程安排和计费。
实体
Invoice
与实体Session
具有一对多关系。所以我的表单类有一个 EntityType
元素,默认情况下,Symfony 会获取数据库中的 all 会话来填充多选元素。这是我不需要的大量数据,此外还有一个令人讨厌的 N+1 问题(我还没有弄清楚如何优化)。我想要做的是不渲染这个EntityType元素并且(显然)不使用任何选项填充它。相反,用户从下拉列表中选择 Patient
实体,然后通过 xhr 调用,我们获取属于该患者且尚未与任何发票关联的会话(数据量要少得多)并进行渲染选项作为复选框。所以,在形式类中:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('invoice_date', DateType::class, ['widget'=>'single_text',])
->add('payer',HiddenType::class)
->add('sessions',null, [
// this does not solve N+1
/*'query_builder'=>function(SessionRepository $repository)
{return $repository->createQueryBuilder('s')->leftJoin('s.invoice','invoice')
->join('s.patients','patients')
->leftJoin('patients.payer','payer')
->andWhere('s.invoice is null')
->addSelect('patients','payer');},
*/
// ...but with this, it does not assign our input to the form
'choices'=>[],
])
->add('patient',EntityType::class,[
'placeholder'=>'',
'class'=>Patient::class,
'mapped'=>false,
'constraints' =>[
new Assert\NotBlank(['message'=>'patient is required']),
],
'query_builder'=>$this->accountService->getActivePatientQueryBuilder(),
'choice_attr' => function($patient){
$payer = $patient->getPayer();
return $payer === null ? [] :[
'data-payer_id'=>$payer->getId(),
];
},
])
// other elements etc omitted for brevity
;
}
}
当我让 Symfony 用选项填充 Sessions/EntityType 元素时,一切正常,但过多的查询问题仍然存在。当我按照我的方式做时,即将选择键定义为空数组并动态添加选择到表单中,看起来应该可以工作,但是 Symfony 抱怨验证错误“所选选择无效”并且不分配提交给元素的值。查看分析器中的异常消息,我们看到
选项“4714”、“4743”、“4772”、“4801”、“4830”、“4859”、“4888”、 选择列表中不存在“4917”、“4946”。
很公平。所以问题是:提交表单后如何将这些选择添加到我的 EntityType 元素中?
我已经在文档中阅读了有关 EventListeners 的内容,并且
FormEvents::PRE_SUBMIT
看起来是一个很好的挂钩事件。但经过一番努力后,没有任何效果,我在文档中找不到任何有关此内容的信息。
另一个可行的可能解决方案是分两步进行,首先他们选择患者,然后我动态呈现一个表单,其中候选会话被过滤到属于该患者且与发票无关的会话。但是我 我很固执,想让 Symfony 让这个元素接受我的数据。
建议?
我不知道这是答案,但它是一个答案。我想有某种方法可以直接更改 EntityType 元素的“选择”选项,但如果有的话,我无法弄清楚。因此,我设置了一个事件侦听器,用一个新元素(同名)覆盖“sessions”EntityType 元素,该新元素的选项(选择)与用户提交的数据相同。这感觉有点像通过保持灯泡静止并旋转房子来更换灯泡。好消息是它可以工作——创建一个新的发票实体。更新是一个不同的故事,我还没有讲到。
class InvoiceFormType extends AbstractType
{
public function __construct(private AccountService $accountService)
{
// AccountService is a class that does some heavy lifting and it
// has an EntityManager
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('invoice_date', DateType::class, ['widget'=>'single_text',])
->add('payer',HiddenType::class)
->add('sessions',null, [
'label'=>'Sessions',
'choices'=>[],
]
)
->add('patient',EntityType::class,[
'placeholder'=>'',
'class'=>Patient::class,
'mapped'=>false,
'constraints' =>[
new Assert\NotBlank(['message'=>'patient is required']),
],
'query_builder'=>$this->accountService->getActivePatientQueryBuilder(),
'choice_attr' => function($patient){
$payer = $patient->getPayer();
return $payer === null ? [] :[
'data-payer_id'=>$payer->getId(),
];
},
])
// more elements omitted for brevity, then...
->addEventListener(FormEvents::PRE_SUBMIT,[$this,'onPreSubmit'])
;
$builder->get('payer')->addModelTransformer(new CallbackTransformer(
// outgoing
function($entity){ return $entity ? $entity->getId() : null; },
// incoming
function($id){
return $id ? $this->accountService->getEntityManager()->find(Person::class,$id) : null;
},
));
}
// here's the interesting bit that handles our issue
public function onPreSubmit(FormEvent $event) : void
{
$form_data = $event->getData();
$ids = $form_data['sessions'];
$payer_id = $form_data['payer'];
$sessions = $this->accountService->getEntityManager()->createQuery(
"SELECT s FROM App\Entity\Session s
-- might as well sanity-check the sessions while we're here
LEFT JOIN s.invoice invoice
JOIN s.payer payer
WHERE s.id IN (:ids) AND s.invoice IS NULL
AND payer.id = :payer_id"
)->setParameters(['ids'=>$ids,'payer_id'=>$payer_id])->getResult();
$form = $event->getForm();
$form->add('sessions',EntityType::class,[
'class'=>Session::class,
'multiple'=>true,
'choices' => $sessions,
'by_reference'=>false,// <-- you do NOT want to forget this
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Invoice::class,
]);
}
}
如果有人有更好的想法,请赐教。
https://github.com/symfony/symfony/issues/42451#issuecomment-1049069268
有点难看,但是有用!它曾经在现场使用 ->resetViewTransformers() 方法来工作:
->add('myField', ChoiceType::class,[
'label' => 'My Field',
'required' => false,
'multiple' => true,
'expanded' => false,
])
->get('myField')->resetViewTransformers()