Symfony 表单集合 - 维护与主键的关联

Symfony form collection - maintain association with primary key

我有一个包含定义如下的集合的 Symfony 表单:

<?php declare(strict_types=1);

namespace App\Form;

use App\Entity\Documents;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DocumentsType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add(
            'documents',
            CollectionType::class,
            [
                'entry_type' => DocumentType::class,
                'by_reference' => false,
                'entry_options' => [
                    'label' => false,
                ],
                'allow_add' => true,
                'allow_delete' => true,
                'delete_empty' => true,
                'attr' => [
                    'class' => 'documents-collection',
                    'data-min-items' => 1,
                ],
                'required' => true,
            ]
        );

        parent::buildForm($builder, $options);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class' => Documents::class,
            ]
        );
    }
}

DocumentType 如下:

<?php declare(strict_types=1);

namespace App\Form;

use App\Entity\Document;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DocumentType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add(
                'description',
                TextType::class,
                [
                    'required' => true,
                    'attr' => [
                        'placeholder' => 'Document description, eg: Ticket, receipt, itinerary, map, etc…',
                    ],
                ]
            )
            ->add(
                'document',
                FileType::class,
                [
                    'mapped' => false,
                    'required' => true,
                ]
            );

        parent::buildForm($builder, $options);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'data_class' => Document::class,
            ]
        );
    }
}

文档实体是:

<?php declare(strict_types=1);

namespace App\Entity;

use App\Service\Uuid;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class Documents
{
    /**
    * @ORM\Column(type="uuid")
    * @ORM\GeneratedValue(strategy="UUID")
    * @ORM\Id
    */
    private $id;

    /**
    * @ORM\ManyToMany(
    *     targetEntity="Document",
    *     cascade={"persist", "remove"},
    *     orphanRemoval=true
    * )
    * @ORM\JoinTable(
    *     name="documents_document",
    *     joinColumns={
    *         @ORM\JoinColumn(name="documents_id", referencedColumnName="id"),
    *     },
    *     inverseJoinColumns={
    *         @ORM\JoinColumn(name="document_id", referencedColumnName="id", unique=true),
    *     }
    * )
    * @var Document[]
    */
    private $documents;


    public function __construct()
    {
        $this->id = Uuid::uuid4();

        $this->documents = new ArrayCollection();
    }


    /**
    * @return mixed
    */
    public function getId()
    {
        return $this->id;
    }

    /**
    * @return Collection
    */
    public function getDocuments(): Collection
    {
        return $this->documents;
    }

    /**
    * @param Document $document
    *
    * @return $this
    */
    public function addDocument(Document $document): Documents
    {
        if (!$this->documents->contains($document)) {
            $this->documents->add($document);
            $document->setDocuments($this);
        }

        return $this;
    }

    /**
    * @param Document $document
    *
    * @return bool
    */
    public function hasDocument(Document $document): bool
    {
        return $this->documents->contains($document);
    }

    /**
    * @param Document $document
    *
    * @return $this
    */
    public function removeDocument(Document $document): Documents
    {
        if ($this->documents->contains($document)) {
            $this->documents->removeElement($document);
        }

        return $this;
    }

    /**
    * @param Collection $documents
    *
    * @return $this
    */
    public function setDocuments(Collection $documents): Documents
    {
        $this->documents = $documents;

        return $this;
    }

    /**
    * @return $this
    */
    public function clearDocuments(): Documents
    {
        $this->documents = new ArrayCollection();

        return $this;
    }
}

文档实体是:

<?php declare(strict_types=1);

namespace App\Entity;

use App\Service\Uuid;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class Document
{
    /**
    * @var Uuid|string
    * @ORM\Column(type="uuid")
    * @ORM\GeneratedValue(strategy="UUID")
    * @ORM\Id
    */
    private $id;

    /**
    * @var Documents
    * @ORM\ManyToOne(targetEntity="Documents")
    */
    private $documents;

    /**
    * @var string
    * @ORM\Column(type="string", length=1024, nullable=false)
    */
    private $description;


    public function __construct()
    {
        $this->id = Uuid::uuid4();
    }


    /**
    * @return Uuid|string
    */
    public function getId()
    {
        return $this->id;
    }

    /**
    * @return Documents
    */
    public function getDocuments(): Documents
    {
        return $this->documents;
    }

    /**
    * @param Documents $documents
    *
    * @return $this
    */
    public function setDocuments(Documents $documents): Document
    {
        $this->documents = $documents;

        return $this;
    }

    /**
    * @return string
    */
    public function getDescription(): ?string
    {
        return $this->description;
    }

    /**
    * @param string $description
    *
    * @return $this
    */
    public function setDescription(string $description): Document
    {
        $this->description = $description;

        return $this;
    }
}

我在我的控制器中创建表单是这样的:

$repo = $entityManager->getRepository(Documents::class);
$documents = $repo->findOneBy(['id' => $id]);

$form = $this->formFactory->create(
    DocumentsType::class,
    $documents
);

当我将新的 Document 条目添加到呈现的表单中的集合然后保存表单时,它们会正确地保存到数据库并链接到 Documents 实体。

如果我删除集合中的最后一个条目,它会正确地从 $documents 集合中删除,然后从文档 table 中删除,因为不再有任何对它的引用。

但是,如果我删除集合中间的一个条目,Doctrine 会将剩余条目的数据保存在已删除的条目及其追随者之上,然后删除列表中的最后一个实体,更改所有实体的 ID实体的数量。

我正在使用 UUID 作为新文件名保存在 DocumentTypedocument 字段中上传的文件,因此从集合中删除条目时 ID 需要保持不变。我已经尝试将映射和未映射的 id 字段添加到集合中,但是未映射的字段被完全忽略,并且映射的字段将允许用户修改 id 列中的数据,因此不是 suitable在这里使用。

我需要做什么来修改这个表单来让 Doctrine 维护集合中的数据和它在数据库中代表的实体之间的联系?

所以在找到 this issue in the bug tracker of the repo having a similar behavior, the last linked issue 后将我指向阅读我的这一部分:

Do not change field names

Symfony use field names to order the collection, not the position of each elements on the dom. So by default, if you delete an element in the middle, all following elements will have their index decreased of 1 (field[3] will become field[2] and so on) and if you add some elements in the middle, all subsequent elements will see their index increase to leave the space for the new one.

With this implementation, you're sure to keep the right positions when clicking "move up" and "move down" for exmaple. But in some situations, you may not want to overwrite indexes, most probably to maintain Doctrine relationships.

Set the preserve_names option to true to never touch field names. But be aware that this option will disable allow_up, allow_down, drag_drop options and will enforce add_at_the_end to true.

Default value:

$('.collection').collection({
   preserve_names: false
});

来源:https://github.com/ninsuo/symfony-collection/blob/d5e6cbc7c7dc1f0509631c9bb6094fead0f6c8f0/README.md#options

因此解决方案应该是使用选项 preserve_names 设置为 true 来初始化集合,而不是默认值 false.

$('.collection').collection({
    preserve_names: true // this is our fix
});