Symfony 2 实体表单类型中的转换器

5
我正在尝试在Symfony 2中创建一个新的表单类型。它基于实体类型,前端使用select2,我需要让用户能够选择现有实体或创建新实体。
我的想法是发送实体的ID,并让默认实体类型将其转换为现有实体,如果用户输入新值,则发送类似“_new:输入文本”的内容。然后,我的自定义模型转换器应将此字符串转换为新的表单实体,它应该类似于以下内容:
<?php
namespace Acme\MainBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

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 Acme\MainBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Acme\MainBundle\Form\DataTransformer\EmptyEntityTransformer;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

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',
            ))

感谢您的建议。

你需要向我们展示FormType代码,以及(如果有的话)表单的自定义类型。谢谢 :) - DonCallisto
2个回答

0

我认为我找到了答案,但我不确定这是否是正确的解决方案。当我试图理解EntityType的工作原理时,我注意到它使用EntityChoiceList来检索可用选项列表,在这个类中有一个getChoicesForValues方法,当ids转换为实体时会调用它。因此,我实现了自己的ChoiceList,将我的自定义类添加到返回数组的末尾:

<?php
namespace Acme\MainBundle\Form\ChoiceList;

use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;


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注册到表单类型有点复杂,因为原始选择列表的类名在DoctrineType中是硬编码的,而EntityType继承了它,但如果您查看此类,就不难理解如何做。

DataTransformer没有被调用的原因可能是EntityType能够返回结果数组,并且转换应用于此集合的每个项目。如果结果数组为空,则显然没有项目可以调用转换器。


你找到实现这个的方法了吗? - Genar
我没有在全局范围内覆盖EntityChoiceList,而是创建了自己的类,该类是从Symfony的EntityChoiceList扩展而来。然后,我创建了自己的表单类型,该类型是从Symfony\Component\Form\AbstractType扩展而来,并在这个新的表单类型中重写了setDefaultOptions方法,在这个方法中实例化了EntityChoiceList类,并为这个表单类型硬编码了自己的EntityChoiceList。 - Lukas Kral

0

我有和你完全相同的问题,我选择使用一个带有DataTransformerFormEvent

这个想法是在提交之前切换字段类型(实体类型)。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接