Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Order of Symfony form CollectionType field

In my model I have a Recipe entity and Ingredient entity. In Recipe entity, the relation is defined like this:

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

In Ingredient entity:

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

I am working on CRUD controller for the recipe and I want the user to be able to add ingredients dynamically. I also want the user to drag-and-drop ingredients to set their priority (order) in the recipe. I am using CollectionType form field for this.

and this page as tutorial:

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

Adding and showing of the recipe are working perfectly so far, however there is a problem with Edit/Update action, which I will try to describe below:

In the controller, I load the entity and create the form like this:

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


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

      ...

    }

Since the priority is saved in the DB, and I have @ORM\OrderBy({"priority" = "ASC"}), the initial loading and display of ingredients works fine. However if the user drags and drops ingredients around, the priority values change. In case there are form validation errors and the form needs to be displayed repeatedly, ingredients inside the form get displayed in the old order, even though priority values are updated.

For example, I have the following initial Ingredient => priority values in DB:

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

The form rows are displayed in order: A,B,C;

After user changes the order, I have:

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

but the form rows are still displayed as A,B,C;

I understand that the form has been initialized with order A,B,C, and updating priority doesn't change the element order of ArrayCollection. But I have (almost) no idea how to change it.

What I have tried so far:

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

This doesn't work, as apparently it isn't allowed to use setData() on form which already has input.

I have also tried to set a DataTransformer to order the rows, but the form ignores new order.

I have also tried to use PRE/POST submit handlers in the FormType class to order the rows, however the form still ignores the new order.

The last thing that (kind of) works is this:

In Recipe entity, define sortIngredients() method which sorts ArrayCollection in memory,

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

Then, in the controller:

  $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

This works, but the form is created and processed twice, and honestly it looks like a hack...

I am looking for a better way to solve the problem.

like image 731
Karolis Avatar asked Jan 23 '16 19:01

Karolis


1 Answers

You need to use finishView method of your form type.

Here is the example of code:

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;
    });
}
like image 85
Sergey Fedotov Avatar answered Nov 12 '22 19:11

Sergey Fedotov