未提交时如何将子表单设置为空?

How to set a subform to null when it's not submitted?

精简版

长版

我有一个简单的表格。此表单有一个用于我的自定义 FormType 的子表单(键“模型”)。

如果我用“模型”已经存在的数据初始化表单,模型将不会为空,即使 没有提交模型 的数据 。另一方面,如果初始数据中“model”为空,则“model”的值保持为空。

问题

如果未提交任何内容,如何配置我的表单以将“模型”设置为空?

我已经尝试设置 'required' => false 和/或 'empty_data' => null,但两者似乎都无济于事。

作为单元测试的简约示例

<?php

namespace AppBundle\Tests\Common\Form\Type;

use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class NullModelTest extends TypeTestCase {

    public function testNullOnNotSubmitted() {
        $tm = new TestModel(123);
        $data = ['model' => $tm];
        $form = $this->factory->createBuilder(FormType::class, $data)
                ->add('model', TestModelType::class, ['required' => false])
                ->getForm();
        $form->submit([]); // submit no data
        $this->assertTrue($form->isSynchronized());
        $this->assertNull($form->getData()['model']); // ERROR: returns the empty model
    }

}

class TestModel {
    protected $id;
    public function __construct($id = null) {
        $this->id = $id;
    }
    public function getId() { return $this->id; }
    public function setId($id) { $this->id = $id; }
}

class TestModelType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add('id', TextType::class);
    }
    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefault('data_class', TestModel::class);
    }
}

我不确定它是否是错误,因为此文档:http://symfony.com/doc/current/cookbook/form/use_empty_data.html#option-2-provide-a-closure 说你可以关闭(可能 return NULL),但它对我也不起作用。

非常简单的解决方案是使用 DataTransformer:

use Symfony\Component\Form\DataTransformerInterface;

class NullToEmptyTransformer implements DataTransformerInterface {
    public function transform($object) {
        return $object;  //DO NOTHING
    }
    public function reverseTransform($object) {
        if (is_null($object->getId()))
            return NULL; //Return NULL if object is empty
        return $object;
    }
}

并将其附加到您的表单字段:

$fb = $this->factory->createBuilder(FormType::class, $data);
$model = $this->factory->createBuilder()
            ->create('model', TestModelType::class, ['required' => false])
            ->addModelTransformer(new NullToEmptyTransformer());
$fb->add($model);
$form = $fb->getForm();

好的,这是非常罕见的情况。在 "real" HTML 表格中没有 "it's not submitted at all" 这样的东西。 IMO 这就是不存在将子表单设置为 null 等功能的原因。此外:子表单可能只代表真实模型的一部分。想象一下子表单只处理客户的字段 "name"。客户本身有很多领域。在经典的 HTML 表单中,此子表单将呈现为一个简单的文本字段。现在假设您的整个客户在未为此子表单提交任何内容后将设置为 null。看起来很不对 ;) 选项 empty_data 用于在填充数据之前初始化未初始化的对象(例如您的客户)- 如果它已经存在,则不用于替换它。

我遇到这种情况是因为我使用 Symfony Forms 以安全和结构化的方式用请求数据填充我的模型。因此,我的请求数据几乎是自定义的(JSON API)并且整个子表单可以为 null 或什至不存在。

目前我使用以下 EventSubscriber 来动态

  1. 查看请求数据是否为空并存储它以供以后替换数据
  2. 稍后替换数据(因为我需要替换标准数据,而不是请求数据[已经是null])

use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Replaces the data of this form type by the given value if it's not part of the request data.
 */
class ReplaceIfNotSubmittedListener implements EventSubscriberInterface {

    public static function getSubscribedEvents() {
        return [
            FormEvents::PRE_SUBMIT => 'preSubmit',
            FormEvents::SUBMIT => 'submit',
        ];
    }

    /**
     * @var bool
     */
    private $shouldBeReplaced = false;

    /**
     * @var mixed|callable
     */
    private $replaceValue;

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

    public function preSubmit(FormEvent $event) {
        if ($event->getData() === null) {
            $this->shouldBeReplaced = true;
        }
    }

    function submit(FormEvent $event) {
        if ($this->shouldBeReplaced) {
            $value = $this->replaceValue;
            $event->setData(is_callable($value) ? $value() : $value);
        }
    }

}

...以及修改后的主窗体中的用法:

$fb = $this->factory->createBuilder(FormType::class, $data);
$model = $this->factory->createBuilder()
            ->create('model', TestModelType::class, ['required' => false])
            ->addEventSubscriber(new ReplaceIfNotSubmittedListener(null));
$fb->add($model);
$form = $fb->getForm();