Symfony表单未保存带有ManyToMany关系的实体

4

我在通过表单保存带有多对多关系的实体时遇到问题。

我无法保存在关系“mappedBy”一侧的字段。

以下代码未将任何内容保存到数据库中,也没有抛出任何错误:

// Entity/Pet
/**
 * @var \Doctrine\Common\Collections\Collection
 *
 * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Customer", mappedBy="pet", cascade={"persist"})
 */
private $customer;

/**
 * Set customer
 *
 * @param \AppBundle\Entity\Customer $customer
 * @return Pet
 */
public function setCustomer($customer)
{
    $this->customer = $customer;

    return $this;
}

// Entity/Customer
/**
 * @var Pet
 *
 * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Pet", inversedBy="customer", cascade={"persist"})
 * @ORM\JoinTable(name="customer_pet",
 *   joinColumns={
 *     @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
 *   },
 *   inverseJoinColumns={
 *     @ORM\JoinColumn(name="pet_id", referencedColumnName="id")
 *   }
 * )
 */
private $pet;

// PetType.php
$builder->add('customer', 'entity', 
          array(
            'class' => 'AppBundle:Customer',
            'property' => 'firstname',
            'empty_value' => 'Choose owner',
            'multiple' => true
          ));

它是反向工作的。所以,如果我从CustomerType保存某些东西,一切都正常。编辑:下面的解决方案对我有用,但几天后我发现了一个问题。如果表单提交的值已经保存在数据库中,则Symfony将抛出错误。为了防止这种情况,我必须检查是否已将给定客户分配给宠物。当前分配的客户的检查必须在函数开始时进行,而不是在表单提交后进行,因为由于某种原因,在提交后Pet()对象包含提交的值,而不仅仅是数据库中存在的值。所以在开始时,我将所有已分配的客户放入数组中。
  $em = $this->getDoctrine()->getManager();
  $pet = $em->getRepository('AppBundle:Pet')->find($id);
  $petOriginalOwners = array();
  foreach ($pet->getCustomer() as $petCustomer) 
  {
      $petOriginalOwners[] = $petCustomer->getId();
  } 

提交表单后,我检查了提交的ID是否在数组中

if ($form->isValid()) 
{
  foreach ($form['customer']->getData()->getValues() as $v) 
  {
    $customer = $em->getRepository('AppBundle:Customer')->find($v->getId());
    if ($customer && !in_array($v->getId(), $petOriginalOwners) )      
    {
      $customer->addPet($pet);
    }
  }
  $em->persist($pet);
  $em->flush();
  return $this->redirect($this->generateUrl('path'));
} 
3个回答

2
在Symfony2中,具有带有inversedBy doctrine注释的属性的实体应该编辑由ManyToMany关系创建的额外表。这就是为什么当您创建客户时,它会在该额外表中插入相应的行,保存相应的宠物的原因。
如果您希望以相同的方式发生反向行为,我建议:
//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;
    }

这两个动作都是标准的Symfony2 CRUD生成的Pet实体控制器中的操作。

唯一的调整是在第一个动作中插入了foreach结构,这样你就可以强制将相同的宠物添加到你在表单中选择的每个客户端,从而获得所需的行为。

请注意,这很可能不是正确的方式或适当的方式,但它是一种方式,并且有效。希望能帮到你。


谢谢,那个可行,我认为这个解决方案没有任何问题,它很简单。如果它能够正常工作并且不会给系统增加任何漏洞,那么这就是正确的方式。再次感谢! - Draco
嘿,我也在疯狂地谷歌寻找答案。似乎使用“inverseBy”可以正确持久化,但使用“mappedBy”则不行。 - Adambean
@Adambean;根据您的评论,您遇到的问题与 OP 的问题相同,请查看此答案是否有帮助。如果不行,请发布一个完整的问题,并提供您的问题细节。 - Scaramouche
最终我不需要进行任何控制器调整@Scaramouche。因为它是一个使用EntityType的托管表单,对于“mappedBy”方面在表单上使用"by_reference"=false就足够了。-我只是没有注意到"by_reference"对于EntityType也是可用的。(我以为它只适用于CollectionTypeChoiceType)。 - Adambean
@Scaramouche,看起来"by_reference"EntityType中已经不再起作用了。尽管它继承自具有此功能的ChoiceType,但我可以确认,在Symfony 3.4.22上为“mappedBy”侧表单设置"by_reference"false不再起作用。(我在两个不同的项目上都试过了。) - Adambean

0

我之前也遇到了同样的问题,但我采用了不同的解决方法。

在控制器中更改代码并不是最好的做法。在我的情况下,我有一个通用控制器来处理所有的CRUD操作,因此我不能在其中放置特定的代码。

最好的方法是在你的PetType中添加一个监听器,像这样:

    // PetType.php
    $builder->add('customer', 'entity', 
          array(
            'class' => 'AppBundle:Customer',
            'property' => 'firstname',
            'empty_value' => 'Choose owner',
            'multiple' => true
          ))
            ->addEventListener( FormEvents::SUBMIT, function( FormEvent $event ) {
                /** @var Pet $pet */
                $pet = $event->getData();
                foreach ( $pet->getCustomers() as $customer ) {
                    $customer->addPet( $pet );
                }
            } );

这样你就可以将映射逻辑保持在同一个地方。


0
在我的情况下,涉及到服务<->项目的场景,其中服务具有"inversedBy"属性,而项目具有"mappedBy"属性。因此,在我的项目控制器的编辑操作中,我必须这样做,以便在编辑项目时,您选中的服务将被持久化。
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,
    ]);
}

这适用于在“mappedBy”一侧添加和删除多对多实体,因此EntityType输入应按预期工作。

这里发生的是,我们首先构建一个“原始”集合,其中包含已链接到此项目的所有服务实体。然后当表单保存时,我们确保:

  • 首先,任何未选中的服务(即原始集合中但不在项目对象中的服务)都将从其内部集合中删除该项目,然后持久化。
  • 其次,任何新选中的服务都会将项目添加到其内部集合中,然后持久化。

重要提示:这取决于您实体的addService()addProject()方法分别检查彼此的集合是否不包含重复项。如果您不这样做,您将遇到有关重复记录插入的SQL级错误。

在服务实体中,我有:

/**
 * 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;
}

在项目实体中,我有以下内容:

/**
 * 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;
}

你也可以在控制器中进行检查,但如果可能的话,让模型自己验证这一点是有意义的,因为如果来自任何来源的重复数据,模型将会遇到问题。

最后,在您的控制器的创建操作中,很可能需要在$em->persist($project)之前加入这个代码段。(由于还不存在原始集合,您不需要使用“original”集合。)

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接