Symfony forms are often backed by a DTO or another class with typed properties. Those types say a lot about each field, but you have to repeat much of that information when creating your forms. In Symfony 8.2, the Form component uses those types, so forms need less configuration, especially with the new attribute-based forms.
Field Types Guessed from PHP Types
Consider a plain DTO (not a Doctrine entity) with the properties ?int $capacity
and ?\DateTimeImmutable $startsAt. Their types say what these fields hold, but
you still have to repeat it in the form:
1 2 3 4 5 6 7
$builder
->add('capacity', IntegerType::class, ['required' => false])
->add('startsAt', DateTimeType::class, [
'input' => 'datetime_immutable',
'required' => false,
])
;
If you omit the type, Symfony asks its form type guesser services. They read the
Doctrine mapping of entities and validation constraints such as Type, but a plain
DTO usually has neither, so an int property ends up as a TextType field.
Symfony 8.2 adds a guesser that reads the PHP type of the property. It's enabled
by default, so ->add('capacity') is now enough. Fields with an explicit type
don't change, and mapped Doctrine columns already got these types from their mapping.
This is especially useful with attribute-based forms, which are usually DTOs. A
#[FormField] attribute without a type uses the same guessing, so you don't need
to write #[FormField(IntegerType::class)]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Dto/EventDto.php
namespace App\Dto;
use Symfony\Component\Form\Attribute\AsFormType;
use Symfony\Component\Form\Attribute\FormField;
#[AsFormType]
class EventDto
{
#[FormField] // IntegerType, not required
public ?int $capacity = null;
#[FormField] // NumberType, not required
public ?float $price = null;
#[FormField] // CheckboxType, not required
public bool $isPublic = false;
#[FormField] // DateTimeType (input: datetime_immutable), not required
public ?\DateTimeImmutable $startsAt = null;
#[FormField] // DateIntervalType, not required
public ?\DateInterval $duration = null;
}
DateTime, DateTimeInterface and DatePoint properties also become a
DateTimeType. string properties are left alone so they don't override better
guesses, such as the TextareaType that Doctrine suggests for text columns.
Empty Values for Non-Nullable Properties
Submitting an empty text field writes null to the mapped property. With
public string $name in a DTO or setName(string $name) in an entity, that
throws an exception:
1
Expected argument of type "string", "null" given at property path "name".
The usual fix was to repeat 'empty_data' => '' on every affected field. Symfony
8.2 guesses the empty_data option from the type of the setter or public property:
1 2 3 4 5 6 7 8 9 10 11 12
#[AsFormType]
class ProductDto
{
#[FormField] // an empty value is stored as ''
public string $name = '';
#[FormField] // an empty value is stored as 0
public int $stock = 0;
#[FormField] // an empty value is stored as null, as before
public ?string $description = null;
}
Keep in mind that 0 passes the NotBlank constraint. To reject empty numeric
fields, make the property nullable.
Note
This option is only guessed when the field type is guessed too. Fields with an
explicit type, such as ->add('name', TextType::class), must still define
empty_data.
Readonly Classes as Form Data
Forms create their data with new $class() and update it through setters or
public properties, so a readonly DTO needed an empty_data closure plus a
setter closure per field. In Symfony 8.2, when the constructor has required
arguments or promoted readonly properties, the form passes it the data of the
fields, matched by property path:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Dto/SignUp.php
namespace App\Dto;
use Symfony\Component\Form\Attribute\AsFormType;
use Symfony\Component\Form\Attribute\FormField;
use Symfony\Component\Validator\Constraints as Assert;
#[AsFormType]
final readonly class SignUp
{
public function __construct(
#[FormField, Assert\NotBlank, Assert\Email]
public string $email,
#[FormField]
public ?\DateTimeImmutable $startsAt = null,
// there's no field for this argument, so it gets its default value
public bool $confirmed = false,
) {
}
}
If email is empty, NotBlank reports the error on that field. Arguments that
can't take an empty value, such as a required date, also report it on their field.
You can edit existing objects too. When a field mapped to a promoted readonly property changes, Symfony creates a new object with the other arguments of the previous one and leaves the original untouched, so you must get the result from the form:
1 2 3 4 5 6 7 8 9
$form = $this->createForm(SignUp::class, $signUp);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// a new object if any readonly field changed; otherwise, the same $signUp
$signUp = $form->getData();
// ...
}
This requires a constructor that promotes all the properties of the class, so no
state is lost. In other classes, such as an entity with a private $id, changing
a readonly field still throws an exception.