16

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.

2 Answers 2

33

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;
    });
}
3
  • 6
    works!!! Spent half a day on this... thank you. Too bad there is not much documentation on Symfony website about this method.
    – Karolis
    Jan 23, 2016 at 20:19
  • 1
    Great one!! Thanks
    – curuba
    Feb 22, 2021 at 15:17
  • @Sergey Fedotov how did you know about this? There are no docs available for this anywhere? Did you just debug it?
    – MiKE
    Aug 19, 2021 at 10:36
0

It is possible to combine arrow function https://www.php.net/manual/en/functions.arrow.php with spaceship https://www.php.net/manual/en/migration70.new-features.php with PHP 7.

With the previous example :

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

A more advanced example, ordered any collection from everywhere by using any ordered field :

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.