Symfony 2.8 动态 ChoiceType 选项

9
在我的项目中,有一些带有大量选项的选择类型表单。因此我决定基于jquery自动完成构建一个自动完成选择类型,它在运行时向原始
2个回答

22

要处理动态添加的值,请使用选择类型的'choice_loader'选项。这是Symfony 2.7中新增的功能,可惜没有任何文档。

基本上它是实现了ChoiceLoaderInterface接口的服务,该接口定义了三个函数:

  • loadValuesForChoices(array $choices, $value = null)
    • 在构建表单时调用,接收绑定到表单中的对象的预设值
  • loadChoiceList($value = null)
    • 在构建视图时调用,应该返回所有可用选项的完整列表
  • loadChoicesForValues(array $values, $value = null)
    • 在提交表单时调用,接收提交的数据

现在的思路是,在选择加载器内部保留一个ArrayChoiceList作为私有属性。在构建表单时,调用loadValuesForChoices(...),我们将所有预设选项添加到选择列表中,以便向用户显示。在构建视图时,调用loadChoiceList(...),但我们不会加载任何内容,只需返回我们之前创建的私有选择列表。

现在用户与表单交互,通过自动完成加载一些附加选项并将其放入HTML中。当提交表单时,所选值被提交,在我们的控制器操作中首先创建表单,然后在$form->handleRequest(..)上调用loadChoicesForValues(...),但提交的值可能与最初包含的值完全不同。因此,我们使用仅包含提交值的新列表替换内部选择列表。

我们的表单现在完美地保存了通过自动补全添加的数据。

棘手的部分是,无论何时使用表单类型,我们都需要一个选择加载器的新实例,否则内部选择列表将包含所有选项的混合。

由于目标是编写新的自动完成选择类型,因此通常会使用依赖注入将选择加载器传递给类型服务。但对于类型,如果始终需要一个新实例,则无法使用依赖注入,而必须通过选项来包含它。在默认选项中设置选择加载器是行不通的,因为它们也被缓存了。要解决这个问题,您必须编写一个匿名函数,该函数需要将选项作为参数:

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

编辑:这里是选择加载器类的精简版本:

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;
    }
}

哦,如果我知道数组的键和值应该是什么就好了! - Ian Phillips
@IanPhillips 取决于你指的是哪个数组。要查看函数返回值,请查看 ChoiceLoaderInterface 的 phpDoc。键始终与参数数组中的键相同,而值则为选择或值。请注意,如果您使用实体,则仍需要 DataTransformer!用于创建内部 ArrayChoiceList 的数组应将后来的 <option> 标签作为键,其值作为值。 - SBH
1
@IanPhillips 我添加了一个缩短版本的自动完成选择加载器。 - SBH
@SBH 我的选定值出现了一些奇怪的问题。当我将一个关联数组发送到我的加载器中,例如 [values => options],这些值最终会显示为表单上的选项,但至少选定的值实际上是被选中的。 然后,如果我翻转数组,选项会正确地显示,但它们不再被选中。 - vctls

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

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

在控制器中,验证后获取数据并将其发送到实体,如下所示:
$data = request->request->get('field');
// OR
$data = $form->get('field')->getData();
// and finish with :
$entity = setField($data);

3
'mapped' => false设置并不能解决问题。我希望在我的自定义类型类中找到一个通用的解决方案,而不需要将代码添加到任何控制器中。 - SBH
是的,你说得对,我把另一件事情和事件监听器混淆了:动态表单修改。其思想是获取表单中所有选择的字段,并将其添加到你的选择中。 - Aridjar

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