DTO in Symfony Form

You’re building your first form in Symfony. What do you do? You check the documentation! But what if there’s a better way than the one described in the docs?

You’re building your first form in Symfony. What do you do? You check the documentation! It walks you through the entire process step-by-step: how to create a form in a controller, why and how to separate the form into its own class, and the whole validation process. Sounds great, right? Just follow along and start creating forms. But what if there’s a better approach—using DTOs in your forms?

Hi! A follow-up post has been published expanding on the information shared here: DTO in Symfony Forms – Part 2.

The Temptation of Tying Forms to Doctrine Entities

Although the examples in the Symfony documentation don’t rely on Doctrine entities, the temptation to do so is strong. Attaching entities to forms is both tempting and convenient. You don’t need to write extra code to transfer form data to the database, and validation can be directly tied to Doctrine entities. This makes application development feel faster.

Despite the examples being based on Symfony Form and Doctrine, there’s nothing stopping you from using these methods with other libraries.

However, this approach—though easy to use—has its downsides:

  • Incorrect Doctrine entity state when the form contains errors.
  • Issues with external validation libraries (e.g., beberlei/assert).
  • The need for form fields to match entity attributes, limiting flexibility.
  • Probably other I didn't think of yet...

Entity Attached to a Form

Let’s look at an example. You have a form and a Doctrine entity:

php
Copied
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
/**
 * @ORM\Entity()
 */
class Post
{
    /**
     * @ORM\Id()
     * @ORM\Column(type="uuid", unique=true)
     */
    private UuidInterface $id;

    /**
     * @Assert\Length(min=5)
     * @ORM\Column(type="string", nullable=false)
     */
    private string $title;

    public function __construct(UuidInterface $id, string $title)
    {
        $this->id = Uuid::uuid4();
        $this->setTitle($title);
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function setTitle(string $title): void
    {
        $this->title = $title;
    }
}

You hit “submit,” and… the title is too short. Now your entity contains invalid data. If $em->flush() is called anywhere in the application, this invalid state is saved to the database.

To avoid this problem, you might add a condition like:

php
Copied
1
2
3
4
5
public function setTitle(string $title): void
{
    Assertion::minLength($title, 5);
    $this->title = $title;
}

You hit “submit” again, and… 500! Symfony Form uses the setter, throws an exception, and doesn’t know how to handle it.

Enter DTO in Forms to the Rescue!

A DTO (Data Transfer Object) is a simple object that doesn’t even need setters—public properties are sufficient. Its sole purpose is to transfer data from point A to point B.

Here’s how you could define a DTO:

php
Copied
1
2
3
4
5
6
7
8
class UpdatePost
{
    /**
     * @Assert\Length(min=5)
     * @Assert\NotBlank()
     */
    public ?string $title = null;
}

You can remove the validation constraints from the entity. Next, you connect the UpdatePost DTO to the form and handle it like this:

php
Copied
1
2
3
4
5
6
7
8
9
10
11
$postUpdateData = new UpdatePost();
$form = $formFactory->create(UpdatePostType::class, $postUpdateData);

$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    $post = $postRepository->get($id);
    $post->setTitle($postUpdateData->title);

    $em->flush();
}

Changing a User’s Password with a DTO

Here’s another example that highlights the value of using DTOs in forms. Let’s say you want to allow users to change their passwords. You create a form with:

php
Copied
1
$builder->add('newPassword', PasswordType::class);

Do you see the problem? In the entity, you’d need to add an attribute that won’t be saved to the database. It might exist, or it might not… Either way, it’s unnecessary. By handling this with a DTO, you can freely map form fields and, after successful validation, transfer the data to the entity. Plus, the entity never handles plaintext passwords because you hash them first.

Why Use DTOs in Forms?

There are several reasons:

  1. Decoupling forms from Doctrine entities – Your form and entity remain independent of each other.
  2. Ensuring only valid data enters the database – You can confidently process data after validation.
  3. Centralizing entity updates in a service – This allows you to reuse the logic across different inputs (e.g., forms, APIs, or CLI).

By leveraging DTOs, you create a more modular, robust, and maintainable codebase. While it might take a little extra effort initially, the benefits far outweigh the costs in the long run.

Related posts