Как встроить коллекцию форм

В этой статье вы узнаете, как создавать форму, в которую встроена коллекция многих других форм. Это может быть полезно, к примеру, если у вас был класс Task и вы хотели редактировать/создать/удалить много объектов Tag, относящихся к этому классу, прямо внутри той же формы.

Note

В этой статье предполагается, что в качестве хранилища БД вы используете Doctrine. Но если вы не используете Doctrine (а используете, например, Propel или просто коллекцию БД), то всё очень похоже. Есть только несколько частей этого туториала, которые действительно заботятся о "персистенции".

Если вы используете Doctrine, то вам нужно будет добавить метаданные Doctrine, включая ассоциацию ManyToMany, связывающую определение со свойством tags.

Для начала, представьте, что каждый Task принадлежит нескольким объектам Tag. Начните с создания простого класса Task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/Entity/Task.php
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;

class Task
{
    protected $description;

    protected $tags;

    public function __construct()
    {
        $this->tags = new ArrayCollection();
    }

    public function getDescription()
    {
        return $this->description;
    }

    public function setDescription($description)
    {
        $this->description = $description;
    }

    public function getTags()
    {
        return $this->tags;
    }
}

Note

ArrayCollection относится именно к Doctrine, и фактически это то же самое, что использовать array (но это должно быть ArrayCollection, если вы используете Doctrine).

Теперь, создайте класс Tag. Как вы видели выше, Task может иметь много объектов Tag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/Entity/Tag.php
namespace App\Entity;

class Tag
{
    private $name;

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }
}

Далее, создайте класс формы так, чтобы объект Tag мог быть изменён пользователем:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Form/Type/TagType.php
namespace App\Form\Type;

use App\Entity\Tag;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TagType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Tag::class,
        ));
    }
}

Теперь у вас есть достаточно, чтобы отобразить форму тега саму по себе. Но так как конечная цель - позволить тегам Task быть изменёнными прямо внутри самой формы задачи, создайте форму для класса Task.

Заметьте, что вы встраиваете коллекцию форм TagType, используя поле CollectionType:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Form/Type/TaskType.php
namespace App\Form\Type;

use App\Entity\Task;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('description');

        $builder->add('tags', CollectionType::class, array(
            'entry_type' => TagType::class,
            'entry_options' => array('label' => false),
        ));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Task::class,
        ));
    }
}

В вашем контроллере, вы создадите новую форму из TaskType:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/Controller/TaskController.php
namespace App\Controller;

use App\Entity\Task;
use App\Entity\Tag;
use App\Form\Type\TaskType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class TaskController extends Controller
{
    public function new(Request $request)
    {
        $task = new Task();

        // фиктивный код - он здесь просто, чтобы Task имел какие-то теги
        // иначе это не будет интересным примером
        $tag1 = new Tag();
        $tag1->setName('tag1');
        $task->getTags()->add($tag1);
        $tag2 = new Tag();
        $tag2->setName('tag2');
        $task->getTags()->add($tag2);
        // конец фиктивного кода

        $form = $this->createForm(TaskType::class, $task);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // ... возможно, обработайте как-то код, например, сохраите объекты Task и Tag
        }

        return $this->render('task/new.html.twig', array(
            'form' => $form->createView(),
        ));
    }
}

Соответствующий шаблон теперь может отображать как поле description для формы задачи, так и все формы TagType для любых тегов, которые уже связаны с этим Task. В контроллере выше, я добавил немного фиктивного кода, чтобы вы могли увидеть это в действии (так как Task при создании не имеет ни одного тега).

  • Twig
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {# templates/task/new.html.twig #}
    
    {# ... #}
    
    {{ form_start(form) }}
        {# отобразить единственное поле задачи: описание #}
        {{ form_row(form.description) }}
    
        <h3>Tags</h3>
        <ul class="tags">
            {# выполнить перебор каждого существующего тега и отобразить его единственное
               поле: имя #}
            {% for tag in form.tags %}
                <li>{{ form_row(tag.name) }}</li>
            {% endfor %}
        </ul>
    {{ form_end(form) }}
    
    {# ... #}
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- templates/task/new.html.php -->
    
    <!-- ... -->
    
    <?php echo $view['form']->start($form) ?>
        <!-- отобразить единственное поле задачи: описание -->
        <?php echo $view['form']->row($form['description']) ?>
    
        <h3>Tags</h3>
        <ul class="tags">
            <?php foreach ($form['tags'] as $tag): ?>
                <li><?php echo $view['form']->row($tag['name']) ?></li>
            <?php endforeach ?>
        </ul>
    <?php echo $view['form']->end($form) ?>
    
    <!-- ... -->
    

Когда пользователь отправляет форму, оправленные данные для поля tags используются для построения ArrayCollection объектов Tag, который потом устанавливается в поле tag экземпляра Task.

К коллекции tags можно получить доступ через $task->getTags() и её можно сохранить в БД или использовать так, как вам нужно.

До этих пор, всё работало отлично, но это не позволяет вам динамически добавлять новые теги или удалять уже существующие. Так что, несмотря на то, что редактирование существующих тегов будет отлично работать, вам пользователь на самом деле пока ещё не может добавлять новые теги.

Caution

В этой статье вы встраиваете только одну коллекцию, но у вас нет таких ограничений. Вы также можете встроить вложенную коллекцию на столько уровней ниже, насколько вам этого захочется. Но если в установке разработки вы используете Xdebug, то вы можете получить ошибку Достигнут максимальный уровень функционирования вложенности '100', прерывание!. Это из-за PHP-установки xdebug.max_nesting_level, которая по умолчанию равняется 100.

Эта директива ограничивает рекурсию до 100 вызовов, что может быть недостаточным для отображения формы в шаблоне, если вы отображаете сразу всю форму целиком (например, form_widget(form)). Чтобы исправить это, вы можете установить в этой директиве более высокое значение (либо через файл php.ini, либо через ini_set, например, в public/index.php), либо отбразить каждое поле формы вручную, используя form_row().

Разрешение "новых" тегов с помощью "прототипа"

Разршить пользователю динамически добавлять новые теги - означает, что вам понадобится использовать JavaScript. Ранее вы добавляли в вашу форму два тега в контроллере. Теперь, позвольте пользователю добавлять столько форм тегов, сколько ему нужно, прямо в браузере. Это будет достигнуто с помощью JavaScript.

Первое, что вам нужно сделать, это дать коллекции форм знать, что она будет получать неизвестное число тегов. До этого этапа вы добавили два тега и тип формы ожидает получать именно два, а иначе будет выдана ошибка: Эта форма не должна содержать дополнительных полей. Чтобы сделать это более гибким, добавьте опцию allow_add в ваше поле коллекции:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// src/Form/Type/TaskType.php

// ...
use Symfony\Component\Form\FormBuilderInterface;

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

    $builder->add('tags', CollectionType::class, array(
        'entry_type' => TagType::class,
        'entry_options' => array('label' => false),
        'allow_add' => true,
    ));
}

В дополнение к тому, чтобы заставить полю принимать любое количество отправленных объектов, allow_add также создаёт переменную "прототип", доступную вам. Этот "прототип" - это маленький "шаблон", содержащий весь HTML, чтобы иметь возможность отображать любые новые формы "тегов". Чтобы отобразить их, сделайте в вашем шаблоне следующее изменение:

  • Twig
    1
    2
    3
    <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
        ...
    </ul>
    
  • PHP
    1
    2
    3
    4
    5
    <ul class="tags" data-prototype="<?php
        echo $view->escape($view['form']->row($form['tags']->vars['prototype']))
    ?>">
        ...
    </ul>
    

Note

Если вы отображаете всю вашу подформу "теги" одновременно (например, form_row(form.tags)), тогда прототип будет автоматически доступен во внешнем div в качестве атрибута data-prototype, похоже на то, что вы видели выше.

Tip

form.tags.vars.prototype - это элемент формы, который выглядит и ведёт себя точно так же, как индивдиуальные элементы form_widget(tag) внутри вашего цикла for. Это означает, что вы можете вызвать form_widget(), form_row() или form_label(). Вы даже можете выбрать отображение только одного из его полей (например, поля name):

1
{{ form_widget(form.tags.vars.prototype.name)|e }}

На отображённой странице, результат будет выглядеть как-то так:

1
<ul class="tags" data-prototype="&lt;div&gt;&lt;label class=&quot; required&quot;&gt;__name__&lt;/label&gt;&lt;div id=&quot;task_tags___name__&quot;&gt;&lt;div&gt;&lt;label for=&quot;task_tags___name___name&quot; class=&quot; required&quot;&gt;Name&lt;/label&gt;&lt;input type=&quot;text&quot; id=&quot;task_tags___name___name&quot; name=&quot;task[tags][__name__][name]&quot; required=&quot;required&quot; maxlength=&quot;255&quot; /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;">

Целью этого раздела будет использование JavaScript для чтения этого атрибута и динамического добавления новых форм тегов, когда пользователь кликает по ссылке "Добавить тег". Чтобы всё упростить, этот пример использует jQuery и предполагает, что вы включили его где-то на вашей странице.

Добавьте тег script где-то на вашей страниче, чтобы вы могли начать писать JavaScript.

Для начала, добавьте ссылку внизу списка "тегов" через JavaScript. Потом, привяжите событие "клик" этой ссылки так, чтобы вы могли добавлять новую форму тега (addTagForm() будет показан дальше):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var $collectionHolder;

// установка ссылки "добавить тег"
var $addTagLink = $('<a href="#" class="add_tag_link">Add a tag</a>');
var $newLinkLi = $('<li></li>').append($addTagLink);

jQuery(document).ready(function() {
    // Получите ul, содержащий коллекцию тегов
    $collectionHolder = $('ul.tags');

    // добавьте привязку "добавить тег" и li к тегам ul
    $collectionHolder.append($newLinkLi);

    // почитайте текущие вводы формы, которые у вас есть (например, 2) и используйте
    // это в качестве нового индекса при вставке нового объекта (например, 2)
    $collectionHolder.data('index', $collectionHolder.find(':input').length);

    $addTagLink.on('click', function(e) {
        // предотвратите ссылку от создания "#" в URL
        e.preventDefault();

        // добавьте новую форму тега (смотрите следующий блок кода)
        addTagForm($collectionHolder, $newLinkLi);
    });
});

Работой функции addTagForm() будет использовать атрибут data-prototype, чтобы динамически добавлять новую форму, когда переходят по её ссылке. HTML data-prototype содержит вводный элемент тега text с именем task[tags][__name__][name] и id task_tags___name___name. __name__ - это маленький "заполнитель", который вы замените уникальным увеличивающимся числом (например task[tags][3][name]).

Код, необходимый для того, чтобы это всё работало, может очень разниться, но вот один пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function addTagForm($collectionHolder, $newLinkLi) {
    // Получить прототип данных, объяснённый ранее
    var prototype = $collectionHolder.data('prototype');

    // получить новый индекс
    var index = $collectionHolder.data('index');

    var newForm = prototype;
    // Вам нужно это только в случае, если вы не установили 'label', как "false" в вашем поле тегов в TaskType
    // Заменить '__name__label__' в HTML прототипа, чтобы
    // он был числом, основанным на том, сколько объектов у нас есть
    // newForm = newForm.replace(/__name__label__/g, index);

    // Заменить '__name__' в HTML прототипа на
    // номер, основанный на количестве имеющихся объектов
    newForm = newForm.replace(/__name__/g, index);

    // увеличить индекс на единицу для следующего объекта
    $collectionHolder.data('index', index + 1);

    // Отобразить форму на странице в li, до ссылки Li "добавить тег"
    var $newFormLi = $('<li></li>').append(newForm);
    $newLinkLi.before($newFormLi);
}

Note

Лучше разделять ваш JavaScript на реальные файлы JavaScript, вместо того, чтобы писать его внутри HTML, как показано тут.

Теперь, каждый раз, когда пользователь кликает по ссылке Add a tag, на странице будет появляться новая подформа. Когда форма будет отправлена, любые новые формы тегов будут конвертированы в новые объекты Tag и добавлены в свойство tags объекта Task.

Вы можете найти рабочий пример тут - JSFiddle.
Если вы хотите настроить HTML-код в прототипе, прочтите Как настроить прототип коллекции.

Чтобы облегчить управление новыми тегами, добавьте методы "adder" и "remover" для тегов в классе Task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/Entity/Task.php
namespace App\Entity;

// ...
class Task
{
    // ...

    public function addTag(Tag $tag)
    {
        $this->tags->add($tag);
    }

    public function removeTag(Tag $tag)
    {
        // ...
    }
}

Далее, добавьте опцию by_reference в поле tags и установите её как false:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/Form/Type/TaskType.php

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

    $builder->add('tags', CollectionType::class, array(
        // ...
        'by_reference' => false,
    ));
}

С этими двумя изменениями, при отправке форме, каждый новый объект Tag будет добавляться в класс Task, путём вызова метода addTag(). До этого, они добавлялись формой внутренне, путём вызова $task->getTags()->add($tag). Это было нормально, использование метода "adder" делает уапрвление новыми объектами Tag более лёгким (особенно, если вы используете Doctrine, о чём вы узнаете далее!).

Caution

Вам нужно создать оба метода: addTag() и removeTag(), иначе форма всё равно будет использовать setTag() даже если by_reference в значении false. Вы узнаете больше о методе removeTag() далее в этой статье.

Чтобы сохранить в Doctrine новые теги, вам нужно учесть еще несколько вещей. Для начала, если вы не выполните перебор всех новых объектов Tag и не вызовете в каждом $em->persist($tag), вы получите ошибку от Doctrine:

Найдена новая сущность в отношениях AppBundle\Entity\Task#tags, которая не была сконфигурирована для каскадного сохранения для сущности...

Чтобы исправить это, вы можете выбрать автоматическиую операцию "каскадного" сохранения из объекта Task в любst связанные теги. Чтобы сделать это, добавьте опцию cascade в ваши метаданные ManyToMany:

  • Annotations
    1
    2
    3
    4
    5
    6
    7
    8
    // src/Entity/Task.php
    
    // ...
    
    /**
     * @ORM\ManyToMany(targetEntity="Tag", cascade={"persist"})
     */
    protected $tags;
    
  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # src/Resources/config/doctrine/Task.orm.yml
    App\Entity\Task:
        type: entity
        # ...
        oneToMany:
            tags:
                targetEntity: Tag
                cascade:      [persist]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- src/Resources/config/doctrine/Task.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                        http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="App\Entity\Task">
            <!-- ... -->
            <one-to-many field="tags" target-entity="Tag">
                <cascade>
                    <cascade-persist />
                </cascade>
            </one-to-many>
        </entity>
    </doctrine-mapping>
    

Вторая возможная проблема касается Стороны владения и стороны инверсии отношений Doctrine. В этом примере, если сторона "владения" в отношениях - это "Task", тогда сохранение будет нормально работать, так как в Task правильно добавлены теги. Однако, если стороны владения в "Tag", тогда вам понадобится проделать некоторую работу, чтобы убедиться, что изменена правильна сторона отношений.

Секрет в том, чтобы убедиться, что один "Task" установлен в каждом "Tag". Простой способ сделать это - добавить дополнительную логику к addTag(), которая вызывается типом формы, так как by_reference установлен, как false:

1
2
3
4
5
6
7
8
9
// src/Entity/Task.php

// ...
public function addTag(Tag $tag)
{
    $tag->addTask($this);

    $this->tags->add($tag);
}

Внутри Tag, убедитесь в том, что у вас есть метод addTask():

1
2
3
4
5
6
7
8
9
// src/Entity/Tag.php

// ...
public function addTask(Task $task)
{
    if (!$this->tasks->contains($task)) {
        $this->tasks->add($task);
    }
}

Если у вас есть отношение один-ко-многим, тогда обходной путь похож, кроме того, что вы просто можете вызвать setTask() изнутри addTag().

Разрешение удаления тегов

Следующим шагом является разрешение удаления конкретного предмета в коллекции. Решение схоже с разрешением на добавление тегов.

Начните, добавив опцию allow_delete a форму Type (Тип):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/Form/Type/TaskType.php

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

    $builder->add('tags', CollectionType::class, array(
        // ...
        'allow_delete' => true,
    ));
}

Теперь, вам нужно поместить некоторый код в метод removeTag() в Task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// src/Entity/Task.php

// ...
class Task
{
    // ...

    public function removeTag(Tag $tag)
    {
        $this->tags->removeElement($tag);
    }
}

Изменения шаблонов

Опция allow_delete означает, что если предмет коллекци не отослан при отправке, связанные данные удаляются из коллекции на сервере. Для того, чтобы это работало в HTML форме, вам нужно удалить элемент DOM в удаляемом объекте коллекции, до отправки формы.

Для начала, добавьте ссылку "удалить этот тег" к каждой форме тега:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
jQuery(document).ready(function() {
    // Получить ul, которы содержит коллекцию тегов
    $collectionHolder = $('ul.tags');

    // добавить ссылку удаления ко всем существующим элементам li в форме тегов
    $collectionHolder.find('li').each(function() {
        addTagFormDeleteLink($(this));
    });

    // ... оставшийся блок, описанный выше
});

function addTagForm() {
    // ...

    // добавить ссылку удаления к новой форме
    addTagFormDeleteLink($newFormLi);
}

Функция addTagFormDeleteLink() будет выглядеть примерно так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function addTagFormDeleteLink($tagFormLi) {
    var $removeFormA = $('<a href="#">delete this tag</a>');
    $tagFormLi.append($removeFormA);

    $removeFormA.on('click', function(e) {
        // предотвратить ссылку от создания "#" в URL
        e.preventDefault();

        // удалить li в форме тегов
        $tagFormLi.remove();
    });
}

Когда форма тегов удалена из DOM и отправлена, удалённый объект Tag не будет включён в коллекцию, переданную в setTags(). В зависимости от вашего уровня сохранения, это может быть (не) достаточным для удаления отношения между удалённым Tag и объектом Task.

При удалении объектов таким способом, вам может понадобиться проделать немного больше работы, чтобы гарантировать правильное удаление отношений между Task и удалённым Tag.

В Doctrine, у вас есть две стороны отношений: сторона владения и сторона инверсии. Обычно в этом случае у вас будет отношение многие-ко-многим, и удалённые теги исчезнут и будут правильно сохранены (добавление новых тегов также работает без усилий).

Но если у вас отношение один-ко-многим, или отношение многие-ко-многим с mappedBy в сущности Task (что означает, что Task - сторона "инверсии"), вам понадобится проделать больше работы, чтобы удалённые теги правильно сохранялись.

В этом случае, вы можете изменять контроллер так, чтобы он удалял отношения удалённого тега. Это предполагает, что у вас есть некоторое действие edit(), который работает над "обновлением" вашего Task:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// src/Controller/TaskController.php

use App\Entity\Task;
use Doctrine\Common\Collections\ArrayCollection;

// ...
public function edit($id, Request $request)
{
    $em = $this->getDoctrine()->getManager();
    $task = $em->getRepository(Task::class)->find($id);

    if (!$task) {
        throw $this->createNotFoundException('No task found for id '.$id);
    }

    $originalTags = new ArrayCollection();

    // Создать ArrayCollection текущих объектов Tag в БД
    foreach ($task->getTags() as $tag) {
        $originalTags->add($tag);
    }

    $editForm = $this->createForm(TaskType::class, $task);

    $editForm->handleRequest($request);

    if ($editForm->isValid()) {

        // удалить отошения между тегом и Task
        foreach ($originalTags as $tag) {
            if (false === $task->getTags()->contains($tag)) {
                // удалить Task из Tag
                $tag->getTasks()->removeElement($task);

                // если это было отношение многие-к-одному, удалить отношения, как это
                // $tag->setTask(null);

                $em->persist($tag);

                // если вы хотите удалить Tag полностью, вы также можете это сделать
                // $em->remove($tag);
            }
        }

        $em->persist($task);
        $em->flush();

        // перенаправение на ту же страницу редактирования
        return $this->redirectToRoute('task_edit', array('id' => $id));
    }

    // отобразить какой-то шаблон формы
}

Как вы видите, правильное добавление и удаление элементов может быть коварным. Кроме случаев, когда у вас отношение многие-ко-многим, где Task - сторона "владения", вам понадобится делать дополнительную работу, чтобы убедиться в том, что отношения правильно обновлены (независимо от того, добавляете вы новые теги, или удаляете уже существующие) в каждом объекте Tag.

Плагин jQuery symfony-collection помогает с элекментами формы collection, предоставляя JavaScript функциональность, необходимую для добавления, редактирования и удаления элементов коллекции. Более продвинутая функциональность вроде перемещения или дублирования элемента в коллекции и настройки кнопок также возможна.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.