Symfony5:自引用多对多的形式

Symfony5: form for self-referencing many-to-many

在 Symfony5 应用程序中,我们如何利用表单组件正确设置自引用、多对多关系?

注意:这纯粹是形式方面的事情,Doctrine已经很高兴了。

假设我们有一个“人”实体,这些人实例托管客人,他们也是人:

#[Entity(repositoryClass: PersonRepository::class)]
class Person
{
    #[Id]
    #[GeneratedValue]
    #[Column(type: Types::INTEGER)]
    private int $id;

    #[Column(type: Types::STRING, length: 255)]
    private string $firstName;

    #[Column(type: Types::STRING, length: 255)]
    private string $lastName;

    /** @var Collection<Person> */
    #[ManyToMany(targetEntity: self::class, inversedBy: 'guestsHosted')]
    private Collection $hosts;

    /** @var Collection<Person> */
    #[ManyToMany(targetEntity: self::class, mappedBy: 'hosts')]
    private Collection $guestsHosted;

    public function __construct()
    {
        $this->fellowshipHosts = new ArrayCollection();
        $this->fellowsHosted = new ArrayCollection();
    }

    // getters/setters ommitted for brevity


    public function addHost(self $host): self
    {
    }

    public function removeHost(self $host): self
    {
    }

    public function addGuestsHosted(self $guestHosted): self
    {
    }

    public function removeGuestsHosted(self $guestsHosted): self
    {
    }

我想要实现的是一个包含 addable/removable 行集合的表单,用户可以在其中添加一行,select 现有的其他人作为他们的主持人,添加另一行,select 另一个人,依此类推。一个简单的模型:

我如何使用 Symfony 的表单组件实现这一点?由于相同表单类型的递归,我现在在那里的内容不起作用:

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder           
            ->add('firstName', null, [])
            ->add('lastName', null, [])            
            ->add('hosts', CollectionType::class, [
                'entry_type' => self::class,
                'entry_options' => ['label' => false],
                'required' => false,
                'allow_delete' => true,
                'allow_add' => true,
                'by_reference' => false,
            ]);
    }

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

注意:我过去构建过类似的 UI,并且知道涉及 form.vars.prototype 的小舞蹈并将 JS 连接到它。我迷路的地方是如何在表单级别映射此关联,以便我获得所有人员的下拉列表,并将其映射到实体的 $hosts 属性。

@Bossman 的评论非常正确,但以防万一你想用单一的表单类型来做,我会做这样的事情:

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder           
            ->add('firstName', null, [])
            ->add('lastName', null, []);

        if (!$options['is_host']) {            
            ->add('hosts', CollectionType::class, [
                'entry_type' => self::class,
                'entry_options' => ['label' => false, 'is_host' => true],
                'required' => false,
                'allow_delete' => true,
                'allow_add' => true,
                'by_reference' => false,
            ]);
        }
    }

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

基本上我在这里做的是通过明确地将 'is_host' 传递给表单选项来防止递归,这表明这是否是一个子实体,然后我正在构建 'hosts' 表单仅适用于根实体的字段。

如果您想在现有主机中 select,请使用 EntityType:

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        // You'll be able to use this later in the 'query_builder' option
        $personId = $options['person_id'];

        $builder
            ->add('firstName', null, [])
            ->add('lastName', null, [])
            ->add('hosts', CollectionType::class, [
                'entry_type' => EntityType::class,
                'entry_options' => ['label' => false, 'class' => Person::class],
                'required' => false,
                'allow_delete' => true,
                'allow_add' => true,
                'by_reference' => false,
            ])
        }
    }

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

        $resolver->setAllowedTypes('person_id', ['int', 'null']);
    }
}

记住以下几点很重要:

  1. 您必须将当前人员 ID 作为表单的 person_id 选项传递(或对于新条目为 null)。如果需要,您也可以从 $builder 变量中检索此值。

  2. 根据您的需要修改 EntityType 选项中的 'query_builder',例如不要从主机列表等中排除当前(root)人

  3. 这种方法 可能 会导致多个数据库查询(每个集合项 1 个),但我现在不确定。如果是这样,你应该自己想办法解决这个问题。