表单中的嵌套集合

I tried to follow this answer to handle nested collections in forms.

I have an Application's Form with a collection of LienAppliServ's Form :

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('servLiens', 'collection', array(
            'label' => ' ',
            'type' => new LienAppliServType(),
            'allow_add' => true,
            'allow_delete' => true,
            'by_reference' =>false,
            'prototype' => true,
        ))
    //...

In my LienAppliServ's Form, I have another collection of PortLienAppliServ's Form :

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('ports', 'collection', array(
            'type' => new PortLienAppliServType(),
            'allow_add' => true,
            'allow_delete' => true,
            'prototype' => true,
            'by_reference' =>false
        ))
    //...

And the form of PortLienAppliServ is :

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('numPort')
        ->add('type')
    ;
}

Now, I'd like to handle add/delete for each collection... As I said, I tried to follow this answer

In order to do that, I tried :

{% block body -%}
    {{ form_start(form) }}

            <ul id="col-servliens" data-prototype="{{ form_widget(form.servLiens.vars.prototype)|e }}">
                {# iterate over each existing tag and render its only field: name #}
                {% for servLiens in form.servLiens %}
                    <li>{{ form_row(servLiens) }} </li>
                    <ul id="col-ports" data-prototype="{{ form_widget(ports.vars.prototype)|e }}">
                    {%for ports in servLiens.ports %}
                        <li>{{ form_row(ports) }}</li>
                    {% endfor %}

                {% endfor %}
            </ul>
        {{ form_end(form) }}

{% endblock %}

{% block app_js %}

    //Same as the other question
    <script>
        function FormCollection(div_id)
        {
            // keep reference to self in all child functions
            var self=this;

            self.construct = function () {
                // set some shortcuts
                self.div = $('#'+div_id);
                self.div.data('index', self.div.find(':input').length);

                // add delete link to existing children
                self.div.children().each(function() {
                    self.addDeleteLink($(this));
                });

                // add click event to the Add new button
                self.div.next().on('click', function(e) {
                    // prevent the link from creating a "#" on the URL
                    e.preventDefault();

                    // add a new tag form (see next code block)
                    self.addNew();
                });
            };

            /**
             * onClick event handler -- adds a new input
             */
            self.addNew = function () {
                // Get the data-prototype explained earlier
                var prototype = self.div.data('prototype');

                // get the new index
                var index = self.div.data('index');

                // Replace '__name__' in the prototype's HTML to
                // instead be a number based on how many items we have
                var newForm = prototype.replace(/__name__/g, index);

                // increase the index with one for the next item
                self.div.data('index', index + 1);

                // Display the form in the page in an li, before the "Add a tag" link li
                self.div.append($(newForm));

                // add a delete link to the new form
                self.addDeleteLink( $(self.div.children(':last-child')[0]) );

                // not a very nice intergration.. but when creating stuff that has help icons,
                // the popovers will not automatically be instantiated
                //initHelpPopovers();

                return $(newForm);
            };

            /**
             * add Delete icon after input
             * @param Element row
             */
            self.addDeleteLink = function (row) {
                var $removeFormA = $('<a href="#" class="btn btn-danger" tabindex="-1"><i class="entypo-trash"></i></a>');
                $(row).find('select').after($removeFormA);
                row.append($removeFormA);
                $removeFormA.on('click', function(e) {
                    // prevent the link from creating a "#" on the URL
                    e.preventDefault();

                    // remove the li for the tag form
                    row.remove();
                });
            };

            self.construct();
        }

    </script>


    <script>
        $(document).ready(function() {
            new FormCollection('col-servliens');
            new FormCollection('col-ports');
        });
    </script>

And I get

Variable "ports" does not exist.

I really need some help.. Collections are actually my nightmare... Thanks !

Looks like you use symfony 2.6 or older.

I would start by saying you should'nt do

"'type' => new PortLienAppliServType()"
but pass the form type name. Symfony will instantiate it and it could be the problem.

Everything is described in extend here: https://symfony.com/doc/2.6/reference/forms/types/collection.html#basic-usage

Honestly, Collections are not that difficult you just need to understand how it works then Symfony will manage everything for you.

Let me start with apologies, because my answer in the post you mention was clearly wrong :) At least the example code was incorrect. I wonder if the original poster ever found the right solution.

The error is easy enough to spot. In the Twig you use the variable ports, but how should Twig know where it comes from?

{% for servLiens in form.servLiens %}
<li>{{ form_row(servLiens) }} </li>
    <ul id="col-ports" data-prototype="{{ form_widget(ports.vars.prototype)|e }}">
        {%for ports in servLiens.ports %}
            <li>{{ form_row(ports) }}</li>
        {% endfor %}
{% endfor %}

Using {{ form_row(servLiens) }} will actually already create the collection classes, so the whole <ul> inside it is not needed. In fact Symfony is smart enough that you don't even need the first <ul>. It will find any child forms (ex. collections) and create all the HTML needed.

So the only thing you need to have in your code is:

{{ form_row(form.servLiens) }}

If you would look at the HTML generated from that, you will see the data-prototype and the children. It will automatically have an id attribute that you can use.

Problem is the javascript part. You are using a collection within a collection. The javascript FormCollection class (which I wrote) cannot handle this as simply as you've tried it. It will handle the top collection fine, but it will not automatically instantiate collections on load/create/delete. Remember that each servLiens collection has a collection of ports. Therefore already at loading you can't simply load the col-ports as you do now, but would need to load each port collection. Then you'd need to instantiate a new FormCollection on the port collection every time you add a servLiens row.

I don't have time to create your code for you, but I hope my explanation helps you to find a solution.