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:
The form rows are displayed in order: A,B,C;
After user changes the order, I have:
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.
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;
});
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With