I cannot make my logic work when following the official Symfony docs here: https://symfony.com/doc/current/form/form_collections.html#allowing-tags-to-be-removed
Based on the example i need to get the originalTags and then compare them with the new tags after form has been handled.
In my case I have a Purchase
entity, that can have a collection of PurchaseProducts
(ManyToMany). In my case, when I change a PurchaseProduct
I need to update the stock of the purchase that has been removed. However no matter how I get the original PurchaseProducts, after $form->handleRequest() they are updated with the new values and I lose any information about the original ones.
Fragments form my controller with the logic:
/** @var Purchase $purchase */
$purchase = $this->getDoctrine()
->getRepository(Purchase::class)
->find($id);
if (!$purchase) {
$this->addFlash('error', 'Purchase not found');
return $this->redirect($this->generateUrl('purchase_list'));
}
$originalProducts = new ArrayCollection();
foreach ($purchase->getPurchaseProducts() as $purchaseProduct) {
$originalProducts->add($purchaseProduct);
}
$form = $this->createForm(PurchaseType::class, $purchase);
if ($request->isMethod('POST')) {
dump($originalProducts); // Original entities are here
$form->handleRequest($request);
dump($originalProducts);die; // Original entities are updated with the new ones
...
// This will not work since originalProducts already has the new entities
foreach ($originalProducts as $purchaseProduct) {
if (false === $purchase->getPurchaseProducts()->contains($purchaseProduct)) {
// update stock here
}
}
I have tried many options, like cloning, querying the database and so on, but after handleRequest I always get the same updated entities. Why?
CodePudding user response:
Explanation of Behavior
As you are referring to the "allow_delete" => true
or "allow_add" => true
concepts of the form processor. The entity property changes within the collection appearing in the copy when the Form is submitted is the expected behavior. However, the $originalProducts
collection will NOT contain any new entities (new PurchaseProduct()
).
This occurs because PHP passes the objects by-reference, namely the PurchaseProduct
object to the ArrayCollection
. Meaning any changes made to the embedded form object are applied to both the Purchase:::$purchaseProducts
and the $originalProducts
collections, since they are the same PurchaseProduct
object (by-reference).
However, when they are removed from the Purchase:::$purchaseProducts
collection after $form->handleRequest($request)
, the objects will still exist in the $originalProducts
collection. Which allows for you to compare the two collections to remove them from the Entity Manager or your Entity collection in the event your Entity does not contain the necessary logic.
Example using ArrayObject and Foo objects: https://3v4l.org/oLZqO#v7.4.25
Class Foo
{
private $id;
public function __construct()
{
$this->id = uniqid('', false);
}
public function setId($id)
{
$this->id = $id;
}
}
//initial state of Purchase::$purchaseProducts
$foo1 = new Foo();
$foo2 = new Foo();
$a = new ArrayObject([$foo1, $foo2]);
//Create Copy as $originalProducts
$b = new ArrayObject();
foreach ($a as $i => $foo) {
$b->offsetSet($i, $foo);
}
//form submission
$foo1->setId('Hello World');
$a->offsetUnset(1);
Result
Initial state of Copy:
ArrayObject::__set_state(array(
0 =>
Foo::__set_state(array(
'id' => '6182c24b00a21',
)),
1 =>
Foo::__set_state(array(
'id' => '6182c24b00a28',
)),
))
-----------------------
Form Submitted ID Changed in Copy:
ArrayObject::__set_state(array(
0 =>
Foo::__set_state(array(
'id' => 'Hello World',
)),
1 =>
Foo::__set_state(array(
'id' => '6182c24b00a28',
)),
))
-----------------------
Source has foo2 entity removed:
ArrayObject::__set_state(array(
0 =>
Foo::__set_state(array(
'id' => 'Hello World',
)),
))
-----------------------
Copy still contains both entities:
ArrayObject::__set_state(array(
0 =>
Foo::__set_state(array(
'id' => 'Hello World',
)),
1 =>
Foo::__set_state(array(
'id' => '6182c24b00a28',
)),
))
Resolutions
Depending on your desired result, there are several approaches that can be used to detect and handle the changes, such as Change Tracking Policies
.
Another way to determine what has changed for the entities, is the $em->getUnitOfWork()->getEntityChangeSet($entity)
to retrieve the changes for an entity that was proposed to doctrine.
Code Suggestion
To ensure the request is not misinterpreted, you should always handle the form request (regardless of the request method) and verify that the form was submitted/valid. This is because the handleRequest
method triggers multiple events depending on the request method.
/** @var Purchase $purchase */
$purchase = $this->getDoctrine()
->getRepository(Purchase::class)
->find($id);
if (!$purchase) {
$this->addFlash('error', 'Purchase not found');
return $this->redirect($this->generateUrl('purchase_list'));
}
$originalProducts = new ArrayCollection();
foreach ($purchase->getPurchaseProducts() as $purchaseProduct) {
$originalProducts->add($purchaseProduct);
}
$form = $this->createForm(PurchaseType::class, $purchase);
dump($originalProducts); // Original entities are here
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dd($originalProducts); // dump die as dd() - Original entities are updated with the new ones
//...
// This will not work since originalProducts already has the new entities
foreach ($originalProducts as $purchaseProduct) {
if (false === $purchase->getPurchaseProducts()->contains($purchaseProduct)) {
//remove original from the Purchase entity
}
}