未提交时如何将子表单设置为空?
How to set a subform to null when it's not submitted?
精简版
- 表单数据为:
['model' => new TestModel(123)]
- 提交的表单包含以下数据:
[]
- 我的期望:
['model' => null]
- 我得到的是:
['model' => $anEmptyInstanceOfTestModel]
长版
我有一个简单的表格。此表单有一个用于我的自定义 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 来动态
- 查看请求数据是否为空并存储它以供以后替换数据
- 稍后替换数据(因为我需要替换标准数据,而不是请求数据[已经是
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();
精简版
- 表单数据为:
['model' => new TestModel(123)]
- 提交的表单包含以下数据:
[]
- 我的期望:
['model' => null]
- 我得到的是:
['model' => $anEmptyInstanceOfTestModel]
长版
我有一个简单的表格。此表单有一个用于我的自定义 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 来动态
- 查看请求数据是否为空并存储它以供以后替换数据
- 稍后替换数据(因为我需要替换标准数据,而不是请求数据[已经是
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();