Home > Software design >  Form with self-referencing data (Symfony 5)
Form with self-referencing data (Symfony 5)

Time:12-03

I'm doing an app with Symfony 5 and there is a problem i don't manage to find a solution, I have no idea.

I want to make a form of an entity "Person". A Person can add in his family other Person.

So in my entity I made a Many-To-Many self referencing to Person.

class Person
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=50)
     */
    private $name;

    /**
     * @ORM\Column(type="string", length=50)
     */
    private $firstname;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $birthdaydate;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $gender;

    /** 
     * @ManyToMany(targetEntity="Person")
     * @JoinTable(name="family",
     *      joinColumns={@JoinColumn(name="person__id", referencedColumnName="person__id")},
     *      inverseJoinColumns={@JoinColumn(name="family_id", referencedColumnName="person__id")}
     *      )
     */
    private $myFamily;

And now, I want to make a form in which I can add new Person, in a person. I did a CollectionType, like symfony said, but when i want to print it to the page, I get a timeout because of an infinite loop.

It's the "allow_add" which causes the problem.

And i need the prototype variable returned by "allow_add" to add new field in the front.

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class,  ['attr' => ['class' => 'form_textfield']])
            ->add('firstname')
            ->add('birthdayDate', TextType::class,  ['attr' => ['class' => 'form_datetime']])
            ->add('gender', GenderType::class)
            ->add('submit', SubmitType::class)
            ->add('myFamily', CollectionType::class, array('entry_type' => PersonType::class, 'mapped' => false, 'allow_add' => true, 'by_reference' => false, 'allow_delete' => true));
    }

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

Here is my form, but there is nothing interesting, I will add the js necessary when i will be resolve this problem.

{% extends 'base.html.twig' %}

{% block title %}Hello PersonController!
{% endblock %}

{% block body %}
    {{ form_start(form) }}
    {{ form_row(form.name) }}
    {{ form_row(form.firstname) }}
    {{ form_row(form.birthdayDate) }}
    {{ form_row(form.gender) }}
{{ form_row(form.myFamily) }}
    <button type="button" class="add_item_link" data-collection-holder-class="tags">Add a tag</but
    {{ form_end(form) }}
{% endblock %}

Thanks everyone in advance.

CodePudding user response:

There is an infinite loop because the myFamily property references a Person entity which itself references a myFamily property ...

To keep things simple, one way to manage the family of a person would be to create a separate Family entity. From the Person point of view, it seems more coherent to have a ManyToOne relationship with a family. After that, you can add the family of a Person by using the EntityType:class inside the PersonFormType.

Here is the documentation for EntityType : https://symfony.com/doc/current/reference/forms/types/entity.html

CodePudding user response:

The answer from Dylan Kas was good, just by adding a new form, it's good.

The Person Form

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class,  ['attr' => ['class' => 'form_textfield']])
            ->add('firstname')
            ->add('birthdayDate', TextType::class,  ['attr' => ['class' => 'form_datetime']])
            ->add('gender', GenderType::class)
            ->add('submit', SubmitType::class)
            ->add('myFamily', CollectionType::class, array('entry_type' => ChildType::class, 'by_reference' => false, 'allow_add' => true, 'allow_delete' => true));
    }

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

The child, referenced by myFamily :

class ChildType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class,  ['attr' => ['class' => 'form_textfield']])
            ->add('firstname')
            ->add('birthdayDate', TextType::class,  ['attr' => ['class' => 'form_datetime']])
            ->add('gender', GenderType::class)
            ->add('submit', SubmitType::class);
    }

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

And the view :

{% block body %}
    {{ form_start(form) }}
    {{ form_row(form.name) }}
    {{ form_row(form.firstname) }}
    {{ form_row(form.birthdayDate) }}
    {{ form_row(form.gender) }}
    <button type="button" class="add_item_link" data-collection-holder-class="myFamily">Add a tag</button>
    <ul class="myFamily" data-index="{{ form.myFamily|length > 0 ? form.myFamily|last.vars.name   1 : 0 }}" data-prototype="{{ form_widget(form.myFamily.vars.prototype)|e('html_attr') }}"></ul>
    {{ form_end(form) }}
{% endblock %}

With the js associated

const addFormToCollection = (e) => {
  const collectionHolder = document.querySelector(
    "."   e.currentTarget.dataset.collectionHolderClass
  );

  const item = document.createElement("li");

  item.innerHTML = collectionHolder.dataset.prototype.replace(
    /__name__/g,
    collectionHolder.dataset.index
  );

  collectionHolder.appendChild(item);

  collectionHolder.dataset.index  ;
};

document
  .querySelectorAll(".add_item_link")
  .forEach((btn) => btn.addEventListener("click", addFormToCollection));

It still need some work, maybe I can make the child form extending the person form. The front need some work too. But the next people facing this problem will have the solution here.

I am still asking myself how could I do if I've needed to have a form including itself the same form, including itself the same form etc... The form would be recursivable.

  • Related