[Solved] Symfony 4. ManyToMany association with attribute. Custom form to insert attribute value, how do I?


It seems I did find a way, indeed it is pretty straightforward Synfony 4 code.
Hope it could be useful to somebody.

(note: I used php bin/console make:entity/crud/form to write the needed scripts. Below how I needed to modify the code I got from make)

So, say I have Alpha and Beta entities.
I wish to have a form to create a new Alpha object, to associate to it one or more Beta objects, and to fill in a Cost value for each Alpha-Beta association. I want a edit form too.

First I create a new AlphaBeta entity, whose fields are:

 /**
 * @ORM\ManyToOne(targetEntity="App\Entity\Alpha", inversedBy="alphabetas", cascade={"persist"})
 * @ORM\JoinColumn(nullable=false)
 */
private $alpha;

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\Beta", inversedBy="betaalphas", cascade={"persist"})
 * @ORM\JoinColumn(nullable=false)
 */
private $beta;

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

Within class Alpha, I need

    /**
 * @ORM\OneToMany(targetEntity="App\Entity\AlphaBeta", mappedBy="alpha", orphanRemoval=true, cascade={"persist"})
 */
private $alphabetas;

with usual ‘getAlphaBeta, addAlphaBeta and removeAlphaBeta methods. (similarly for Beta)

I create usual CRUD controllers for the new AlphaBeta entity. To have an AlphaBeta Form which could be used as a subform too, I define

class `AlphaBetaEmbeddedForm`  extends AbstractType {

      public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder ->add('Beta', EntityType::class, array(
                    'class' => Beta::class,
                    'multiple' => false,
                    'expanded' => true,
                    'choice_label' => 'betaTitle' ))
             ->add('cost', TextType::class);
   if(empty($options['remove_alpha_field'])) {
   $builder->add('Alpha', EntityType::class, array(
                   'class' => Alpha::class,
                   'multiple' => false,
                   'expanded' => true,
                   'choice_label' => 'alphaTitle'
    ));}}

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

remove_alpha_field is the trick that let me use the form above as a subform within a form to create an Alpha object:

class AlphaType extends AbstractType {

     public function buildForm(FormBuilderInterface $builder, array $options) {

     $builder
        ->add('alphaTitle')       
        ->add('AlphaBetas', CollectionType::class, array(
           'entry_type'  => AlphaBetaEmbeddedForm ::class,
           'entry_options' => ['label' => true, 'remove_alpha_field' => true],
           'allow_add' => true,
           'label' => 'Betas',
           'by_reference' => false
    ));}

To render the subforms within the main form, I need some JS, as suggested here, to be inserted within the new.html.twig and edit.html.twig templates for Alpha:

{% block javascripts %} <script type="text/javascript"> 
     jQuery(document).ready(function () {
        $("#add-another-collection-widget").click(function(e){ 
          var list = jQuery(jQuery(this).attr('data-list-selector'));
          var counter = list.data('widget-counter') | list.children().length;
          var newWidget = list.attr('data-prototype');
          newWidget  = newWidget.replace(/__name__/g, counter); 
          counter++;
          list.data('widget-counter', counter);
          var newElem = jQuery(list.attr('data-widget-tags')).html(newWidget);
          newElem.appendTo(list);
    });});</script>{% endblock %}

To apply the latter, it seems you need to write each single row in the main form template _form.html.twig:

{{ form_start(form) }}
<div class="my-custom-class-for-errors">
    {{ form_errors(form) }}
</div>
     <div id="alphaTitle">
        {{ form_row(form.alphaTitle) }}
    </div>

{% if  form.AlphaBetas %} 
     <b>Betas</b> </br></br>
 {% for Beta in form.AlphaBetas %}
   {% for BetaField in Beta %}
     {{ form_row(BetaField) }}
   {% endfor %} 
       {% endfor %}
 {% endif %}

<div id="AlphaBeta-fields-list"
    data-prototype="{{ form_widget(form.AlphaBetas.vars.prototype)|e }}"
    data-widget-tags="{{ '<p></p>' |e }}"
    data-widget-counter="{{ form.children|length }}">
{% for AlphaBetaField in form.AlphaBetas.vars.prototype.children %}
{{ form_row(AlphaBetaField) }}
{% endfor %}
</div> 

<button type="button" id="add-another-collection-widget" data-list-selector="#AlphaBeta-fields-list">Insert Beta</button>
</br>
<button class="btn">{{ button_label|default('Save') }}</button>
{{ form_end(form) }}

That’s all.

Note: within the main form to edit an Alpha object, I would like to insert a delete buttons for each Beta associated object. As much as I can see, it is not possible, since it would mean to insert a html form within an html form. Since I may delete a AlphaBeta association by the corresponding AlphaBeta delete action, it is not a big deal.

If you see how I can improve my code, you are welcome to advise me.

1

solved Symfony 4. ManyToMany association with attribute. Custom form to insert attribute value, how do I?