Symfony表单CollectionType字段的顺序

16

在我的模型中,我有一个菜谱实体和一个食材实体。在菜谱实体中,关系的定义如下:

/**
 * @ORM\OneToMany(targetEntity="Ingredient", mappedBy="recipe", cascade={"remove", "persist"}, orphanRemoval=true) 
 * @ORM\OrderBy({"priority" = "ASC"})
 */
private $ingredients;

在Ingredient实体中:

/**
 * @ORM\ManyToOne(targetEntity="Recipe", inversedBy="ingredients")
 * @ORM\JoinColumn(name="recipe_id", referencedColumnName="id")
 */
private $recipe;

我正在为食谱编写CRUD控制器,希望用户能够动态添加食材。我还希望用户可以拖放食材以设置它们在食谱中的优先级(顺序)。我将使用CollectionType表单字段来实现这一点。

并且需要参考以下页面作为教程:

http://symfony.com/doc/current/cookbook/form/form_collections.html

到目前为止,添加和显示食谱都运作得很好,但是在编辑/更新操作中存在一个问题,我将尝试在下面进行描述:

在控制器中,我加载实体并创建表单,如下所示:

  public function updateAction($id, Request $request)
  {
      $em = $this->getDoctrine()->getManager();
      $recipe = $em->getRepository('AppBundle:Recipe')->find($id);


      $form = $this->createEditForm($recipe);
      $form->handleRequest($request);

      ...

    }

由于优先级在数据库中已保存,并且我有@ORM\OrderBy({"priority" = "ASC"}),因此成分的初始加载和显示工作正常。但是,如果用户拖放成分,则优先级值会更改。如果存在表单验证错误并且需要反复显示表单,则表单中的成分将按旧顺序显示,即使更新了优先级值。

例如,数据库中的初始成分 => 优先级值如下:

  • A => 1
  • B => 2
  • C => 3

表单行按顺序显示:A、B、C;

用户更改顺序后,我有:

  • B => 1
  • A => 2
  • C => 3

但表单行仍显示为 A、B、C;

我明白表单是使用顺序A、B、C初始化的,更新priority不会改变ArrayCollection的元素顺序。但我几乎不知道该如何改变它。

我迄今为止尝试过的:

$form->getData();
// sort in memory
$form->setData();

这不起作用,因为在已经有输入的表单上使用setData()是不允许的。

我还尝试使用DataTransformer来对行进行排序,但表单忽略了新顺序。

我还尝试在FormType类中使用PRE / POST提交处理程序来对行进行排序,然而表单仍然忽略了新顺序。

最后一件(有点)有效的事情是:

在Recipe实体中,定义一个sortIngredients()方法,它可以在内存中对ArrayCollection进行排序。

  public function sortIngredients()
  {
      $sort = \Doctrine\Common\Collections\Criteria::create();
      $sort->orderBy(Array(
          'priority' => \Doctrine\Common\Collections\Criteria::ASC
      ));

      $this->ingredients = $this->ingredients->matching($sort);

      return $this;
  }

然后,在控制器中:

  $form = $this->createEditForm($recipe);
  $form->handleRequest($request);

  $recipe->sortIngredients();

  // repeatedly create and process form with already sorted ingredients
  $form = $this->createEditForm($recipe);
  $form->handleRequest($request);

  // ... do the rest of the controller stuff, flush(), etc

这个方法可以运行,但表单会被创建和处理两次,而且老实说它看起来像是一个hack...

我正在寻找更好的解决问题的方法。

2个回答

33

你需要使用表单类型的finishView方法。

下面是代码示例:

public function finishView(FormView $view, FormInterface $form, array $options)
{
    usort($view['photos']->children, function (FormView $a, FormView $b) {
        /** @var Photo $objectA */
        $objectA = $a->vars['data'];
        /** @var Photo $objectB */
        $objectB = $b->vars['data'];

        $posA = $objectA->getSortOrder();
        $posB = $objectB->getSortOrder();

        if ($posA == $posB) {
            return 0;
        }

        return ($posA < $posB) ? -1 : 1;
    });
}

6
太好了!我花了半天时间才搞定这个问题...谢谢。可惜Symfony网站上关于这种方法的文档不多。 - Karolis
1
太棒了!谢谢。 - curuba
@Sergey Fedotov,你是怎么知道这个的?这方面没有任何文档可用吗?你只是调试它了吗? - MiKE

0

在 PHP 7 中,可以将箭头函数 https://www.php.net/manual/en/functions.arrow.php 与太空船运算符 https://www.php.net/manual/en/migration70.new-features.php 结合使用。

以前的示例为:

public function finishView(FormView $view, FormInterface $form, array $options)
{
    usort($view['photos']->children, fn (FormView $a, FormView $b) => $a->vars['data']->getSortOrder() <=> $b->vars['data']->getSortOrder());
}

一个更高级的例子,使用任何有序字段可以从任何地方排序任何集合:
public function finishView(FormView $view, FormInterface $form, array $options)
{
    $view['blocks']->children = array_merge($view['blockTexts']->children, $view['blockImages']->children);

    usort($view['blocks']->children, fn (FormView $a, FormView $b) => $a->vars['data']->orderNumber <=> $b->vars['data']->orderNumber);
}

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