Search code examples
phpformssymfonyformbuilder

Saving multiple embedded forms using Symfony's Formbuilder, multiple entrees work fine 1 level deep, goes wrong 2 levels deep. (1-to-many),


The problem I have is regarding Symfony's (3) formbuilder. I have 3 entities for which I created 3 FormTypes. See below; actual question below the code.

I have the following entities:

DocumentBaldoc 1 -> * DocumentBaldocConnectionPoint 1 -> * DocumentBaldocAccount

I try to achieve the following. I want to create a form for a DocumentBaldoc, I want to create multiple forms for DocumentBaldocConnectionPoints and I want to create multiple forms for DocumentBaldocConnectionPointAccounts. When saved these need to be stored in the database with the relation (fk).

I have the following class for DocumentBaldoc (Form)

namespace FooBundle\Form\Documents\Baldoc;

use FooBundle\Entity\Documents\Baldoc\DocumentBaldoc;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class DocumentBaldocType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('document_release')
            ->add('identification')
            ->add('version')
            ->add('type')
            ->add('creationDateTime')
            ->add('validityPeriod')
            ->add('contractReference')
            ->add('contractType')
            ->add('issuerMarketParticipantIdentification')
            ->add('issuerMarketParticipantIdentificationCodingScheme')
            ->add('issuerMarketParticipantMarketRoleCode')
            ->add('recipientMarketParticipantIdentification')
            ->add('recipientMarketParticipantIdentificationCodingScheme')
            ->add('recipientMarketParticipantMarketRoleCode')
            ->add('applicationContext')
            ->add('applicationContextCodingScheme')
            ->add('connectionPoints', CollectionType::class, [
                'entry_type'   => DocumentBaldocConnectionPointType::class,
                'allow_add'    => true,
                'by_reference' => false,
            ])
            ->add('save', SubmitType::class);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => DocumentBaldoc::class
        ));
    }
}

I have the following class for DocumentBaldocConnectionPoint (Form)

namespace FooBundle\Form\Documents\Baldoc;

use FooBundle\Entity\Documents\Baldoc\DocumentBaldocConnectionPoint;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DocumentBaldocConnectionPointType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('identification')
            ->add('identificationCodingScheme')
            ->add('measureUnitCode')
            ->add('accounts', CollectionType::class, [
                'entry_type' => DocumentBaldocAccountType::class,
                'allow_add' => true,
                'by_reference' => false,
            ]);
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => DocumentBaldocConnectionPoint::class
        ));
    }
}

and I have the following class for DocumentBaldocAccount (Form)

namespace FooBundle\Form\Documents\Baldoc;

use FooBundle\Entity\Documents\Baldoc\DocumentBaldocAccount;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DocumentBaldocAccountType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('identification')
            ->add('identificationCodingScheme')
            ->add('accountTso')
            ->add('accountTsoCodingScheme');
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => DocumentBaldocAccount::class
        ));
    }

}

I use Symfony's formbuilder to generate the form for the DocumentBaldoc entity. By adding a CollectionType with the DocumentBaldocConnectionPoint as entry_type gives me the ability to make uses of the prototype variable in the template.

{% block DOCUMENT %}
    {{ form_start(form) }}
    <ul class="connectionPoints"
        data-prototype="{{ form_widget(form.connectionPoints.vars.prototype)|e('html_attr') }}">
        {{ form_widget(form) }}
    </ul>
    {{ form_end(form) }}
{% endblock %}

After this I can use some Javascript to create an endless stream of DocumentBaldocConnectionPoint forms and when I press save those (multiple!!) DocumentBaldocConnectionPoints are stored in the DB with a FK to the parent DocumentBaldoc.

So far this works perfectly well, the problem though comes when I want to do the same for DocumentBaldocConnectionPointAccount. The DocumentBaldocConnectionPoint form does not exists on pageload so I can not access the prototype attribute directly, I solved this by creating some Javascript logic which fires when a DocumentBaldocConenctionPoint form is created. It then follows the same logic as its parent. The problem I encounter is that I can not save multiple DocumentBaldocConenctionPointAccounts, it always saves the last one which makes me think that somewhere in the process it's not saved as array, or somewhere the array gets overwritten by the last entry. I've been tinkering on it for a couple of days but can not find any differences in logic (formtypes or entities, the relation definitions follow the same structure) which makes me think that I am not using this as it is supposed to be used.

These are the Annotation relation definitions for both Entities, both $connectionPoints and $accounts are instantiated as ArrayCollections in the constructor of the entity.

// DocumentBalcon
@ORM\OneToMany(targetEntity="DocumentBaldocConnectionPoint", mappedBy="document", cascade={"persist"})
private $connectionPoints;

// DocumentBalconConnectionPoint
@ORM\OneToMany(targetEntity="DocumentBaldocAccount", mappedBy="connectionPoint", cascade={"persist"})
private $accounts;

The only thing that stands out is that the prototype of the DocumentBaldocConnectionPoint form is rendered with the __name__ syntax which needs to be replaced with an unique identifier and that the same thing does not happen when I grab the DocumentBaldocConnectionPointAccount prototype, I solved this by creating an own identifier to guarantee uniqueness.

I've read he Symfony documentation regarding formbuilder and creating these kind of embedded forms but the documentation only states that what I am trying to do is possible, without examples. I have no clue if what I am doing is a proper way of handling these kind of embedded forms or that I am missing some steps in the process.

Any information or help regarding this problem is more than welcome!


Solution

  • Problem solved.

    The first child form DocumentBaldocConnectionPoint also contained the attribute prototype which contains the form for DocumentBaldocConnectionPointAccount. When changing the __name__ value in the DocumentBaldocConnectionPoint the values for DocumentBaldocConnectionPointAccount als got changed unintentionally which resulted in the unexpected behaviour that every DocumentBaldocConnectionPointAccount form got placed in the array on the key with value of 'index', which only changed per DocumentConnectionPoint.

    The solution I used is as following:

    in the DocumentBaldocConnectionPointType class in the function buildForm I added the prototype key with value "accounts" which gives me the possibility to fetch the prototype form by name reference.

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('identification')
            ->add('identificationCodingScheme')
            ->add('measureUnitCode')
            ->add('accounts', CollectionType::class, [
                'entry_type' => DocumentBaldocAccountType::class,
                'allow_add' => true,
                'by_reference' => false,
                'prototype' => 'accounts'
            ]);
    }
    

    In the twig template I added a div as following:

    <div id="prototypes"
         data-prototype-account="{{ form_widget(form.connectionPoints.vars.prototype.children['accounts'].vars.prototype) | e }}"
         data-prototype-connection-point="{{ form_widget(form.connectionPoints.vars.prototype) | e }}">
    </div>
    

    This results in an div where the prototype forms are stored and gives me the possibility to easily query and use them when needed.