Creating a new entity, and updating another at once - datetime

I've searched a lot about this, and seriously asking is my last resource, doctrine is kicking me hard.
I have an entity named "Contract" and another "Request", a Contract may have several Requests, when adding a new Request I search for an existent contract of that client and associate it if already exists or create it if not.
In RequestRepository.php:
public function findOrCreate($phone)
{
$em = $this->getEntityManager();
$contract = $this->findOneBy(array('phone' => $phone));
if($contract === null)
{
$contract = new Contract();
$contract->setPhone($phone)
->setDesDate(new \DateTime());
# save only if new
$em->persist($contract);
}
return $contract;
}
The thing is, when the contract is new it works ok, but when is "reused" from db I can't modify its attributes. I checked the OneToMany and ManyToOne already.
In Contract.php:
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\OneToMany(targetEntity="Request", mappedBy="contract")
*/
private $id;
In Request.php:
/**
* #var string
*
* #ORM\JoinColumn(nullable=false)
* #ORM\ManyToOne(targetEntity="Cid\FrontBundle\Entity\Contract", inversedBy="id", cascade={"persist"})
*/
protected $contract;
I also have a method which modifies an attribute within Contract.php:
public function addTime($months)
{
$days = $months * 30;
$this->des_date->add(new \DateInterval("P".$days."D"));
return $this;
}
I create the request and "findOrCreate" a contract, but if the later is not "fresh" the addTime does not save to db.
What am I doing wrong?
Edit: The controller is a common CRUD with minor modifications.
Don't worry about "request" name clash, the actual code is in spanish, Request = Solicitud
public function createAction(Request $req)
{
$entity = new Request();
$form = $this->createForm(new RequestType(), $entity);
$form->bind($req);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$entity->setUser($this->getUser());
$data = $request->request->get('cid_frontbundle_requesttype');
$phone = $data['phone_number'];
$reqRep = $em->getRepository('FrontBundle:Request');
$entity = $reqRep->newRequest($entity, $phone);
return $this->redirect($this->generateUrl('request_show', array('id' => $entity->getId())));
}
return $this->render('FrontBundle:Request:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
The newRequest:
public function newRequest($request, $phone)
{
$em = $this->getEntityManager();
$contractRep = $em->getRepository('FrontBundle:Contract');
$contract = $contractRep->findOrCreate($phone);
$contract->addTime(123); # this is the problem, I use var_dump and this method works, but doesn't persists
$em->persist($request);
$em->flush();
return $request;
}

Eureka!! The issue was that doctrine seems to check the objects by reference, and all I did with the contract was adding a DateInterval to a DateTime property, so the object was the same for doctrine's matter and there was no saving. This is the code that made it.
public function addTime($months)
{
$days = $months * 30; # I know DateInterval has months but this is company policy ;)
$other = new \DateTime($this->des_date->format('Y-m-d')); # creating a brand new DateTime did the trick
$other->add(new \DateInterval("P".$days."D"));
$this->des_date = $other;
return $this;
}
Thanks for everything #cheesemacfly.

Related

Database updates not readable while PHPunit WebTestCase (Symfony 5.1)

I have a problem asserting updates made by a form are really written in the database.
I explain, I first do a create test (testCreateLactationForm), very similar to the edit test. In that firs test y check the record is finally inserted and all works fine:
$finalRecords = $qb->where($qb->expr()->like('p.comments', ':comments'))
->setParameter('comments', '%'.$payload["form[comments]"].'%')
->getQuery()
->getResult();
$this->assertTrue(count($finalRecords) == (count($initialRecords) + 1));
After that, I made a dependant test where I locate las record, navigate to edit form, and update the modality and comments fields. In last line, when asserting Modality is equal to ONE_HOUR_REDUCTION, entity modality field remains with original creation value, so it fails, while in database the value was succesfully updated.
class HhrrProceduresControllerTest extends WebTestCase
{
/** #var \Doctrine\ORM\EntityManager */
private $entityManager;
private $client;
public function setUp()
{
parent::setUp();
$this->client = self::createClient();
$kernel = self::bootKernel();
$this->entityManager = $kernel
->getContainer()
->get('doctrine')
->getManager();
$this->hrmLogger = $kernel
->getContainer()
->get('monolog.logger.hrm');
}
[...]
/**
* #depends testCreateLactationForm
*/
public function testEditLactationProcedure()
{
$payload = [
"hhrr_procedure_lactation[modality]" => HhrrProcedureLactationModality::ONE_HOUR_REDUCTION,
"hhrr_procedure_lactation[iniDate]" => "07/02/2022",
"hhrr_procedure_lactation[endDate]" => "13/02/2022",
"hhrr_procedure_lactation[hhrrProcedure][comments]" => "XXX--TEST--XXX | UPDATED",
];
$qb = $this->entityManager->getRepository(HhrrProcedure::class)->createQueryBuilder('p');
$initialRecord = $qb->orderBy('p.id', 'DESC')
->setMaxResults(1)
->getQuery()
->getResult();
$this->client->followRedirects(true);
$this->client->request('GET', '/rrhh/tramites/'.$initialRecord[0]->getId().'/editar-tramite/', [], [], ['HTTP_X-AUTH-USERNAME' => $this->logged_in_user]);
$crawler = $this->client->submitForm('Siguiente', $payload);
$this->writeOutput(__FUNCTION__, $this->client->getResponse()->getContent());
// verificar retorno correcto de la página
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
/** #var HhrrProcedure $updatedRecord */
$updatedRecord = $this->entityManager->getRepository(HhrrProcedure::class)->find($initialRecord[0]->getId());
/** FAILS --> **/ $this->assertEquals($updatedRecord->getHhrrProcedureLactation()->getModality(),HhrrProcedureLactationModality::ONE_HOUR_REDUCTION);
}
In addition to that, if I try to write aditional dependant test, assertion is ok...
/**
* #depends testEditLactationProcedure
*/
public function testEditLactationProcedureAssertion()
{
$qb = $this->entityManager->getRepository(HhrrProcedure::class)->createQueryBuilder('p');
$initialRecord = $qb->orderBy('p.id', 'DESC')
->setMaxResults(1)
->getQuery()
->getResult();
$this->assertEquals($initialRecord[0]->getHhrrProcedureLactation()->getModality(),HhrrProcedureLactationModality::ONE_HOUR_REDUCTION);
}
Some help? Thanks!!
Doctrine entities are persisted in an internal cache (which is not the case when you use findBy methods, that load a new documents). You need to use
EntityManagerInterface's refresh($entityObject) or clear() in order to get fresh data from the database.

Doctrine ManyToMany self referencing bidirectionnal - Parent not updated

I'm trying to create a ManyToMany relation beetwin services of a company.
Each service had N parents services and N children services.
I looked at the doctrine documentation here : Many-To-Many, Self-referencing and I implemented it as followed :
Here is my service entity :
<?
namespace AppBundle\Entity;
class Service
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
private $parents;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", inversedBy="parents")
* #ORM\JoinTable(name="app_services_hierarchy",
* joinColumns={#ORM\JoinColumn(name="parent_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="enfant_id", referencedColumnName="id")}
* )
*/
private $enfants;
public function __construct()
{
$this->enfants = new ArrayCollection();
$this->parents = new ArrayCollection();
}
public function getId(){
return $this->id;
}
//--------------------------------------------------Enfants
public function getEnfants(){
return $this->enfants;
}
public function setEnfants($enfant){
$this->enfants = $enfant;
}
public function addEnfant(Service $s){
$this->enfants[] = $s;
return $this;
}
public function removeEnfant(Service $s){
$this->enfants->removeElement($s);
}
//--------------------------------------------------Parents
public function getParents(){
return $this->parents;
}
public function setParents($parents){
$this->parents = $parents;
}
public function addParent(Service $s){
$this->parents[] = $s;
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
}
}
And here is my edit function( Controller.php) :
public function editAction(Request $request, $id)
{
$service = $this->getDoctrine()->getRepository(Service::class)->find($id);
$form = $this->createForm(ServiceType::class, $service);
$form ->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager ->persist($service);
dump($service);
$entityManager ->flush();
}
return $this->render('AppBundle:Service:edit.html.twig', array(
'form' => $form->createView(),
));
}
And the generated form looks like :
PROBLEM :
My problem is that the childrens are updated but not the parents. I can see the parents in the $service variable when I dump() it in my controler but the only ones updated in my database table (app_services_hierarchie) are the children.
The difference between $parents and $enfants in your code is that the service you are looking at is the Owning side in case of your $enfants mapping, but not in the case of your $parents mapping.
Doctrine will not store the $parents unless you tell it to do so via cascade={"persist"}.
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
This is basically the same anwer given in the post linked by #GregoireDucharme.
Edit: after some research, apparently this problem cannot be solved using cascade. According to the Doctrine documentation:
Doctrine will only check the owning side of an association for changes.
So what you have to do is tell your $parents to also update the $children property.
public function addParent(Service $s){
$this->parents[] = $s;
$s->addEnfant($this);
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
$s->removeEnfant($this);
}
In your form, make sure to specify the following:
->add('parents', 'collection', array(
'by_reference' => false,
//...
))
(I haven't spellchecked any of the code above, so tread carefully.)
If 'by_reference' is set to true, addParent and removeParent will not be called.
Credit goes to this blog post by Anny Filina.
It also states that you can remove the cascade option from your $parents property, but you probably should add cascade={"persist","remove"} to your $enfants property.

Symfony doctrine ManyToMany unidirectional mapping

Well, back again, i'll try to simplify my question as much as i can.
First of all, i have 2 Entities
Post
PostRating
I've created unidirectional ManyToMany relation between them, because I only need ratings to be added to each Post, if I try to map Post to PostRating too, I get Circular Reference error.
Post Entity, it creates 3rd table post_has_rating, no mapping inside PostRating Entity, It workes like expected, rating collection is added to each post, but if i want to find one rating, and edit it if needed, then it comes to be bigger headache than expected.
/**
* Post have many PostRating
* #ORM\ManyToMany(targetEntity="PostRating")
* #ORM\JoinTable(name="post_has_rating",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="postrating_id", referencedColumnName="id", unique=true)}
* )
*/
protected $ratings;
PostController thumbAction, simple word "ratingAction"
/**
* Search related videos from youtube
* #Route("/post/thumb", name="post_thumb")
* #param Request $request
* #return string
*/
public function thumbAction (Request $request) {
$content = json_decode($request->getContent());
$serializer = $this->get('serializer');
$em = $this->getDoctrine()->getManager();
$postRatingRepo = $this->getDoctrine()->getRepository(PostRating::class);
$postRepo = $this->getDoctrine()->getRepository(Post::class);
$me = $this->getUser()->getId();
/** #var PostRating $rating */
$rating = $postRatingRepo->findOneBy(['userId' => $me]);
/** #var Post $post */
$post = $postRepo->find($content->id);
if ($post->getRatings()->contains($rating)) {
$post->removeRating($rating);
$em->remove($rating);
}
$rating = new PostRating();
$rating->setUserId($me);
switch ($content->action) {
//NVM about those hardcoded words, they are about to be removed
case 'up':
$rating->setRating(1);
break;
case 'down':
$rating->setRating(0);
break;
}
$post->addRating($rating);
$em->persist($rating);
$em->persist($post);
$em->flush();
return new JsonResponse( $serializer->normalize( ['success' => 'Post thumbs up created'] ) );
}
Problems: $rating = $postRatingRepo->findOneBy(['userId' => $me]); this row needs to have postId too for $post->getRatings()->contains($rating), right now im getting all the raitings, that I have ever created, but it Throws error if i add it, Unknown column
Should i create custom repository, so i can create something like "findRating" with DQL?
OR
Can i make Post and PostRating Entities mapped to each other more simple way, i don't really want many-to-many relation, because I don't see point of using it
Considering you want to keep OneToMany unidirectional here is my suggestion
create a custom repository for your Post Entity
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class PostRepository extends EntityRepository
{
public function findOneRatingByUser($post, $user)
{
$query = $this->createQueryBuilder('p')
->select('r')
->innerJoin('p.ratings', 'r')
->where('p.id = :post')
->andWhere('r.user = :user')
->setParameter('post', $post)
->setParameter('user', $user)
->getQuery()
;
return $query->getOneOrNullResult();
}
}
Then in your controller:
public function thumbAction (Request $request)
{
$content = json_decode($request->getContent());
$serializer = $this->get('serializer');
$em = $this->getDoctrine()->getManager();
$postRepo = $this->getDoctrine()->getRepository(Post::class);
$me = $this->getUser()->getId();
/** #var Post $post */
$post = $postRepo->find($content->id);
$rating = $postRepo->findOneRatingByUser($post->getId(), $me);
if (null === $rating) {
$rating = new PostRating();
$rating->setUserId($me);
}
switch ($content->action) {
//NVM about those hardcoded words, they are about to be removed
case 'up':
$rating->setRating(1);
break;
case 'down':
$rating->setRating(0);
break;
}
$post->addRating($rating);
$em->persist($rating);
$em->persist($post);
$em->flush();
return new JsonResponse( $serializer->normalize( ['success' => 'Post thumbs up created'] ) );
}
If you want your custom repository to work dont forget to declare it in your entity
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
*/
class Post

Symfony - Avoid store related entities in preFlush Doctrine

I have an entity Subject:
/**
*
* #ORM\Table()
* #ORM\HasLifecycleCallbacks()
*/
class Subject
{
//... Some fields
/**
* #ORM\OneToMany(targetEntity="Subject", mappedBy="mark", cascade={"persist", "remove"})
*/
private $subjects;
private function calculateMarks()
{
//... Do something
// return array with (abilities => marks);
}
/**
* #ORM\PrePersist()
*/
public function prePersist(){
$now = new \DateTime();
$this->setCreatedAt( $now );
$this->setModifiedAt( $now );
}
/**
* #ORM\PreUpdate()
*/
public function preUpdate(){
$this->setModifiedAt( new \DateTime() );
$this->setUpdated(true);
}
/**
* #ORM\PreFlush()
*/
public function preFlush(){
$marks = calculateMarks();
foreach($marks as $ability => $score){
$mark = new Mark();
$mark->setSubject( $this );
$this->addMark( $score );
$mark->setAbility( $ability );
}
}
}
and the class Mark:
class Mark{
// Many fields
/**
* #ORM\ManyToOne(targetEntity="Subject", inversedBy="subjects")
* #ORM\JoinColumn(name="subject_id", referencedColumnName="id")
*/
private $subject;
}
My problem is that I calculate and I create the Marks in the preFlush event (this is done this because in the official documentation is said this about preUpdate event: "Changes to associations of the updated entity are never allowed in this event, since Doctrine cannot guarantee to correctly handle referential integrity at this point of the flush operation"). When I save one subject, all work fine, but when I save many Subjects at the same time in a webservice, some marks are stored in the database many times.
The webservice action below:
public function setSubjects(Request $request)
{
//... Do something
$subjects = $request["Subjects"];
foreach($subjects as $s){
$em = $this->getDoctrine()->getManager();
//... Do something
$em->persist($s);
$em->flush();
}
return new JsonResponse($response);
}
Has anybody an idea of how could I avoid this behavior in the preFlush event?
Thanks in advance.
I always try to avoid LifecycleCallbacks unless it's simple and i'm only changing properties in the same entity.
to solve your issue i would create a function calculateMarks() inside the entity and tweak my loop to be something like
$em = $this->getDoctrine()->getManager();
foreach($subjects as $s){
//... Do something
$s->calculateMarks();
$em->persist($s);
}
$em->flush();
NOTICE
avoid $em = $this->getDoctrine()->getManager(); & $em->flush(); inside the loop

Cascading an association

I am trying to embed a collection of forms, but I am having trouble persisting the newly created objects.
A Customer has many Emails. In my controller,
// CustomersController.php
$customer = new Customer();
$customer->setCreatedBy(0);
$blankEmail = new Email();
$customer->addEmail($blankEmail);
$form = $this->createForm(new CustomerType(), $customer);
I did remember to set the cascade option in my Customer class:
// Customer.php
...
/**
* #ORM\OneToMany(targetEntity="Email", mappedBy="customer", cascade={"persist"})
*/
protected $emails;
My Email class also has the required information:
// Email.php
...
/**
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="emails", cascade={"persist"})
* #ORM\JoinColumn(name="customers_id", referencedColumnName="id")
*/
protected $customer;
For some reason, this doesn't work:
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($customer);
$em->flush();
It adds the customer alright, but when it tries to add the Email, it says no customerId has been set. So, I tried this, and it works:
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($customer->getEmails() as $email)
$email->setCustomer($customer);
$em->persist($email);
}
$em->persist($customer);
$em->flush();
But I'd really like to be able to get it in one fell swoop, just by persisting the $customer object (as I know it can).
Try to change the default "addEmail" method in your Customer class.
This method should look like:
public function addEmail($email)
{
$email->setCustomer($this);
$this->emails[] = $email;
return $this;
}

Resources