9

在我的项目中,我有一些带有很多选项的选择类型的表单。

所以我决定构建一个基于 jquery 自动完成的自动完成选择类型,它在运行时将新<option>的 HTML 元素添加到原始元素中。<select>选择后,它们会正确提交,但无法在 default 中处理ChoicesToValuesTransformer,因为在我创建表单时它不存在于我的表单中。

如何让 symfony 接受我动态添加的值?

我在 Symfony 2 中找到了这个答案验证动态加载的选择,其中提交的值用于修改PRE_SUBMIT表单事件中的表单,但在我的情况下无法运行。我需要更改当前类型已知的选项,而不是在表单中添加新的小部件

4

2 回答 2

22

To deal with dynamically added values use 'choice_loader' option of choice type. It's new in symfony 2.7 and sadly doesn't have any documentaion at all.

Basically it's a service implementing ChoiceLoaderInterface which defines three functions:

  • loadValuesForChoices(array $choices, $value = null)
    • is called on build form and receives the preset values of object bound into the form
  • loadChoiceList($value = null)
    • is called on build view and should return the full list of choices in general
  • loadChoicesForValues(array $values, $value = null)
    • is called on form submit and receives the submitted data

Now the idea is to keep a ArrayChoiceList as private property within the choice loader. On build form loadValuesForChoices(...) is called, here we add all preset choices into our choice list so they can be displayed to the user. On build view loadChoiceList(...) is called, but we don't load anything, we just return our private choice list created before.

Now the user interacts with the form, some additional choices are loaded via an autocomplete and put into th HTML. On submit of the form the selected values are submitted and in our controller action first the form is created and afterwards on $form->handleRequest(..) loadChoicesForValues(...) is called, but the submitted values might be completly different from those which where included in the beginning. So we replace our internal choice list with a new one containing only the submitted values.

Our form now perfectly holds the data added by autocompletion.

The tricky part is, that we need a new instance of our choice loader whenever we use the form type, otherwise the internal choice list would hold a mixture of all choices.

Since the goal is to write a new autocomplete choice type, you usually would use dependency injection to pass your choice loader into the type service. But for types this is not possible if you always need a new instance, instead we have to include it via options. Setting the choice loader in the default options does not work, since they are cached too. To solve that problem you have to write a anonymous function which needs to take the options as parameters:

$resolver->setDefaults(array(
    'choice_loader' => function (Options $options) {
        return AutocompleteFactory::createChoiceLoader();
    },
));

Edit: Here is a reduced version of the choice loader class:

use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;

class AutocompleteChoiceLoader implements ChoiceLoaderInterface
{
    /** @var ChoiceListInterface */
    private $choiceList;

    public function loadValuesForChoices(array $choices, $value = null)
    {
        // is called on form creat with $choices containing the preset of the bound entity
        $values = array();
        foreach ($choices as $key => $choice) {
            // we use a DataTransformer, thus only plain values arrive as choices which can be used directly as value
            if (is_callable($value)) {
                $values[$key] = (string)call_user_func($value, $choice, $key);
            }
            else {
                $values[$key] = $choice;
            }
        }

        // this has to be done by yourself:  array( label => value )
        $labeledValues = MyLabelService::getLabels($values);

        // create internal choice list from loaded values
        $this->choiceList = new ArrayChoiceList($labeledValues, $value);

        return $values;
    }


    public function loadChoiceList($value = null)
    {
        // is called on form view create after loadValuesForChoices of form create
        if ($this->choiceList instanceof ChoiceListInterface) {
            return $this->choiceList;
        }

        // if no values preset yet return empty list
        $this->choiceList = new ArrayChoiceList(array(), $value);

        return $this->choiceList;
    }


    public function loadChoicesForValues(array $values, $value = null)
    {
        // is called on form submit after loadValuesForChoices of form create and loadChoiceList of form view create
        $choices = array();
        foreach ($values as $key => $val) {
            // we use a DataTransformer, thus only plain values arrive as choices which can be used directly as value
            if (is_callable($value)) {
                $choices[$key] = (string)call_user_func($value, $val, $key);
            }
            else {
                $choices[$key] = $val;
            }
        }

        // this has to be done by yourself:  array( label => value )
        $labeledValues = MyLabelService::getLabels($values);

        // reset internal choice list
        $this->choiceList = new ArrayChoiceList($labeledValues, $value);

        return $choices;
    }
}
于 2016-03-11T13:29:36.027 回答
0

一个基本的(可能不是最好的)选项是取消映射表单中的字段,例如:

->add('field', choiceType::class, array(
       ...
       'mapped' => false
    ))

在控制器中,验证后,获取数据并将它们发送到实体,如下所示:

$data = request->request->get('field');
// OR
$data = $form->get('field')->getData();
// and finish with :
$entity = setField($data);
于 2016-02-17T13:38:14.670 回答