Symfony Form: use the Type Class as DTO

Fernando Castillo
2 min readMar 31, 2024

--

When working with Symfony forms, you may need the data in a DTO instead of an Entity. Maybe for that form you need to send a request to an API instead of persisting something in the database, or because of the architecture you are using you need to send a command to the application layer to keep it decoupled from the controller.

There is nothing preventing you from using any object to get the data from the form, so you can create a DTO class with some properties, add some validation, and use it with the Symfony Form. What is the problem here? That we have a single responsibility split in two different classes.

With this approach, when we need to add or remove one more input to the form, we need to update two classes: the form Type and the DTO. We can even say that the DTO has no other reason to change but to adapt to changes in the form. They are completely coupled together, so why not merge them?

This is what a Form Type class could look like when extended as a DTO in a simple example for getting the name and optional description for a task that would be created.

final class TaskType extends AbstractType
{
#[Assert\NotBlank]
public string $name;
public ?string $description = null;

public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['required' => true])
->add('description', TextareaType::class, ['required' => false])
->add('submit', SubmitType::class, ['label' => 'Create Task'])
;
}
}

You can see that the class includes properties for a name and a description, in the same way that they would be in a different class, and that validation can be also applied in the same way.

Then in the controller, we can set the form just by creating and passing a Type instance.

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

$form->handleRequest($request);

From that point, you have the Type instance populated and validated, and ready to be used.

As an extra, you can also use the Type class as a factory for other objects like a command based on the data received from the form. This way the class closes the whole circle: defining the form, receiving and validating the data, and then using for the next step in the processing.

final class TaskType extends AbstractType
{
#[Assert\NotBlank]
public string $name;
public ?string $description = null;

public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['required' => true])
->add('description', TextareaType::class, ['required' => false])
->add('submit', SubmitType::class, ['label' => 'Create Task'])
;
}

public function createTaskCommand(): CreateTaskCommand
{
return new CreateTaskCommand($this->name, $this->description);
}
}

--

--