Symfony 表单不保存具有 ManyToMany 关系的实体

Symfony form not saving entity with ManyToMany relation

我在通过多对多关系的形式保存实体槽时遇到问题。

我无法保存关系 "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'));
} 

在 Symfony2 中,带有 属性 和 inversedBy 原则注释的实体应该编辑由多对多关系创建的额外 TABLE。这就是为什么当您创建一个客户时,它会在额外 table 中插入相应的行,从而保存相应的宠物。

如果你想让同样的行为反过来发生,我建议:

//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 结构,这样您就可以在表单中为 select 的每个客户强行添加相同的宠物,因此获得所需的行为。

看,这很可能不是正确的方法或正确的方法,但它是一种方法并且有效。希望对你有帮助。

在我的服务 <-> 项目场景中,服务有 "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 输入应该按预期工作。

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

  • 首先,任何未经检查的服务(那些在原始集合中但不在项目对象中的服务)都将项目从其内部集合中删除,然后保留。
  • 其次,任何新检查的服务都将项目添加到它们的内部集合中,然后持久化。

重要:这取决于您实体的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" 集合,因为 none 还存在。)

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

我刚遇到同样的问题,但我用不同的方式解决了它。

更改控制器中的代码并不是更好的方法。 在我的例子中,我有一个 GenericController 来处理我所有的 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 );
                }
            } );

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