Symfony form not saving entity with ManyToMany relation

纵然是瞬间 提交于 2019-12-04 12:56:23

In Symfony2 the entity with the property with the inversedBy doctrine comment is the one that is supposed to EDIT THE EXTRA TABLE CREATED BY THE MANYTOMANY RELATION. That is why when you create a customer it inserts the corresponding rows in that extra table, saving the corresponding pets.

If you want the same behavior to happen the other way around, I recommend:

//PetController.php
public function createAction(Request $request) {
    $entity = new Pet();
    $form = $this->createCreateForm($entity);
    $form->submit($request);



    if ($form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        foreach ($form['customer']->getData()->getValues() as $v) {
            $customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
            if ($customer) {
                $customer->addPet($entity);
            }
        }
        $em->persist($entity);
        $em->flush();

        return $this->redirect($this->generateUrl('pet_show', array('id' => $entity->getId())));
    }

    return $this->render('AppBundle:pet:new.html.twig', array(
                'entity' => $entity,
                'form' => $form->createView(),
    ));
}

private function createCreateForm(Pet $entity) {
        $form = $this->createForm(new PetType(), $entity, array(
            'action' => $this->generateUrl('pet_create'),
            'method' => 'POST',
        ));

        return $form;
    }

These two are but standard Symfony2 CRUD-generated actions in the controller corresponding to Pet entity.

The only tweak is the foreach structure inserted in the first action, that way you forcibly add the same pet to each customer you select in the form, thus getting the desired behavior.

Look, it is highly probable THIS is not the RIGHT WAY, or the PROPER WAY, but is A WAY and it works. Hope it helps.

In my case with a services <-> projects scenario, where services has "inversedBy" and projects has "mappedBy" I had to do this in my project controller's edit action so that when editing a project the services you checked would be persisted.

public function editAction(Request $request, Project $project = null)
{
    // Check entity exists blurb, and get it from the repository, if you're inputting an entity ID instead of object ...

    // << Many-to-many mappedBy hack
    $servicesOriginal = new ArrayCollection();
    foreach ($project->getServices() as $service) {
        $servicesOriginal->add($service);
    }
    // >> Many-to-many mappedBy hack

    $form = $this->createForm(ProjectType::class, $project);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();

        // << Many-to-many mappedBy hack
        foreach ($servicesOriginal as $service) {
            if (!$project->getServices()->contains($service)) {
                $service->removeProject($project);
                $em->persist($service);
            }
        }

        foreach ($project->getServices() as $service) {
            $service->addProject($project);
            $em->persist($service);
        }
        // >> Many-to-many mappedBy hack

        $em->persist($project);
        $em->flush();

        return; // I have a custom `redirectWithMessage()` here, use what you like ...
    }

    return $this->render("Your-template", [
        $form       => $form->createView(),
        $project    => $project,
    ]);
}

This works for both adding and removing entities in the many-to-many from the "mappedBy" side, so EntityType inputs should work as intended.

What's going on here is we're first building an "original" collection containing all of the service entities already linked to for this project. Then when the form is saving we're ensuring:

  • First that any unchecked services (those in the original collection but not the project object) have the project removed from their internal collection, then persisted.
  • Second that any newly checked services each add the project to their internal collection, then persisted.

Important: This depends on your entity's addService() and addProject() methods respectively check that each others' collections do not contain duplications. If you don't do this you'll end up with an SQL level error about a duplicate record insertion.

In the service entity I have:

/**
 * Add project
 *
 * @param Project $project
 *
 * @return Service
 */
public function addProject(Project $project)
{
    if (!$this->projects->contains($project)) {
        $this->projects->add($project);
    }

    if (!$project->getServices()->contains($this)) {
        $project->getServices()->add($this);
    }

    return $this;
}

In the project entity I have:

/**
 * Add service
 *
 * @param Service $service
 *
 * @return Project
 */
public function addService(Service $service)
{
    if (!$this->services->contains($service)) {
        $this->services->add($service);
    }

    if (!$service->getProjects()->contains($this)) {
        $service->getProjects()->add($this);
    }

    return $this;
}

You could alternatively check this in your controller instead, but makes sense if the model validates this itself when possible, as the model would break anyway if there were duplicates from any source.

Finally in your controller's create action you'll likely need this bit too just before $em->persist($project). (You won't need to work with an "original" collection as none will exist yet.)

// << Many-to-many mappedBy hack
foreach ($project->getServices() as $service) {
    $service->addProject($project);
    $em->persist($service);
}
// >> Many-to-many mappedBy hack
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!