Symfony5 handleRequest 更新原始 collectionType 对象

Symfony5 handleRequest updating original collectionType objects

在此处遵循官方 Symfony 文档时,我无法使我的逻辑工作:https://symfony.com/doc/current/form/form_collections.html#allowing-tags-to-be-removed

根据示例,我需要获取原始标签,然后在处理表单后将它们与新标签进行比较。

在我的例子中,我有一个 Purchase 实体,它可以有一个 PurchaseProducts(ManyToMany) 的集合。在我的例子中,当我更改 PurchaseProduct 时,我需要更新已删除的购买的库存。但是,无论我如何获得原始 PurchaseProducts,在 $form->handleRequest() 之后,它们都会更新为新值,我会丢失有关原始值的任何信息。

片段构成了我的逻辑控制器:

 /** @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
            }
        }

我尝试了很多选项,比如克隆、查询数据库等等,但是在 handleRequest 之后我总是得到相同的更新实体。为什么?

行为解释

正如您所指的表单处理器的 "allow_delete" => true"allow_add" => true 概念。当提交表单时,实体 属性 在副本中出现的集合发生变化是预期的行为。但是,$originalProducts 集合将不包含任何新实体 (new PurchaseProduct())。

这是因为 PHP 通过引用传递对象,即 PurchaseProduct 对象到 ArrayCollection。这意味着对嵌入式表单对象所做的任何更改都会应用于 Purchase:::$purchaseProducts$originalProducts 集合,因为它们是相同的 PurchaseProduct 对象(通过引用)。

但是,当它们在 $form->handleRequest($request) 之后从 Purchase:::$purchaseProducts 集合中删除时,这些对象仍将存在于 $originalProducts 集合中。如果您的实体不包含必要的逻辑,这允许您比较这两个集合以将它们从实体管理器或您的实体集合中删除。

示例 使用 ArrayObject 和 Foo 对象: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);

结果

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',
  )),
))

决议

根据您想要的结果,有几种方法可用于检测和处理更改,例如 Change Tracking Policies

另一种确定实体发生了什么变化的方法是$em->getUnitOfWork()->getEntityChangeSet($entity)检索被提议用于学说的实体的变化。

代码建议

为确保请求不被误解,您应该始终处理表单请求(无论请求方法如何)并验证表单是 submitted/valid。这是因为 handleRequest 方法会根据请求方法触发多个事件。

/** @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
            }
        }