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.
Let’s start by creating a Task entity:
// 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.
Now, create a Tag class. As you saw above, a Task can have many Tag
objects:
// 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:
// 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:
// 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:
// 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:
{# 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().
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.
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)).