How to Embed a Collection of Forms

Symfony Forms can embed a collection of many other forms, which is useful to edit related entities in a single form. In this article, you'll create a form to edit a Task class and, right inside the same form, you'll be able to edit, create and remove many Tag objects related to that Task.

Symfony フォームは、他の多くのフォームのコレクションを埋め込むことができます。これは、関連するエンティティを単一のフォームで編集するのに役立ちます。この記事では、Task クラスを編集するためのフォームを作成し、同じフォーム内で、その Task に関連する多くの Tag オブジェクトを編集、作成、および削除できるようにします。

Let's start by creating a Task entity:

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;
use Doctrine\Common\Collections\Collection;

class Task
{
    protected $description;
    protected $tags;

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

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

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

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

Note

ノート

The ArrayCollection is specific to Doctrine and is similar to a PHP array but provides many utility methods.

ArrayCollection は Doctrine 固有のもので、PHP の配列に似ていますが、多くのユーティリティ メソッドを提供します。

Now, create a Tag class. As you saw above, a Task can have many Tag objects:

次に、Tag クラスを作成します。上で見たように、Task は多くの Tagobjects を持つことができます:
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(): string
    {
        return $this->name;
    }

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

Then, create a form class so that a Tag object can be modified by the user:

次に、フォーム クラスを作成して、ユーザーが 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/TagType.php
namespace App\Form;

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): void
    {
        $builder->add('name');
    }

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

Next, let's create a form for the Task entity, using a CollectionType field of TagType forms. This will allow us to modify all the Tag elements of a Task right inside the task form itself:

次に、TagTypeforms の CollectionType フィールドを使用して、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
// src/Form/TaskType.php
namespace App\Form;

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

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

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

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

In your controller, you'll create a new form from the TaskType:

コントローラーで、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
39
// src/Controller/TaskController.php
namespace App\Controller;

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

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

        // dummy code - add some example tags to the task
        // (otherwise, the template will render an empty list of tags)
        $tag1 = new Tag();
        $tag1->setName('tag1');
        $task->getTags()->add($tag1);
        $tag2 = new Tag();
        $tag2->setName('tag2');
        $task->getTags()->add($tag2);
        // end dummy code

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

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // ... do your form processing, like saving the Task and Tag entities
        }

        return $this->renderForm('task/new.html.twig', [
            'form' => $form,
        ]);
    }
}

In the template, you can now iterate over the existing TagType forms to render them:

テンプレートでは、既存の TagType フォームを反復してレンダリングできるようになりました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{# 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) }}

{# ... #}

When the user submits the form, the submitted data for the tags field is used to construct an ArrayCollection of Tag objects. The collection is then set on the tag field of the Task and can be accessed via $task->getTags().

ユーザーがフォームを送信すると、送信されたタグ フィールドのデータが Tag オブジェクトの ArrayCollection の構築に使用されます。コレクションは Task のタグ フィールドに設定され、$task->getTags() 経由でアクセスできます。

So far, this works great, but only to edit existing tags. It doesn't allow us yet to add new tags or delete existing ones.

これまでのところ、これはうまく機能しますが、既存のタグを編集するためだけです。 useet が新しいタグを追加したり、既存のタグを削除したりすることはできません。

Caution

注意

You can embed nested collections as many levels down as you like. However, if you use Xdebug, you may receive a Maximum function nesting level of '100' reached, aborting! error. To fix this, increase the xdebug.max_nesting_level PHP setting, or render each form field by hand using form_row() instead of rendering the whole form at once (e.g form_widget(form)).

ネストされたコレクションは、好きなだけ下のレベルに埋め込むことができます。ただし、Xdebug を使用すると、最大関数ネスト レベル '100'reached, aborting! が表示される場合があります。エラー。これを修正するには、xdebug.max_nesting_levelPHP 設定を増やすか、フォーム全体を一度にレンダリングする代わりに、form_row() を使用して各フォーム フィールドを手動でレンダリングします (例: form_widget(form))。

Allowing "new" Tags with the "Prototype"

Previously you added two tags to your task in the controller. Now let the users add as many tag forms as they need directly in the browser. This requires a bit of JavaScript code.

前に、コントローラーのタスクに 2 つのタグを追加しました。ここで、ユーザーがブラウザで直接必要な数のタグ フォームを追加できるようにします。これには少しの JavaScript コードが必要です。

But first, you need to let the form collection know that instead of exactly two, it will receive an unknown number of tags. Otherwise, you'll see a "This form should not contain extra fields" error. This is done with the allow_add option:

ただし、最初に、正確に 2 つではなく、不明な数のタグを受け取ることをフォーム コレクションに知らせる必要があります。そうしないと、「このフォームには余分なフィールドを含めないでください」というエラーが表示されます。これは、allow_add オプションで行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Form/TaskType.php

// ...

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

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

The allow_add option also makes a prototype variable available to you. This "prototype" is a little "template" that contains all the HTML needed to dynamically create any new "tag" forms with JavaScript.

allow_add オプションを使用すると、プロトタイプ変数も使用できるようになります。この「プロトタイプ」は、JavaScript で新しい「タグ」フォームを動的に作成するために必要なすべての HTML を含む小さな「テンプレート」です。

Let's start with plain JavaScript (Vanilla JS) – if you're using Stimulus, see below.

プレーンな JavaScript (Vanilla JS) から始めましょう。Stimulus を使用している場合は、以下を参照してください。

To render the prototype, add the following data-prototype attribute to the existing <ul> in your template:

プロトタイプをレンダリングするには、次の data-prototype 属性をテンプレートの既存の属性に追加します。
1
2
3
4
5
{# the data-index attribute is required for the JavaScript code below #}
<ul class="tags"
    data-index="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}"
    data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}"
></ul>

On the rendered page, the result will look something like this:

レンダリングされたページでは、結果は次のようになります。
1
2
3
4
<ul class="tags"
    data-index="0"
    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;"
></ul>

Now add a button to dynamically add a new tag:

次に、新しいタグを動的に追加するボタンを追加します。
1
<button type="button" class="add_item_link" data-collection-holder-class="tags">Add a tag</button>

See also

こちらもご覧ください

If you want to customize the HTML code in the prototype, see How to Work with Form Themes.

プロトタイプの HTML コードをカスタマイズする場合は、フォーム テーマの操作方法を参照してください。

Tip

ヒント

The form.tags.vars.prototype is a form element that looks and feels just like the individual form_widget(tag.*) elements inside your for loop. This means that you can call form_widget(), form_row() or form_label() on it. You could even choose to render only one of its fields (e.g. the name field):

form.tags.vars.prototype は、for ループ内の個々の form_widget(tag.*) 要素とまったく同じように見えるフォーム要素です。つまり、form_widget()、form_row()、または form_label() を呼び出すことができます。 .そのフィールドの 1 つだけをレンダリングすることを選択することもできます (例: name フィールド):
1
{{ form_widget(form.tags.vars.prototype.name)|e }}

Note

ノート

If you render your whole "tags" sub-form at once (e.g. form_row(form.tags)), the data-prototype attribute is automatically added to the containing div, and you need to adjust the following JavaScript accordingly.

「タグ」サブフォーム全体 (例: form_row(form.tags)) を一度にレンダリングすると、data-prototype 属性が含まれる div に自動的に追加され、それに応じて次の JavaScript を調整する必要があります。

Now add some JavaScript to read this attribute and dynamically add new tag forms when the user clicks the "Add a tag" link. Add a <script> tag somewhere on your page to include the required functionality with JavaScript:

ここで、JavaScript を追加してこの属性を読み取り、ユーザーが [タグの追加] リンクをクリックしたときに新しいタグ フォームを動的に追加します。ページのどこかに atag を追加して、JavaScript で必要な機能を含めます。
1
2
3
4
5
document
  .querySelectorAll('.add_item_link')
  .forEach(btn => {
      btn.addEventListener("click", addFormToCollection)
  });

The addFormToCollection() function's job will be to use the data-prototype attribute to dynamically add a new form when this link is clicked. The data-prototype HTML contains the tag's text input element with a name of task[tags][__name__][name] and id of task_tags___name___name. The __name__ is a placeholder, which you'll replace with a unique, incrementing number (e.g. task[tags][3][name]):

addFormToCollection() 関数の役割は、data-prototype 属性を使用して、このリンクがクリックされたときに新しいフォームを動的に追加することです。 data-prototypeHTML には、名前が 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
const addFormToCollection = (e) => {
  const collectionHolder = document.querySelector('.' + e.currentTarget.dataset.collectionHolderClass);

  const item = document.createElement('li');

  item.innerHTML = collectionHolder
    .dataset
    .prototype
    .replace(
      /__name__/g,
      collectionHolder.dataset.index
    );

  collectionHolder.appendChild(item);

  collectionHolder.dataset.index++;
};

Now, each time a user clicks the Add a tag link, a new sub form will appear on the page. When the form is submitted, any new tag forms will be converted into new Tag objects and added to the tags property of the Task object.

これで、ユーザーが [タグを追加] リンクをクリックするたびに、新しいサブフォームがページに表示されます。フォームが送信されると、新しいタグ フォームはすべて新しい Tag オブジェクトに変換され、Task オブジェクトの tags プロパティに追加されます。

See also

こちらもご覧ください

You can find a working example in this JSFiddle.

この JSFiddle で実際の例を見つけることができます。

JavaScript with Stimulus

If you're using Stimulus, wrap everything in a <div>:

Stimulus を使用している場合は、すべてを : でラップします。
1
2
3
4
5
6
7
<div {{ stimulus_controller('form-collection') }}
    data-form-collection-index-value="{{ form.tags|length > 0 ? form.tags|last.vars.name + 1 : 0 }}"
    data-form-collection-prototype-value="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}"
>
    <ul {{ stimulus_target('form-collection', 'collectionContainer') }}></ul>
    <button type="button" {{ stimulus_action('form-collection', 'addCollectionElement') }}>Add a tag</button>
</div>

Then create the controller:

次に、コントローラーを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// assets/controllers/form-collection_controller.js

import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ["collectionContainer"]

    static values = {
        index    : Number,
        prototype: String,
    }

    addCollectionElement(event)
    {
        const item = document.createElement('li');
        item.innerHTML = this.prototypeValue.replace(/__name__/g, this.indexValue);
        this.collectionContainerTarget.appendChild(item);
        this.indexValue++;           
    }
}

Handling the new Tags in PHP

To make handling these new tags easier, add an "adder" and a "remover" method for the tags in the Task class:

これらの新しいタグを簡単に処理するには、Task クラスのタグに「adder」メソッドと「remover」メソッドを追加します。
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): void
    {
        $this->tags->add($tag);
    }

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

Next, add a by_reference option to the tags field and set it to false:

次に、tags フィールドに by_reference オプションを追加して false に設定します。
1
2
3
4
5
6
7
8
9
10
11
12
// src/Form/TaskType.php

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

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

With these two changes, when the form is submitted, each new Tag object is added to the Task class by calling the addTag() method. Before this change, they were added internally by the form by calling $task->getTags()->add($tag). That was fine, but forcing the use of the "adder" method makes handling these new Tag objects easier (especially if you're using Doctrine, which you will learn about next!).

これら 2 つの変更により、フォームが送信されると、addTag() メソッドを呼び出すことによって、新しい Tag オブジェクトがそれぞれ Task クラスに追加されます。この変更の前は、$task->getTags()->add($tag) を呼び出して、フォームによって内部的に追加されていました。これは問題ありませんでしたが、「adder」メソッドの使用を強制すると、これらの新しい Tag オブジェクトの処理が簡単になります (特にDoctrine を使用していますが、これについては次に学びます!)。

Caution

注意

You have to create both addTag() and removeTag() methods, otherwise the form will still use setTag() even if by_reference is false. You'll learn more about the removeTag() method later in this article.

addTag() メソッドと removeTag() メソッドの両方を作成する必要があります。作成しないと、by_reference が false であっても、フォームは setTag() を使用します。removeTag() メソッドについては、この記事の後半で詳しく説明します。

Caution

注意

Symfony can only make the plural-to-singular conversion (e.g. from the tags property to the addTag() method) for English words. Code written in any other language won't work as expected.

symfony は英単語の複数形から単数形への変換 (例えば、tags プロパティから addTag() メソッドへ) のみを行うことができます。他の言語で記述されたコードは期待どおりに動作しません。
Doctrine: リレーションのカスケードと「逆」側の保存

To save the new tags with Doctrine, you need to consider a couple more things. First, unless you iterate over all of the new Tag objects and call $entityManager->persist($tag) on each, you'll receive an error from Doctrine:

Doctrine で新しいタグを保存するには、さらにいくつかのことを考慮する必要があります。まず、すべての新しい Tag オブジェクトを反復処理し、それぞれに対して $entityManager->persist($tag) を呼び出さない限り、Doctrine からエラーが発生します:
1
2
3
A new entity was found through the relationship
``App\Entity\Task#tags`` that was not configured to
cascade persist operations for entity...

To fix this, you may choose to "cascade" the persist operation automatically from the Task object to any related tags. To do this, add the cascade option to your ManyToMany metadata:

これを修正するには、持続操作を Task オブジェクトから関連タグに自動的に「カスケード」することを選択できます。これを行うには、カスケード オプションを ManyToMany メタデータに追加します。
  • Attributes
    属性
  • YAML
    YAML
  • XML
    XML
1
2
3
4
5
6
// src/Entity/Task.php

// ...

#[ORM\ManyToMany(targetEntity: Tag::class, cascade: ['persist'])]
protected $tags;

A second potential issue deals with the Owning Side and Inverse Side of Doctrine relationships. In this example, if the "owning" side of the relationship is "Task", then persistence will work fine as the tags are properly added to the Task. However, if the owning side is on "Tag", then you'll need to do a little bit more work to ensure that the correct side of the relationship is modified.

2 番目の潜在的な問題は、Doctrine の所有側と逆側の関係を扱います。この例では、リレーションシップの「所有」側が「タスク」である場合、タグが適切にタスクに追加されるため、持続性は正常に機能します。ただし、所有側が「タグ」にある場合は、関係の正しい側が変更されるように、もう少し作業を行う必要があります。

The trick is to make sure that the single "Task" is set on each "Tag". One way to do this is to add some extra logic to addTag(), which is called by the form type since by_reference is set to false:

秘訣は、単一の「タスク」が各「タグ」に設定されていることを確認することです。これを行う 1 つの方法は、addTag() にロジックを追加することです。これは、by_reference が false に設定されているため、フォーム タイプによって呼び出されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Entity/Task.php

// ...
public function addTag(Tag $tag): void
{
    // for a many-to-many association:
    $tag->addTask($this);

    // for a many-to-one association:
    $tag->setTask($this);

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

If you're going for addTask(), make sure you have an appropriate method that looks something like this:

addTask() を使用する場合は、次のような適切なメソッドがあることを確認してください。
1
2
3
4
5
6
7
8
9
// src/Entity/Tag.php

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

Allowing Tags to be Removed

The next step is to allow the deletion of a particular item in the collection. The solution is similar to allowing tags to be added.

次のステップは、コレクション内の特定のアイテムの削除を許可することです。解決策は、タグの追加を許可するのと似ています。

Start by adding the allow_delete option in the form Type:

タイプのフォームに allow_delete オプションを追加することから始めます。
1
2
3
4
5
6
7
8
9
10
11
12
// src/Form/TaskType.php

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

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

Now, you need to put some code into the removeTag() method of Task:

次に、Task の removeTag() メソッドにコードを追加する必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
// src/Entity/Task.php

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

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

The allow_delete option means that if an item of a collection isn't sent on submission, the related data is removed from the collection on the server. In order for this to work in an HTML form, you must remove the DOM element for the collection item to be removed, before submitting the form.

allow_delete オプションは、コレクションのアイテムが送信時に送信されない場合、関連するデータがサーバー上のコレクションから削除されることを意味します。これを HTML フォームで機能させるには、フォームを送信する前に、削除するコレクション アイテムの DOM 要素を削除する必要があります。

First, add a "delete this tag" link to each tag form:

まず、「このタグを削除」リンクを各タグ フォームに追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
document
    .querySelectorAll('ul.tags li')
    .forEach((tag) => {
        addTagFormDeleteLink(tag)
    })

// ... the rest of the block from above

const addFormToCollection = (e) => {
    // ...

    // add a delete link to the new form
    addTagFormDeleteLink(item);
}

The addTagFormDeleteLink() function will look something like this:

addTagFormDeleteLink() 関数は次のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
const addTagFormDeleteLink = (item) => {
    const removeFormButton = document.createElement('button');
    removeFormButton.innerText = 'Delete this tag';

    item.append(removeFormButton);

    removeFormButton.addEventListener('click', (e) => {
        e.preventDefault();
        // remove the li for the tag form
        item.remove();
    });
}

When a tag form is removed from the DOM and submitted, the removed Tag object will not be included in the collection passed to setTags(). Depending on your persistence layer, this may or may not be enough to actually remove the relationship between the removed Tag and Task object.

タグ フォームが DOM から削除されて送信されると、削除された Tag オブジェクトは setTags() に渡されるコレクションに含まれません。永続化レイヤーによっては、削除されたタグとタスク オブジェクトの間の関係を実際に削除するには、これで十分な場合と不十分な場合があります。
Doctrine: データベースの永続性を確保する

When removing objects in this way, you may need to do a little bit more work to ensure that the relationship between the Task and the removed Tag is properly removed.

この方法でオブジェクトを削除する場合、Task と removedTag の間の関係が適切に削除されるように、もう少し作業が必要になる場合があります。

In Doctrine, you have two sides of the relationship: the owning side and the inverse side. Normally in this case you'll have a many-to-one relationship and the deleted tags will disappear and persist correctly (adding new tags also works effortlessly).

Doctrine では、関係には 2 つの側面があります: 所有側と逆側です。通常、この場合、多対 1 の関係があり、削除されたタグは消えて正しく保持されます (新しいタグの追加も簡単に機能します)。

But if you have a one-to-many relationship or a many-to-many relationship with a mappedBy on the Task entity (meaning Task is the "inverse" side), you'll need to do more work for the removed tags to persist correctly.

ただし、Task エンティティで amappedBy との 1 対多の関係または多対多の関係がある場合 (Task が「逆」側であることを意味します)、削除されたタグを保持するには、さらに作業を行う必要があります。正しく。

In this case, you can modify the controller to remove the relationship on the removed tag. This assumes that you have some edit() action which is handling the "update" of your Task:

この場合、コントローラを変更して、削除されたタグの関係を削除できます。これは、タスクの「更新」を処理する edit() アクションがあることを前提としています。
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
// src/Controller/TaskController.php

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

class TaskController extends AbstractController
{
    public function edit($id, Request $request, EntityManagerInterface $entityManager): Response
    {
        if (null === $task = $entityManager->getRepository(Task::class)->find($id)) {
            throw $this->createNotFoundException('No task found for id '.$id);
        }

        $originalTags = new ArrayCollection();

        // Create an ArrayCollection of the current Tag objects in the database
        foreach ($task->getTags() as $tag) {
            $originalTags->add($tag);
        }

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

        $editForm->handleRequest($request);

        if ($editForm->isSubmitted() && $editForm->isValid()) {
            // remove the relationship between the tag and the Task
            foreach ($originalTags as $tag) {
                if (false === $task->getTags()->contains($tag)) {
                    // remove the Task from the Tag
                    $tag->getTasks()->removeElement($task);

                    // if it was a many-to-one relationship, remove the relationship like this
                    // $tag->setTask(null);

                    $entityManager->persist($tag);

                    // if you wanted to delete the Tag entirely, you can also do that
                    // $entityManager->remove($tag);
                }
            }

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

            // redirect back to some edit page
            return $this->redirectToRoute('task_edit', ['id' => $id]);
        }

        // ... render some form template
    }
}

As you can see, adding and removing the elements correctly can be tricky. Unless you have a many-to-many relationship where Task is the "owning" side, you'll need to do extra work to make sure that the relationship is properly updated (whether you're adding new tags or removing existing tags) on each Tag object itself.

ご覧のとおり、要素を正しく追加および削除するのは難しい場合があります。タスクが「所有」側である多対多の関係がない限り、関係が適切に更新されるように追加の作業を行う必要があります。 (新しいタグを追加する場合でも、既存のタグを削除する場合でも) 各 Tag オブジェクト自体。

See also

こちらもご覧ください

The Symfony community has created some JavaScript packages that provide the functionality needed to add, edit and delete elements of the collection. Check out the @a2lix/symfony-collection package for modern browsers and the symfony-collection package based on jQuery for the rest of browsers.

Symfony コミュニティは、コレクションの要素を追加、編集、および削除するために必要な機能を提供するいくつかの JavaScript パッケージを作成しました。最新のブラウザー用の @a2lix/symfony-collection パッケージと、その他のブラウザー用の jQuery ベースの symfony-collection パッケージを確認してください。