我正在尝试在Symfony 2中创建一种新的表单类型。它基于实体类型,它在前端使用 select2,我需要用户能够选择现有实体或创建新实体。
我的想法是发送实体的 ID,如果用户选择现有实体,则让它由默认实体类型转换,或者如果用户输入新值,则发送类似"_new:输入的文本"之类的内容。然后这个字符串应该通过我自己的模型转换器转换为新的表单实体,它应该看起来像这样:
<?php
namespace AcmeMainBundleFormDataTransformer;
use SymfonyComponentFormDataTransformerInterface;
class EmptyEntityTransformer
implements DataTransformerInterface
{
private $entityName;
public function __construct($entityName)
{
$this->entityName = $entityName;
}
public function transform($val)
{
return $val;
}
public function reverseTransform($val)
{
$ret = $val;
if (substr($val, 0, 5) == '_new:') {
$param = substr($val, 5);
$ret = new $this->entityName($param);
}
return $ret;
}
}
遗憾的是,只有在选择现有实体时才会调用转换器。当我输入一个新值时,字符串会在请求中发送,但根本不调用转换器的 reverseTransform 方法。
我是Symfony的新手,所以我甚至不知道这种方法是否正确。您知道如何解决此问题吗?
编辑:我的表单类型代码是:
<?php
namespace AcmeMainBundleFormType;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentFormFormView;
use SymfonyComponentFormFormInterface;
use SymfonyBundleFrameworkBundleRoutingRouter;
use AcmeMainBundleFormDataTransformerEmptyEntityTransformer;
use SymfonyComponentPropertyAccessPropertyAccess;
use SymfonyComponentOptionsResolverOptionsResolverInterface;
use SymfonyComponentFormFormEvent;
use SymfonyComponentFormFormEvents;
class Select2EntityType
extends AbstractType
{
protected $router;
public function __construct(Router $router)
{
$this->router = $router;
}
/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
parent::setDefaultOptions($resolver);
$resolver->setDefaults(array(
'placeholder' => null,
'path' => false,
'pathParams' => null,
'allowNew' => false,
'newClass' => false,
));
}
public function getParent()
{
return 'entity';
}
public function getName()
{
return 's2_entity';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['newClass']) {
$transformer = new EmptyEntityTransformer($options['newClass']);
$builder->addModelTransformer($transformer);
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$field = $view->vars['name'];
$parentData = $form->getParent()->getData();
$opts = array();
if (null !== $parentData) {
$accessor = PropertyAccess::createPropertyAccessor();
$val = $accessor->getValue($parentData, $field);
if (is_object($val)) {
$getter = 'get' . ucfirst($options['property']);
$opts['selectedLabel'] = $val->$getter();
}
elseif ($choices = $options['choices']) {
if (is_array($choices) && array_key_exists($val, $choices)) {
$opts['selectedLabel'] = $choices[$val];
}
}
}
$jsOpts = array('placeholder');
foreach ($jsOpts as $jsOpt) {
if (!empty($options[$jsOpt])) {
$opts[$jsOpt] = $options[$jsOpt];
}
}
$view->vars['allowNew'] = !empty($options['allowNew']);
$opts['allowClear'] = !$options['required'];
if ($options['path']) {
$ajax = array();
if (!$options['path']) {
throw new RuntimeException('You must define path option to use ajax');
}
$ajax['url'] = $this->router->generate($options['path'], array_merge($options['pathParams'], array(
'fieldName' => $options['property'],
)));
$ajax['quietMillis'] = 250;
$opts['ajax'] = $ajax;
}
$view->vars['options'] = $opts;
}
}
然后我创建此表单类型:
class EditType
extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('masterProject', 's2_entity', array(
'label' => 'Label',
'class' => 'MyBundle:MyEntity',
'property' => 'name',
'path' => 'my_route',
'pathParams' => array('entityName' => 'name'),
'allowNew' => true,
'newClass' => '\...\MyEntity',
))
。
感谢您的建议
我找到了答案,但我不确定这是否是正确的解决方案。当我试图理解EntityType的工作原理时,我注意到它使用EntityChoiceList来检索可用选项的列表,并且在此类中有一个getChoicesForValues方法,当id转换为实体时调用该方法。所以我实现了我自己的 ChoiceList,它将我自己的类添加到返回数组的末尾:
<?php
namespace AcmeMainBundleFormChoiceList;
use SymfonyBridgeDoctrineFormChoiceListEntityChoiceList;
use DoctrineCommonPersistenceObjectManager;
use SymfonyComponentPropertyAccessPropertyAccessorInterface;
class EmptyEntityChoiceList
extends EntityChoiceList
{
private $newClassName = null;
public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null, $newClassName = null)
{
parent::__construct($manager, $class, $labelPath, $entityLoader, $entities, $preferredEntities, $groupPath, $propertyAccessor);
$this->newClassName = $newClassName;
}
public function getChoicesForValues(array $values)
{
$ret = parent::getChoicesForValues($values);
foreach ($values as $value) {
if (is_string($value) && substr($value, 0, 5) == '_new:') {
$val = substr($value, 5);
if ($this->newClassName) {
$val = new $this->newClassName($val);
}
$ret[] = $val;
}
}
return $ret;
}
}
将此 ChoiceList 注册到表单类型有点复杂,因为原始选择列表的类名在 EntityType 扩展的 DoctrineType 中硬编码,但如果您查看此类,不难理解如何做到这一点。
不调用 DataTransformer 的原因可能是 EntityType 能够返回结果数组,并将转换应用于此集合的每个项。如果结果数组为空,则显然没有要调用转换器的项目。
我和你的问题完全相同,我选择使用仍然DataTransformer
的FormEvent
这个想法是在提交之前切换字段类型(实体类型(。
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (substr($data['project'], 0, 5) == '_new:') {
$form->add('project', ProjectCreateByNameType::class, $options);
}
}
如果需要,这将在提交之前将project
字段替换为新的自定义字段。
ProjectCreateByNameType
可以扩展TextField
,并且必须添加DataTransformer
。