Baptiste Donaux
Baptiste Donaux

Reputation: 1310

Form with a collection same entity type

I would want a form for my entity « X ». This entity own a relationship OneToMany with many entities of type « X ». It's a relationship parent <-> children.

When I create my form simply, it works.

class XType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add("name",     "text",       array("label" => "Nom"))
            ->add("children", "collection", array(
                "type"         => new XType(),
                "by_reference" => false));
    }
}

Then, I would like add easily new entity in my collection with the option « allow_add », and used the prototype to add in javascript. This is my form with the « allow_add » option.

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add("name",     "text",       array("label" => "Nom"))
        ->add("children", "collection", array(
            "type"         => new XType(),
            "allow_add"    => true,
            "by_reference" => false));
}

When I execute with or without called the prototype, I have an webserver error. It's XDebug which kick my request because the recursive call is too big. There are cascade call.

What's the best solution to resolve my problem ?

Upvotes: 0

Views: 3193

Answers (2)

Razvan
Razvan

Reputation: 2596

I ran into the same issue (my web server crashes, as well, due to too many recursive calls). My quick workaround was to simply create a dummy (cloned) type that doesn't contain the recursive field (this works for me since I was interested only into the 1st level children).

So, if this scenario is applicable to you, as well, you could change your code as follows:

class XType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add("name",     "text",       array("label" => "Nom"))
            ->add("children", "collection", array(
                "type"         => new XTypeWithoutChildren(),
                "by_reference" => false));
    }
}

class XTypeWithoutChildren extends XType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add("name",     "text",       array("label" => "Nom"));
    }
}

Upvotes: 1

tomazahlin
tomazahlin

Reputation: 2167

I can't really say where your problem is, but should be somewhere around javascript and the collection prototype. Also, when you allow items to be added, you usually allow to allow_delete too.

Take a look at my example:

$builder->add('book',   'collection',   array(
                                                'type' => new BookType(),
                                                 'allow_add' => true,
                                                 'allow_delete' => true,
                                                 'prototype_name' => '__prototype__',
                                                 'by_reference' => false,
                                                 'error_bubbling' => false
                                               );

To the template where you render form, include this:

{% block javascript %}
    {{ parent() }}
    {% javascripts
    '@ProjectSomeBundle/Resources/public/js/form/collection.js'
    %}
    <script type="text/javascript" src="{{ asset_url }}"></script>
    {% endjavascripts %}
{% endblock %}

And the collection.js file holds this:

$(function($) {
    $addButton = $('button.add');
    $collection = $addButton.parent().children().first();

    $addButton.click(function () {
        var prototype = $($collection.data('prototype').replace(/__prototype__/g, function() { return (new Date()).getTime(); }));
        prototype.appendTo($collection.children('ul'));
        });

    $('body').on('click', 'button.remove', function () {
        $(this).parent().remove();
    });
});

Also, you should be using those customised collection widgets, which you pass to twig. Nothing fancy, it will be rendered as unordered list with items, to make items easily accessible.

config.yml:

twig:
    form:
        resources:
            - 'ProjectSomeBundle:Form:fields.html.twig'

fields.html.twig:

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

{% block collection_widget %}
    {% import  "ProjectSomeBundle:Macro:macro.html.twig" as macro %}
    {% spaceless %}
        <div class="collection">
            {% if prototype is defined %}
                {% set attr = attr|merge({'data-prototype': block('prototype_widget') }) %}
            {% endif %}
            <div {{ block('widget_container_attributes') }}>
                <ul>
                    {% for row in form %}
                        {{ macro.collection_item_widget(row) }}
                    {% endfor %}
                </ul>
                {{ form_rest(form) }}
            </div>
            <div class="clear"></div>
            <button type="button" class="add">{% trans %}Add{% endtrans %}</button>
        </div>
        <div class="clear"></div>
    {% endspaceless %}
{% endblock collection_widget %}

{% block prototype_widget %}
    {% spaceless %}
        {{ macro.collection_item_widget(prototype) }}
    {% endspaceless %}
{% endblock prototype_widget %}

You can notice it uses macro, so here it is:

macro.html.twig

{% macro collection_item_widget(fields) %}
    <li>
        {% set fieldNum = 1 %}
        {% for field in fields %}
            <div class="field_{{ fieldNum }}">
                {{ form_label(field) }}
                {{ form_errors(field) }}
                {{ form_widget(field) }}
            </div>
            {% set fieldNum = fieldNum + 1 %}
        {% endfor %}
        <button type="button" class="remove">{% trans %}Delete{% endtrans %}</button>
        <div class="clear"></div>
    </li>
{% endmacro %}

This is a full example, I hope you find it useful and works for you.

Upvotes: 2

Related Questions