Search code examples
symfonydoctrinesymfony4

Doctrine weird behavior, changes entity that I never persisted


I have this situation:

Symfony 4.4.8, in the controller, for some users, I change some properties of an entity before displaying it:

public function viewAction(string $id)
{
    $em = $this->getDoctrine()->getManager();

    /** @var $offer Offer */
    $offer = $em->getRepository(Offer::class)->find($id);

    // For this user the payout is different, set the new payout
    // (For displaying purposes only, not intended to be stored in the db)
    $offer->setPayout($newPayout);

    return $this->render('offers/view.html.twig', ['offer' => $offer]);
}

Then, I have a onKernelTerminate listener that updates the user language if they changed it:

public function onKernelTerminate(TerminateEvent $event)
{
    $request = $event->getRequest();

    if ($request->isXmlHttpRequest()) {
        // Don't do this for ajax requests
        return;
    }

    if (is_object($this->user)) {

        // Check if language has changed. If so, persist the change for the next login
        if ($this->user->getLang() && ($this->user->getLang() != $request->getLocale())) {

            $this->user->setLang($request->getLocale());
            $this->em->persist($this->user);
            $this->em->flush();
        }
    }
}

public static function getSubscribedEvents()
{
    return [
        KernelEvents::TERMINATE => [['onKernelTerminate', 15]],
    ];
}

Now, there is something very weird happening here, if the user changes language, the offer is flushed to the db with the new payout, even if I never persisted it!

Any idea how to fix or debug this?


PS: this is happening even if I remove $this->em->persist($this->user);, I was thinking maybe it's because of some relationship between the user and the offer... but it's not the case.

I'm sure the offer is persisted because I've added a dd('beforeUpdate'); in the Offer::beforeUpdate() method and it gets printed at the bottom of the page.


Solution

  • alright, so by design, when you call flush on the entity manager, doctrine will commit all the changes done to managed entities to the database.

    Changing values "just for display" on an entity that represents a record in database ("managed entity") is really really bad design in that case. It begs the question what the value on your entity actually means, too.

    Depending on your use case, I see a few options:

    • create a display object/array/"dto" just for your rendering:

      $display = [
          'payout' => $offer->getPayout(),
          // ...
      ];
      $display['payout'] = $newPayout;
      return $this->render('offers/view.html.twig', ['offer' => $display]);
      

      or create a new non-persisted entity

    • use override-style rendering logic

      return $this->render('offers/view.html.twig', [
          'offer' => $offer,  
          'override' => ['payout' => $newPayout],
      ]);
      

      in your template, select the override when it exists

      {{ override.payout ?? offer.payout }}
      
    • add a virtual field (meaning it's not stored in a column!) to your entity, maybe call it "displayPayout" and use the content of that if it exists