使用表单事件 Symfony 5 动态修改自定义 FormType 中的表单选项

Dynamically modify the form choices in a Custom FormType using Form Events Symfony 5

我想构建一个自定义 AjaxEntityType 来加载 ajax 上的选项。我不能用所有的选择来构建表单,因为太多了,性能受到很大影响。

问题是,如果我从自定义类型中读取表单字段(就像在说明书中那样),则根本不会提交数据。

我需要一种方法来更改自定义类型中的选项 Class 而无需阅读表单字段。

这是我的 class:

<?php

namespace App\Form\Type;

class AjaxEntityType extends AbstractType
{


    protected $em;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'ajax_url' => null
        ]);
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        
        $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
            $data = $event->getData();
            if ($data === null || $data === []) {
                return;
            }
            // here i get the ids from the submited data, get the entities from the database and I try to set them as choices
            $entityIds = is_array($data) ? $data : [$data];
            $entities = $this->em->getRepository($event->getForm()->getConfig()->getOptions()['class'])->findBy(["id" => $entityIds]);

            $options['choices'] = $entities;

            $event->getForm()->getParent()->add(
                $event->getForm()->getName(),
                self::class,
                $options
            );
            // the result is that the from gets submitted, but the new data is not set on the form. It's like the form field never existed in the first place.
        });

    }

    /**
     * {@inheritdoc}
     */
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $view->vars['ajax_url'] = $options['ajax_url'];
    }

    public function getParent()
    {
        return EntityType::class;
    }
}

我的控制器非常简单:

public function create(Request $request, ProductService $productService, Product $product = null)
    {
        if(empty($product)){
            $product = new Product();
        }

        $form = $this->createForm(ProductType::class, $product);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {

            $this->getDoctrine()->getManager()->persist($product);
            $this->getDoctrine()->getManager()->flush();
            return $this->redirectToRoute('products_list');

        }

        return $this->render('admin/products/form.html.twig', [
            'page_title' => "Product form",
            'product' => $product,
            'form' => $form->createView()
        ]);
    }

参考了form components code,思考了一段时间,我有了一个理论。

首先,表单提交是这样的(简化):

所以它是递归的,子窗体在父窗体继续之前经历它的整个循环。

那么会发生什么:

轮到你的子表单时,你的子表单会做一些复杂的事情来创建一个新的子表单(它不会包含在父表单的循环中)并且它将覆盖原始表单。然而,由于新的子表单没有自己的循环,所以它基本上是空的。 (此外,父表单会删除已处理以检测额外数据的表单的提交数据)。所以我们现在有一个接收数据的分离原始表单和一个没有数据的新链接表单。

父表单现在将为自身(及其子表单)调用数据映射器以将数据映射回视图 -> 规范化 -> 模型数据。由于您的旧子表单不再是子表单并且新子表单为空,因此结果为空。

如何解决?

您必须在父窗体循环遍历子窗体之前添加子窗体。因此,实现此目的的一种方法是将 PRE_SUBMIT 事件侦听器添加到 parent 表单并在那里添加 sub 表单而不是子表单在父表单中覆盖自身。显然,这并不像您希望的那样可重用。 也许您可以在子窗体的 buildForm 方法中将事件侦听器添加到父窗体,但这听起来有点脏(虽然它不是编辑父窗体的地方).

旁注:我也希望您知道,如果表单产生错误,您更改后的表单将显示给用户......其选项集非常有限 - 如果您能够很好地处理表单回复这应该不是问题。

更新

好吧,显然它应该是可重复使用的……在那种情况下,我看到了两种选择,其中任何一种都可能不起作用:

  1. 也设置empty_data on the new form to the entities, since the form don't receive any data, this is probably going to be used. Maybe you could set data

  2. 不替换表单,而是实际执行实际设置数据的事件处理程序。提交处理程序应该 have direct access to the normData(通过 $event->setData(...) 设置新值)然后被传播。

  3. 您可以选择 不扩展 EntityType,而是 wrap 它(即 ->add(..., EntityType...) 在它 buildForm) 并应用我之前为父修改描述的方法。这将使它可重复使用。如果您将 label 设置为 false 和一个方便的表单主题,这应该与直接使用实体类型几乎没有区别。

改变 PRE_SET_DATA 中的表格(将选定的选项保留在表格中)和 PRE_SUBMIT 中的表格(将新选择的项目添加到选择列表中)最终是一个巨大的头痛。总是需要一些复杂的调整。

因此,作为最终解决方案,我删除了自定义类型 class 的 buildView 方法中的所有选项,效果很好。

// in file App\Form\Type\AjaxEntityType.php
/**
 * {@inheritdoc}
 */
public function buildView(FormView $view, FormInterface $form, array $options)
{
    $view->vars['ajax_url'] = $options['ajax_url'];

    // in case it's not a multiple select
    if(!is_array($view->vars['value'])){
        $selected = [$view->vars['value']];
    }else{
        $selected = $view->vars['value'];
    }

    foreach($view->vars['choices'] as $index => $choice){
        if(!in_array($choice->value, $selected)){
            unset($view->vars['choices'][$index]);
        }
    }

}

我在遇到同样的问题后遇到了你的问题,提交的表单中缺少新的字段数据。我的解决方案是有条件地添加新字段,然后在预提交事件处理程序中手动提交它:

$data = $event->getData();
$form = $event->getForm();
$name = $form->getName();
$parent = $form->getParent();

if(!in_array($data, $form->getConfig()->getOption('choices'))) {

    $parent->add(
        $name,
        self::class,
        $options
    );
        
    $newForm = $parent->get($name);
    $newForm->submit($data);
}