Doctrine preUpdate does not actually change entity - php

I am using Doctrine 2.4.6 in my Symfony 2.6.1 project. The problem is that changes made to entity in preUpdate callback are not saved in database. Code follows:
class MyListener {
public function preUpdate(PreUpdateEventArgs $args) {
$entity = $args->getEntity();
$args->setNewValue('name', 'test');
// echo $args->getNewValue('name'); --> prints 'test'
}
}
class DefaultController extends Controller {
/**
* #Route("/commit", name="commit")
*/
public function commitAction(Request $request) {
$content = $request->getContent();
$serializer = $this->get('jms_serializer');
/* #var $serializer \JMS\Serializer\Serializer */
$em = $this->getDoctrine()->getManager();
/* #var $em \Doctrine\Orm\EntityManagerInterface */
$persons = $serializer->deserialize($content, 'ArrayCollection<AppBundle\Entity\Person>', 'json');
/* #var $persons \AppBundle\Entity\Person[] */
foreach($persons as $person) {
$em->merge($person);
}
$em->flush();
return new JsonResponse($serializer->serialize($persons, 'json'));
// Person name is NOT 'test' here.
}
}

The preUpdate doesn't allow you to make changes to your entities. You can only use the computed change-set passed to the event to modify primitive field values. I bet if you check the database you'll see that the Person entities did get updated, you just won't see them in the $persons variable until the next time you manually retrieve them.
What you'll have to do after the flush is retrieve the entities from the database to see their updates values:
$em->flush();
$personIds = array_map(function($person) { return $person->getId(); }, $persons);
$updatedPersons = $em->getRepository('AppBundle:Person')->findById($personIds);
return new JsonResponse($serializer->serialize($updatedPersons, 'json'));

Related

Symfony WebTestCase hook into database listeners

So Im trying to hook into the database events of doctrine from the WebTestCase of Symfony.
I setup the client like this:
self::$client = static::createClient();
self::$client->followRedirects(true);
self::$client->setMaxRedirects(10);
$doctrineDefaultManager = self::getContainer()->get('doctrine.orm.default_entity_manager');
$doctrineDefaultManager->getConnection()->getConfiguration()->getSQLLogger();
/** some other code loading database and fixtures etc **/
// Im adding the event listener to the defaultmanager here
$doctrineDefaultManager->getEventManager()->addEventListener([Events::onFlush], new FlushExampleListener());
https://www.doctrine-project.org/projects/doctrine-orm/en/2.13/reference/events.html#listening-and-subscribing-to-lifecycle-events
Then I have the example class to: (this class never get logged or fired or dumped)
use Doctrine\ORM\Event\OnFlushEventArgs;
class FlushExampleListener
{
/**
* #param OnFlushEventArgs $eventArgs
* #return void
*/
public function onFlush(OnFlushEventArgs $eventArgs): void
{
$em = $eventArgs->getObjectManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
dd($entity);
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
dd($entity);
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
dd($entity);
}
foreach ($uow->getScheduledCollectionDeletions() as $col) {
dd($col);
}
foreach ($uow->getScheduledCollectionUpdates() as $col) {
dd($col);
}
}
}
https://www.doctrine-project.org/projects/doctrine-orm/en/2.13/reference/events.html#reference-events-on-flush
Then I post a form with the WebTestCase like this:
public function iSubmitAFormOnPageValue(string $url, string $btn): void
{
$crawler = self::$client->request('GET', $this->getHost() . $url, [], [], []);
$form = $crawler->selectButton($btn)->form();
$this->crawler = self::$client->submit($form, $this->formData);
$this->response = self::$client->getResponse();
}
What works fine and in this case if I hit the /entity/new route it works. I see a new record in the database. Also can I see with a new GET request that indeed the webpage is updated. Only does the onFlush event not get fired? So how can I see what get inserted into the database from the WebCaseTest or does symfony use another doctrine manager to insert stuff into the database where I need to put the event listener on?

ReferenceMany in Doctorine with Symfony

I am using Symfony with Doctrine.
The annotation for $members (getMembers() returns this variable):
/**
* #var User
* #MongoDB\ReferenceMany(targetDocument="something", storeAs="dbRef")
* #Assert\NotNull
* #JMS\Groups({"Default", "something"})
*/
protected $members;
The controller: (I used $form->submit)
public function updateAction($id, Request $request)
{
$project = $this->fetchProject($id);
$oldMembers = $project->getMembers();
$form = $this->createForm(...);
$form->submit($request->request->all(), false);
$newMembers = $project->getMembers();
...
$this->persist(...);
I add new members in the form and submit it but the '$oldMembers' and the '$newMembers' are the same! which is not desirable!
both of them are referring to the the new data (getMembers()). but I need to keep $oldMembers separate from $newMembers. how?
try to refresh the entity manager like this:
$em = $this->getDoctrine();
$em->refresh($project);
Or you can add member manually if refresh doesn't work
You are using a Form, but if you can add manually members like this:
$project->addMember($member);
Into your entity you can have a method like this:
public function addMember(Member $member)
{
$this->member[] = $member;
return $this;
}

how to synchronize a value between two entities

My project is a php project built with symfony 2.7
I have two entities: Lead and Customer.
Some user action create a Lead entity, and an admin can create a Customer entity based on a Lead.
Both the Lead and the Customer entity have a one to many relation to an entity Status (a Lead/Customer can have a status)
Up until now the Status of a Lead was independent from that of the Customer created from it (and vice versa), but now, I need to keep them synchronized.
They have to be the same.
I don't want to do that in controllers, because the status can be changed in many ways, and it's likely new ways will be added.
So I tried to do it with doctrine PreUpdate event listeners, but for some reason that's just not working (changing the other entity in the first one's PreUpdate event triggers a PreUpdate on the second one - and that probably kills it. but I'm not sure).
I'm going to keep trying fixing the PreUpdate event listeners, unless someone here has a better idea?
Thanks :-)
Customer PreUpdate listener
use CRM\CoreBundle\Entity\Customer;
use Doctrine\ORM\Event\PreUpdateEventArgs;
class CustomerPreUpdateListener
{
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getObject();
if (!($entity instanceof Customer)) {
return;
}
if($this->isStatusChanged($args)) {
$this->syncParentLeadStatus($args);
}
}
private function isStatusChanged(PreUpdateEventArgs $args)
{
return $args->hasChangedField('status');
}
private function syncParentLeadStatus(PreUpdateEventArgs $args)
{
/** #var Customer $entity */
$entity = $args->getObject();
if(!$entity->getLead()) {
return;
}
$newStatus = $args->getNewValue('status');
$entity->getLead()->setStatus($newStatus);
$args->getEntityManager()->persist($entity->getLead());
}
}
Lead PreUpdate listener
use CRM\BasicLeadsBundle\Entity\Lead;
use CRM\CoreBundle\Entity\Customer;
use Doctrine\ORM\Event\PreUpdateEventArgs;
class LeadPreUpdateListener
{
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getObject();
if (!($entity instanceof Lead)) {
return;
}
if($this->isStatusChanged($args)) {
$this->syncCustomerStatus($args);
}
}
private function isStatusChanged(PreUpdateEventArgs $args)
{
return $args->hasChangedField('status');
}
private function syncCustomerStatus(PreUpdateEventArgs $args)
{
/** #var Lead $entity */
$entity = $args->getObject();
if(0 === $entity->getCustomersCreated()->count()) {
return;
}
$newStatus = $args->getNewValue('status');
foreach ($entity->getCustomersCreated() as $customer) {
/** #var Customer $customer */
$customer->setStatus($newStatus);
$args->getEntityManager()->persist($customer);
}
}
}

Doctrine's hasChangedField to account for changes made in the listener

I have a User entity, and I would like to archive it when banned. I have the following preUpdate listener:
/**
* #ORM\PreUpdate
*/
public function preUpdate(PreUpdateEventArgs $eventArgs) {
if ($eventArgs->hasChangedField('banned') {
$this->setIsArchived(true);
}
if ($eventArgs->hasChangedField('isArchived')) {
/* do Special work here */
}
}
How do I inform eventArgs about the field changed inside the handler itself?
if you edit an entity within the eventArgs, I think you need to persist it and then run the computeChangeSet or computeChangeSets in the UnitOfWork in order for you to use the hasChangedField:
$entity = $eventArgs->getObject();
$em = $eventArgs->getObjectManager();
$uow = $em->getUnitOfWork();
if ($eventArgs->hasChangedField('banned') {
$entity->setIsArchived(true);
$em->persist($entity);
}
$uow->computeChangeSets();
if ($eventArgs->hasChangedField('isArchived')) {
/* do Special work here */
}

Symfony2, Doctrine2 update row without ID (this table is with only "one" row)

I want to create my entity Settings, which will have basic editable information about my page. I've created my entity Settings.php with this source:
<?php
namespace Acme\SettingsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\Table(name="settings")
*/
class Settings
{
/**
* #ORM\Column(type="string", length=100)
*/
protected $page_name;
/**
* #ORM\Column(type="string", length=100)
*/
protected $page_description;
/**
* #ORM\Column(type="string", length=100)
*/
protected $page_email;
}
and I do not know, how to tell in my controller that will be only overwriting existing data, not creating new. This is my controller AdminController.php
public function indexAction(Request $request)
{
if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
$settings = new Settings();
$form = $this->createForm('settings', $settings);
$form->handleRequest($request);
if($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->persist($settings);
try {
$em->flush();
} catch (\PDOException $e) {
// sth
}
$this->get('session')->getFlashBag()->add(
'success',
'Settings was successfuly changed'
);
}
return $this->render('AcmeSettingsBundle:Admin:index.html.twig', array('form' => $form));
}
I didn't test it, but I believe, it creates a new Settings object with new data. Any help?
You should always set some field to act as an ID, even if it meant to be dummy.
Assign some default value that you will always use and then you should be able to update your settings with no problem.
Usually, DBMS wars you about absence of unique identifier so same applies to Doctrine.
$settings = new Settings();
$settings->setId(Settings::DUMMY_IDENTIFIER); // const within class
# rest of the logic
# ....
$em = $this->getDoctrine()->getManager();
$em->persist($settings);
try {
$em->flush();
} catch (\PDOException $e) {
}
You could take another approach: persist every property as a single row. However, you would need to build more complex form type and execute more queries.
EDIT:
You could use raw SQL but that is less flexible:
# raw query
$sql = "UPDATE settings SET page_name = ?, page_description = ?, page_email = ?";
# data
$params = array( $settings->getPageName(), $settings->getPageDesc(), $settings->getPageEmail());
# must specify type due to protection against sql injection
$types = array(\PDO::PARAM_STR,\PDO::PARAM_STR,\PDO::PARAM_STR);
# execute it
$stmt = $this->getEntityManager()->getConnection()->executeUpdate($sql, $params, $types);
http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/data-retrieval-and-manipulation.html#using-prepared-statements

Categories