To implement Symfony forms with children forms (form collections) to add or remove elements, we'll create a simple Book entity related to a Author entity in a one-to-many relationship. We'll use form collections to allow users to add, update or remove authors while creating or editing a book.

 

Create the Entity Classes:
Create the Book and Author entity classes and set up the one-to-many relationship between them.

// src/Entity/Book.php
namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Book
{
    // ... other properties and annotations ...

    /**
     * @ORM\OneToMany(targetEntity="Author", mappedBy="book", cascade={"persist", "remove"}, orphanRemoval=true)
     */
    private $authors;

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

    // ... getters and setters ...
}

// src/Entity/Author.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Author
{
    // ... other properties and annotations ...

    /**
     * @ORM\ManyToOne(targetEntity="Book", inversedBy="authors")
     * @ORM\JoinColumn(name="book_id", referencedColumnName="id")
     */
    private $book;

    // ... getters and setters ...
}

 

Create the Form Type Classes:
Now, create the form type classes for the Book and Author entities. In the BookType class, use the CollectionType field type to handle the one-to-many relationship with AuthorType as the embedded child form.

// src/Form/AuthorType.php
namespace App\Form;

use App\Entity\Author;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

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

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

// src/Form/BookType.php
namespace App\Form;

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

class BookType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            ->add('authors', CollectionType::class, [
                'entry_type' => AuthorType::class,
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
            ]);
    }

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

 

Use the Form in a Controller:
Now, use the form in a controller to handle book creation and editing.

// src/Controller/BookController.php
namespace App\Controller;

use App\Entity\Book;
use App\Form\BookType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BookController extends AbstractController
{
    /**
     * @Route("/book/create", name="book_create")
     */
    public function createBook(Request $request): Response
    {
        $book = new Book();
        $form = $this->createForm(BookType::class, $book);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            // Save the book and its related authors to the database
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($book);
            $entityManager->flush();

            // Redirect to the book list page or show a success message
            return $this->redirectToRoute('book_list');
        }

        return $this->render('book/create_book.html.twig', [
            'form' => $form->createView(),
        ]);
    }

    // ... other actions ...
}

 

Render the Form in a Twig Template:
Finally, render the form in a Twig template.

{# templates/book/create_book.html.twig #}
{% extends 'base.html.twig' %}

{% block content %}
    <h1>Create Book</h1>
    {{ form_start(form) }}
    {{ form_row(form.title) }}
    <h2>Authors</h2>
    <ul id="authors-list">
        {% for authorForm in form.authors %}
            <li>{{ form_row(authorForm.name) }}</li>
        {% endfor %}
    </ul>
    <a href="#" id="add-author-link">Add Author</a>
    <button type="submit">Create Book</button>
    {{ form_end(form) }}

    <script>
        // JavaScript to handle adding a new author form
        document.getElementById('add-author-link').addEventListener('click', function (e) {
            e.preventDefault();
            var list = document.getElementById('authors-list');
            var newAuthorForm = document.createElement('li');
            newAuthorForm.innerHTML = '{{ form_widget(form.authors.vars.prototype.name) }}';
            list.appendChild(newAuthorForm);
        });
    </script>
{% endblock %}

In this template, we manually render the "authors" collection using a JavaScript snippet that allows the user to add new author fields dynamically. The allow_add and allow_delete options in the BookType class enable adding and removing author fields respectively.

Now, when you access the /book/create route, you'll see the "Create Book" form with a section to add authors dynamically. The authors will be persisted along with the book entity when you submit the form. Similarly, when you edit an existing book, you'll be able to add or remove authors for that book.

 

To remove an author from the previous example, we'll modify the JavaScript code and add a remove link to each author form. When the user clicks the remove link, the associated author form will be removed from the form collection, allowing the user to delete an author from the book entity.

Update the Twig template to include the remove link for each author form:

{# templates/book/create_book.html.twig #}
{% extends 'base.html.twig' %}

{% block content %}
    <h1>Create Book</h1>
    {{ form_start(form) }}
    {{ form_row(form.title) }}
    <h2>Authors</h2>
    <ul id="authors-list">
        {% for authorForm in form.authors %}
            <li>
                {{ form_row(authorForm.name) }}
                <a href="#" class="remove-author-link">Remove</a>
            </li>
        {% endfor %}
    </ul>
    <a href="#" id="add-author-link">Add Author</a>
    <button type="submit">Create Book</button>
    {{ form_end(form) }}

    <script>
        // JavaScript to handle adding and removing author forms
        document.getElementById('add-author-link').addEventListener('click', function (e) {
            e.preventDefault();
            var list = document.getElementById('authors-list');
            var newAuthorForm = document.createElement('li');
            newAuthorForm.innerHTML = '{{ form_widget(form.authors.vars.prototype.name) }}' +
                '<a href="#" class="remove-author-link">Remove</a>';
            list.appendChild(newAuthorForm);
        });

        document.addEventListener('click', function (e) {
            if (e.target && e.target.classList.contains('remove-author-link')) {
                e.preventDefault();
                e.target.parentNode.remove();
            }
        });
    </script>
{% endblock %}

With this update, each author form will have a "Remove" link next to it. When the user clicks the link, the associated author form will be removed from the form collection. When you submit the form, the authors that were removed will also be deleted from the database since we set the orphanRemoval=true option in the Book entity's authors property.

 

 

To update an author in the previous example, we'll need to retrieve the existing author data and pre-fill the form fields with that data. This way, the user can make changes to the author's information and submit the form to update the database accordingly.

Add the UpdateBook Action to the Controller:

This action will retrieve the book and its associated authors from the database, create the form with the existing data, and process the form submission.

// src/Controller/BookController.php
namespace App\Controller;

use App\Entity\Book;
use App\Form\BookType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BookController extends AbstractController
{
    /**
     * @Route("/book/update/{id}", name="book_update")
     */
    public function updateBook(Request $request, $id): Response
    {
        $entityManager = $this->getDoctrine()->getManager();
        $book = $entityManager->getRepository(Book::class)->find($id);

        if (!$book) {
            throw $this->createNotFoundException('Book not found');
        }

        // Create the form with the existing book data and its associated authors
        $form = $this->createForm(BookType::class, $book);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            // Save the updated book and its associated authors to the database
            $entityManager->flush();

            // Redirect to the book list page or show a success message
            return $this->redirectToRoute('book_list');
        }

        return $this->render('book/update_book.html.twig', [
            'form' => $form->createView(),
        ]);
    }

    // ... other actions ...
}

 

Create the Update Twig Template:
Create a Twig template for the update action, where you'll render the form to update the book and its associated authors.

{# templates/book/update_book.html.twig #}
{% extends 'base.html.twig' %}

{% block content %}
    <h1>Update Book</h1>
    {{ form_start(form) }}
    {{ form_row(form.title) }}
    <h2>Authors</h2>
    <ul id="authors-list">
        {% for authorForm in form.authors %}
            <li>
                {{ form_row(authorForm.name) }}
                <a href="#" class="remove-author-link">Remove</a>
            </li>
        {% endfor %}
    </ul>
    <a href="#" id="add-author-link">Add Author</a>
    <button type="submit">Update Book</button>
    {{ form_end(form) }}

    <script>
        // JavaScript to handle adding and removing author forms
        document.getElementById('add-author-link').addEventListener('click', function (e) {
            e.preventDefault();
            var list = document.getElementById('authors-list');
            var newAuthorForm = document.createElement('li');
            newAuthorForm.innerHTML = '{{ form_widget(form.authors.vars.prototype.name) }}' +
                '<a href="#" class="remove-author-link">Remove</a>';
            list.appendChild(newAuthorForm);
        });

        document.addEventListener('click', function (e) {
            if (e.target && e.target.classList.contains('remove-author-link')) {
                e.preventDefault();
                e.target.parentNode.remove();
            }
        });
    </script>
{% endblock %}

With these changes, when you access the /book/update/{id} route, the "Update Book" form will be displayed with the existing book title and its associated authors' names pre-filled. The user can make changes to the book or author information and submit the form to update the database with the changes.