Home > Back-end >  Symfony5 handleRequest updating original collectionType objects
Symfony5 handleRequest updating original collectionType objects

Time:11-04

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
            }
        }
  • Related