Symfony 5 Many to Many with extra fields form builder

Viewed 657

I'm losing a lot of time on a (maybe) basic feature that everyone needs in his project.

The fact is that I'm stuck with form builder (a real daemon in this framework, I don't understand why it's so hard to use).

I got three entities :

Recette
Ingredient
RecettesIngredients

Both Recette and Ingredient do an One-To-Many with RecettesIngredients. It's a many-to-many with extra fields.

I've done a form builder RecetteType that does :

        ->add('recettesIngredients', CollectionType::class, [
            'entry_type' => RecettesIngredientsType::class,
            'allow_add' => true,
            'allow_delete' => true,
            'label' => 'XXX'
        ])

I've done a form builder (he works fine) RecettesIngredientsType that does :

        ->add('ingredients', EntityType::class, [
            // looks for choices from this entity
            'class' => Ingredient::class,
            'choice_label' => 'nom',
            // used to render a select box, check boxes or radios
            'multiple' => true,
            'expanded' => true
        ])

When I try to render it:

{{ form_row(form.recettesIngredients) }}

If I don't give an object to my form builder : I just got nothing to display but a blank page without error.

If I give an object to my form builder : I have an error:

"Unable to transform value for property path "ingredients": Expected a Doctrine\Common\Collections\Collection object."

If I remove the "multiple" option, I got a list of ingredients but without checkboxes to select them.

My question is simple :

How can I display a form in a Many-To-Many with extra field relation context properly?

1 Answers

I made your exemple following the symfony documentation. The Entities following your exemple:

/**
 * @ORM\Entity(repositoryClass=RecetteRepository::class)
 */
class Recette
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

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

    /**
     * @ORM\OneToMany(targetEntity=RecetteIngredient::class, mappedBy="recette")
     *
     */
    private $ingredients;
/**
 * @ORM\Entity(repositoryClass=IngredientRepository::class)
 */
class Ingredient
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

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

    /**
     * @ORM\OneToMany(targetEntity=RecetteIngredient::class, mappedBy="ingredient")
     */
    private $recette;
/**
 * @ORM\Entity(repositoryClass=RecetteIngredientRepository::class)
 */
class RecetteIngredient
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

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

    /**
     * @ORM\ManyToOne(targetEntity=Recette::class, inversedBy="ingredients")
     * @ORM\JoinColumn(nullable=false)
     */
    private $recette;

    /**
     * @ORM\ManyToOne(targetEntity=Ingredient::class, inversedBy="recette")
     * @ORM\JoinColumn(nullable=false)
     */
    private $ingredient;

My FormTypes are from your exemple.

I created two Ingredient in my DB

enter image description here

And made this controller code to test

 $recette = new Recette();
 $recette->setName("Crèpes");

 $recetteIngredient = new RecetteIngredient();
 $recetteIngredient->setQuantity(5);
 $recette->addIngredient($recetteIngredient);

 $form = $this->createForm(RecetteType::class, $recette);

 return $this->render('demo.html.twig',['form'=>$form->createView()]);

The thing that I changed is the render. Since the number of Ingredient is dynamic, Symfony (and php server's code) has no idea the number of related entities you want to add or delete and you can not just do a form_row() on the collection type.

Here's my code grabed from the doc (I didn't changed html attributes name, it's just a demo.

{{ form_start(form) }}
    {{ form_row(form.name) }}
    <ul id="email-fields-list"
        data-prototype="{{ form_widget(form.ingredients.vars.prototype)|e }}"
        data-widget-tags="{{ '<li></li>'|e }}"
        data-widget-counter="{{ form.ingredients|length }}">
        {% for ingredient in form.ingredients %}
            <li>
                {{ form_errors(ingredient) }}
                {{ form_widget(ingredient) }}
            </li>
        {% endfor %}
    </ul>

    <button type="button"
            class="add-another-collection-widget"
            data-list-selector="#email-fields-list">Add another ingredient</button>
    {{ form_end(form) }}

    <script
            src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
            integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
            crossorigin="anonymous"></script>
    <script>
        jQuery(document).ready(function () {
            jQuery('.add-another-collection-widget').click(function (e) {
                var list = jQuery(jQuery(this).attr('data-list-selector'));
                // Try to find the counter of the list or use the length of the list
                var counter = list.data('widget-counter') || list.children().length;

                // grab the prototype template
                var newWidget = list.attr('data-prototype');
                // replace the "__name__" used in the id and name of the prototype
                // with a number that's unique to your emails
                // end name attribute looks like name="contact[emails][2]"
                newWidget = newWidget.replace(/__name__/g, counter);
                // Increase the counter
                counter++;
                // And store it, the length cannot be used if deleting widgets is allowed
                list.data('widget-counter', counter);

                // create a new list element and add it to the list
                var newElem = jQuery(list.attr('data-widget-tags')).html(newWidget);
                newElem.appendTo(list);
            });
        });
    </script>

Here's the result

enter image description here

Related