Symfony: как сохранить новые объекты с отношением ManytoMany во встроенной форме

Вариант использования

TLDR;

У меня есть две сущности с отношением ManytoMany. Я хочу сохранить два новых объекта одновременно с одной единственной формой. Для этого я создал два типа FromType, один из которых встраивает другой.

Еще немного...

Цель состоит в том, чтобы предоставить пользователям форму для запроса события. Сущность события состоит из таких свойств, как время начала, время окончания, например. которые являются простыми свойствами события, а также местоположения (сущность местоположения с отношением OneToMany, одно событие имеет одно местоположение, одно местоположение может иметь много событий) и контактное лицо (сущность контакта с отношением ManyToMany, одно событие может иметь несколько контактов, одно Контакт может иметь несколько событий). Для конкретной рассматриваемой формы пользователю достаточно (и это осознанный выбор), чтобы указать только один контакт, поскольку это необходимый минимум и достаточно для начала.

Для создания многократно используемых форм существуют две простые формы с LocationFormType и ContactFormType и более сложная форма EventFormType. Более сложный, поскольку он включает в себя как LocationFormType, так и ContactFormType для создания сущности Event, так сказать, «за один раз». Когда я создаю EventFormType с опцией A (см. код ниже), форма отображается правильно и так, как она предназначена. Все выглядит нормально, пока форма не отправлена. Потом начинается проблема...

Проблема

В $form->handleRequest() FormSystem выдает ошибку, потому что встроенная форма не предоставляет Traversable для связанного объекта.

The property "contact" in class "App\Entity\Event" can be defined with the methods "addContact()", "removeContact()" but the new value must be an array or an instance of \Traversable.

Очевидно, что встроенный FormType предоставляет один объект, в то время как для свойства отношения требуется Collection. Когда я использую CollectionType для встраивания (вариант B, см. код ниже), форма больше не отображается, поскольку CollectionType, по-видимому, ожидает, что сущности уже присутствуют. Но я хочу создать новый. Так что нет объекта, который я мог бы передать.

Мой код

#[ORM\Entity(repositoryClass: EventRepository::class)]
class Event
{
    ...

    #[ORM\ManyToMany(targetEntity: Contact::class, inversedBy: 'events')]
    ...
    private Collection $contact;
    
    ...

    public function __construct()
    {
        $this->contact = new ArrayCollection();
    }
    
    ...

    /**
     * @return Collection<int, Contact>
     */
    public function getContact(): Collection
    {
        return $this->contact;
    }

    public function addContact(Contact $contact): self
    {
        if (!$this->contact->contains($contact)) {
            $this->contact->add($contact);
        }

        return $this;
    }

    public function removeContact(Contact $contact): self
    {
        $this->contact->removeElement($contact);

        return $this;
    }
    
    ...
}
#[ORM\Entity(repositoryClass: ContactRepository::class)]
class Contact
{
    ...

    #[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'contact')]
    private Collection $events;

    public function __construct()
    {
        $this->events = new ArrayCollection();
    }
    
    ...

    /**
     * @return Collection<int, Event>
     */
    public function getEvents(): Collection
    {
        return $this->events;
    }

    public function addEvent(Event $event): self
    {
        if (!$this->events->contains($event)) {
            $this->events->add($event);
            $event->addContact($this);
        }

        return $this;
    }

    public function removeEvent(Event $event): self
    {
        if ($this->events->removeElement($event)) {
            $event->removeContact($this);
        }

        return $this;
    }
}
class EventFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ...
            // option A: embedding related FormType directly
            ->add('contact', ContactFormType::class, [
                ...
            ])
            // option B: embedding with CollectionType
            ->add('contact', CollectionType::class, [
                'entry_type' => ContactFormType::class
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Event::class,
        ]);
    }
}
class ContactFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add(
            ... // here I only add the fields for Contact entity, no special config
            )
        ;
    }

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

Неудачные решения

'allow_add' => true с прототипом

Я нашел решения, предлагающие установить «allow_add» => true для CollectionType и отобразить форму в Twig с помощью ..vars.prototype. Это хакерское решение (я так думаю) в моем случае использования. Я не хочу добавлять несколько форм. А без 'allow_add' в CollectionType нет прототипа, поэтому данные для рендеринга формы отсутствуют.

предоставить пустой объект для CollectionType

Чтобы опустить 'allow_add' => true, но иметь объект для правильной визуализации формы, я попытался передать пустой экземпляр Contact в свой контроллер.

$eventForm = $this->createForm(EventFormType::class);
if (!$eventForm->get('contact')) $eventForm->get('contact')->setData(array(new Contact()));

Это работает при начальной загрузке, но создает проблемы при отправке формы. Может быть, я мог бы заставить это работать, но моя интуиция снова дает мне «хакерские вибрации».

Заключение

На самом деле, я думаю, что здесь упущен какой-то базовый момент, поскольку я думаю, что мой вариант использования не является чем-то резким или необычным. Может ли кто-нибудь дать мне подсказку, где я ошибаюсь в своем подходе?

P.S.: Я не уверен, обсуждалась ли моя проблема (без решения) на Github.

🤔 А знаете ли вы, что...
Symfony обладает активным сообществом разработчиков и подробной официальной документацией.


60
1

Ответ:

Решено

Итак, я решил проблему. Для этого сценария необходимо использовать Data Mappers.

Можно сопоставить отдельные поля формы с помощью опций «getter» и «setter» (Документы). В данном конкретном случае достаточно setter-option:

        ->add('contact', ContactFormType::class, [
            ...
            'setter' => function (Event &$event, Contact $contact, FormInterface $form) {
                $event->addContact($contact);
            }
        ])

Метод addContact() предоставляется интерфейсом командной строки Symfonys при создании отношений ManyToMany, но также может быть добавлен вручную (Документы, SymfonyCast).