Symfony2 Embedded forms, How to persist objects - symfony

I am trying to implement Embedded Forms (Symfony2, 2.7), with Task and Tag entities, One2Many.
To save reference to the Task object into a Tag record, I am able to define Task's createAction() only by:
/**
* Creates a new Task entity.
*
* #Route("/", name="MyName_Task_create")
* #Method("POST")
* #Template("MyNameBundleBlogBundle:Task:new.html.twig")
*/
public function createAction(Request $request)
{
$task = new Task();
$form = $this->createCreateForm($task);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$tags = $task->getTags();
foreach($tags as $tg){$tg->setTask($task); $em->persist($tg);} // <-- do I really need to loop?
$em->persist($task);
$em->flush();
return $this->redirect($this->generateUrl('MyName_Task_show', array('id' => $task->getId())));
}
return array(
'entity' => $task,
'form' => $form->createView(),
);
}
EDIT: I know it should work without the loop straightforwardly, but it does not. Question is: What should I look for which I might have written wrong? See related question
Note, I have:
class Task{
....
/**
*
* #ORM\OneToMany(targetEntity="Tag", mappedBy="Task", cascade={"persist"} )
*/
private $Tags;
....
/**
* Add tag
*
* #param \MyName\Bundle\BlogBundle\Entity\Tag $tag
*
* #return Task
*/
public function addTag(\MyName\Bundle\BlogBundle\Entity\Tag $tag)
{
$this->tags[] = $tag;
$tag->setTask($this);
return $this;
}
}

No, you don't need to loop through all tags and explicitly set task, Symfony will do that for you if you configure it correctly.
The only thing you need to add is set by_reference to false inside your form builder. In this case, symfony will explicitly will call setTask on every tag.
For more info 'by_reference'

According to #Cerad comment, the only thing you have to do is persist the Task.
All related tags will be automatically persisted, thanks to cascade={"persist"} in your association mapping.
Your code should be :
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($task);
$em->flush();
return $this->redirect($this->generateUrl('MyName_Task_show', array('id' => $task->getId())));
}
See the Etablishing associations and Cascade operations chapters of Doctrine documentation.

Related

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.

Lifecycle callbacks not working

I'm currently trying out Symfony 4, but I am having some problems with events triggered by database action (prePersist, preUpdate...)
With Symfony 3, I used to use EntityListener to accomplish this, but I found them really convoluted in Symfony 4 documentation. But I also discovered the LifecycleCallbacks, that I used like this:
/**
* #ORM\Entity(repositoryClass="App\Repository\PostRepository")
* #ORM\HasLifecycleCallbacks()
*/
class Post
{
//Attributes and other functions not included for the sake of clarity, but if I use them, consider that they exist
/**
* #ORM\PrePersist
*/
public function setPostSlug()
{
$title = $this->getPostTitle();
$title = strtolower($title);
$keywords = preg_split("/[\s,']+/", $title);
$slug = implode('-', $keywords);
dump($slug);
$this->$slug = $slug;
return $this;
}
}
My post are created through a Symfony form, and before persistence, I want to break down the title I gave to my post in a standardized string that I will use in my URLs to access said post. Unfortunately, the event never trigger on persistence, despite the slug being generated correctly. I tried to do the operation both on prePersist and postPersist events, but none worked. I searched the issue, and saw that LifecycleCallbacks needed a cache clear to be taken into account, but doing so didn't help.
Here is the action responsible for the post creation, if that might help:
/**
* #Route("/admin/create/post", name="admin-create-post")
* #param Request $request
*/
public function createPost(Request $request)
{
$post = new Post();
$form = $this->createForm(PostType::class, $post);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
$em = $this->getDoctrine()->getManager();
$post = $form->getData();
$em->persist($post);
$em->flush();
$this->redirectToRoute('main');
}
return $this->render('admin/new_post.html.twig', array(
'form' => $form->createView()
));
}
Would you know the source of the problem, or which other tools I could use to obtain the desired result?
Thanks in advance.
I handle complex Lifecycle with EventListener
for this .. do :
# services.yml
AppBundle\EventListener\YourListener:
tags:
- { name: doctrine.event_listener, event: prePersist }
// YourListener.php
namespace AppBundle\EventListener;
class YourListener {
/**
* #param LifecycleEventArgs $args
*/
public function prePersist(LifecycleEventArgs $args): void
{
$post = $args->getEntity();
if ($post instanceof Post) {
// Do your job
}
}
}
But I use symfony EventListenerSubscriber Like this:
/**
* This needs to be set through passed argument in case of accident duplicate
*
* #ORM\PrePersist()
*/
public function setTrackingNumber()
{
$this->trackingNumber = NumberCreator::randomStringWithNDigits(self::TRACKING_DIGIT_COUNT);
}
so I think you need do that in your slug setter like this
/**
* #ORM\PrePersist
*/
public function setSlug()
{
$title = $this->getPostTitle();
$title = strtolower($title);
$keywords = preg_split("/[\s,']+/", $title);
$slug = implode('-', $keywords);
dump($slug);
$this->$slug = $slug;
return $this;
}
I think method name is issue ... I hope this is help to you

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

Creating a new entity, and updating another at once

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.

Resources