Proper way to update bidirectional Many to Many relationship symfony2-doctrine
Asked Answered
F

4

4

I did some research, and after reading this and this (and all the related questions) I still can't figure which is the proper way to update a many to many relationship in Symonfy 2 Doctrine. It feels that there should be a very simple way of doing it that I still haven't found.

I have this 2 entities:

class student_main
{
/**
* @ORM\ManyToMany(targetEntity="support_log", inversedBy="student_main")
* @ORM\JoinTable(name="support_log_student")
**/
private $support_log;

and

class support_log
{
/**
* @ORM\ManyToMany(targetEntity="student_main", mappedBy="support_log")
**/
private $student;

I want to start from support_log. In the controller, in the update action, I have something like that:

if ($editForm->isValid()) {        
  //add the relationship the user added
  foreach($students as $student){
    if(!$em->getRepository('mybundle:student_main')->hasSupportLog($entity,$student)){
        $entity->addstudent_main($student);//*
        }
    }
    $em->persist($entity);
    $em->flush();
    return $this->redirect($this->generateUrl('support_log_edit', array('id' => $id)));
}

Of course, as doctrine Documentation says, I changed that function (addstudent_main) accordingly:

public function addstudent_main(student_main $student)
{
    $student->addsupport_log($this); // the important addition
    $this->student[] = $student;
}

This works fine, my question is more about deleting the relationship. In the form there is a multiselect, and the user might select some students that are already related and some that don't. It feels that there should be an automatic way of doing that, but instead I had to do lots of code.

In the controller, just slightly above the code I wrote before, I put that:

//delete all old relationship
foreach($idsldstudents as $idst){ //I take Id's because the doctrine collection is updating always..
            $stu=$em->getRepository('MyBundle:student_main')->find($idst);
            $stu->deletesupport_log($entity);//I had to create that method (in the entity, I do "$this->support_log->removeElement($support_log)")
            $em->persist($stu);
            $em->flush();
        }

I delete all the relationships of the entity in question (of course, taking in care that is a bidirectional relationship, so it has to be deleted first in the other side), and then the ones that the user selected will be added.

There are other ways of doing that, but I haven't found any simple one. In all of them I have the same problems:

  • I need to check all the time if the relationship exists or not
  • I need to get the old relationships (which is difficult) and compare to the new ones that the user indicated, and delete or create accordingly

Is there a way of doing that that takes care of these 2 problems automatically? (I have a strong feeling that there must be - maybe with a better declaration of the relationship? - that's why I am asking).

Thanks in advance

Edit: My form hasn't anything special, I think I didn't even touch the generated code. It displays the multiselect that I want, the default of Symfony2, where you have to use ctrl key to select more than one. Here is the code:

public function buildForm(FormBuilder $builder, array $options)
{
    $builder           
        ->add('student')
        ... 
        ;
}

The key relies here?

Foliation answered 26/3, 2013 at 11:11 Comment(4)
First idea is to use $student->supportLog->clear(); instead of loop. But it's not answer, it's just improvement. Second is that form can manage collections automatically. Can you show the code of your form type, please.Eggert
This first idea gives a pretty valuable improvement, it is much more elegant! thanks! For the form, it is the plane generated by Symfony, I didn't even touch I think, take a look at the Edit please @DmitryKrasunFoliation
(just a note, in supportlog controller: $student->supportLog->clear(); won't work, because it will delete all the relationships that the student has, and I just want to delete one relationship, with the support log I am currently editing (and the loop is to do the same for all the other students that are already related to this support log that I am currently editing)Foliation
I'm dropping this here, since I don't have a solution that is symfony-specific. This should clarify how to handle many to many at entity level: gist.github.com/3121916Collator
F
2

So far, (and to avoid having a question unanswered forever), it looks like there is no "simple way that I still haven't found" to do that. That would be the answer to my question, according to the comments.

But the code can be improved and make it more elegant thanks to the improvements of last comment. If at entity level we have that: gist.github.com/3121916 (from the comment)

Then, the code in the controller can be reduced a little bit:

$editForm->bindRequest($request);
  if ($editForm->isValid()) { 
  //delete all old relationships, we can go from student:    
  foreach($em->getRepository('mybundle:student_main')->findAll() as $oldstudent)
  {
     $oldstudent->removeSupportLog($entity);
     //if they are related, the relationship will be deleted. 
     //(check the code from the url)  
  }  
  //add the relationship the user added in the widget
  $students=$entity->getStudent();
  foreach($students as $student) 
  {
     $entity->addstudent_main($student);
  }
  $em->persist($entity);
  $em->flush();
  return $this->redirect($this->generateUrl('support_log_edit', array('id' => $id)));
}

It is still not the "magical" symfony solution I expected, but so far the best I can do (maybe group this code inside a function in the repository, to make it more elegant).

If you have better ideas, I'm all ears.

Foliation answered 1/4, 2013 at 0:34 Comment(0)
R
2

I present my solution to everybody who are searching a solution.

I use Symfony 2.5.

My 'Post' entity has many-to-many bidirectional.

Controller:

public function editPostAction(Post $post, Request $request)
{
    $form = $this->createForm(new NewPost(), $post, [
            'action' => $this->generateUrl('admin_edit_post', ['id' => $post->getId()])
        ]);

    $form->handleRequest($request);

    if( $form->isSubmitted() )
    {
        $this->get('post.repository')->update();
    }

    return $this->render('BlogJakonAdminPanelBundle:Post:post-edit.html.twig', array(
            'form' => $form->createView(),
            'errors' => $form->getErrors(true)
        ));
}

I bind my entity by following routing:

admin_edit_post:
    path:     /post/edit/{id}
    defaults: { _controller: BlogJakonAdminPanelBundle:Post:editPost }

My repository:

public function update()
{
    try {
        $this->getEntityManager()->flush();
    } catch (\Exception $e) {
        $this->getEntityManager()->getConnection()->rollback();
        return false;
    }

    return true;
}

Form class:

class NewPost extends AbstractType
{

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            [...]
            ->add('categories', 'entity', array(
                    'class' => 'BlogJakon\PostBundle\Entity\Category',
                    'property'     => 'name',
                    'multiple'     => true,
                    'expanded'     => true)
            )
            ->add(
                'save',
                'submit',
                [
                    'label' => 'Add post'
                ]
            )
            ->getForm();

    }

    public function setDefaultOption(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class'      => 'BlogJakon\PostBundle\Entity\Post',
                'csrf_protection' => true
            ]
        );
    }

    public function getName()
    {
        return 'newPost';
    }
} 

It is worth to mention that Symfony can find given entity (Post) by adding only id to routing:

/post/edit/{id}

Refugee answered 24/11, 2014 at 19:57 Comment(0)
H
2

According to doctrine documentation it will only check the owning side of an association for changes.

http://doctrine-orm.readthedocs.org/en/latest/reference/unitofwork-associations.html

So the best way is keep owning side entity assotiations updated.

In this case you must remove and add support_log in student main in add and remove methods

class support_log
{
/**
* @ORM\ManyToMany(targetEntity="student_main", mappedBy="support_log")
**/
private $student;

public function addStudent($student) {
  $this->student[] = $student;
  $student->addSupportLog($this);
}

public function removeStudent($student) {
  $student->removeSupportLog($this);
  $this->student->removeElement($student);
}

Thats all no need to modify controller actions. Important implement this at the inverse side of association!

Hemangioma answered 20/3, 2015 at 21:4 Comment(0)
M
1

ManyToMany Bidirectional with indexBy attribute on the annotation fixed this for me

Student class annotation should be

class student_main
{
    /**
     * @ORM\ManyToMany(targetEntity="support_log", mappedBy="student_main")
     **/
    private $support_log;

Support class annotation should be

    class support_log
    {
        /**
         * @ORM\ManyToMany(targetEntity="student_main", inversedBy="support_log", indexBy="id")
         * @ORM\JoinTable(name="support_log_student",
         *  joinColumns={@ORM\JoinColumn(name="support_log_id",referencedColumnName="id")},
         *  inverseJoinColumns={@ORM\JoinColumn(name="student_id", referecedColumnName="id")}
         * )
         **/            
        private $student;

Now the symfony 2 Form should be

    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
        ->add('student', 'entity', array(
                'class' => '<<ENTER YOUR NAMESPACE PATH TO ENTITY>>\Entity\Student',
                'property' => 'Name', //property you want to display on the select box
                'label' => 'Belongs to Students',
                'multiple' => true,
                'constraints' => array(
                    new NotBlank(array('message' => 'Please choose atleast one student'))
                )
            ))
        ....
        ;
    }

On submit of form usually inside the Action

        if ($editForm->isValid()) {
            $entity = $editForm->getData(); 
            $em->persist($entity); //this should take care of everything saving the manyToMany records 
            $em->flush();
            return $this->redirect($this->generateUrl('support_log_edit', array('id' => $id)));
        }           

Please note: I haven't tested this code. I rewrote this code to fit the scenario mentioned in the question.

Minneapolis answered 16/2, 2016 at 23:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.