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

Nicolas Grekas
Contributed by Nicolas Grekas in #66772

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

Nicolas Grekas
Contributed by Nicolas Grekas in #66026

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

Nicolas Grekas
Contributed by Nicolas Grekas in #66731

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.

Published in #Living on the edge