| name | symfony-form |
| description | ACTIVATE when working with Symfony FormTypes, form handling, data_class, DataTransformers, form options, or property_path. ACTIVATE when creating or modifying any FormType class. Covers: data_class as single source of truth (no data in options), DataTransformer placement (not controller), property_path for collection mapping. DO NOT use for: form submission flow/PRG (see symfony:prg-pattern), form validation rules, general Symfony questions. |
| version | 1.1 |
Symfony Form Conventions
1. The data_class Is the Single Source of Truth
All form data must flow through the data_class, not through options.
Options configure the form type behavior (labels, choices, validation rules), not transport data.
Problem
$this->formFactory->create(IdentityDocumentType::class, $data, [
'customer_birth_date' => $birthDate,
'existing_files' => $existingFiles,
]);
$resolver->setRequired('customer_birth_date');
$resolver->setRequired('existing_files');
Solution
$this->formFactory->create(IdentityDocumentType::class, new IdentityDocument(
customerBirthDate: $birthDate,
existingFiles: $existingFiles,
));
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$identityDocument = $options['data'];
$existingFiles = $identityDocument->existingFiles;
}
Benefits
- The form type needs no custom options → zero-option form
- Event listeners no longer need parameters captured by the closure
- The factory only creates the data object, not the form itself
2. DataTransformer to Convert Form Objects to Model Objects
The conversion between Symfony's native form objects and application model objects belongs in a DataTransformer, not in the controller.
The controller must only manipulate model objects, never form-internal types.
Problem
foreach ($fieldNames as $fieldName) {
$file = $formData->getFileByFieldName($fieldName);
if ($file instanceof UploadedFile) {
$uploadFile = new UploadFile(
file_get_contents($file->getPathname()),
$file->getClientOriginalName(),
);
}
}
Solution
$builder->get($fieldName)->addModelTransformer(new CallbackTransformer(
static fn (): mixed => null,
static fn (?UploadedFile $file): ?UploadFile => $file instanceof UploadedFile
? new UploadFile(
file_get_contents($file->getPathname()),
$file->getClientOriginalName(),
)
: null,
));
foreach ($formData->getUploadFiles() as $file) {
}
3. property_path for Non-Standard Mappings
Use property_path to map multiple fields to an indexed collection, instead of creating a getter/setter per field.
Problem
final class FormData
{
private ?UploadedFile $frontFile = null;
private ?UploadedFile $backFile = null;
private ?UploadedFile $passportFile = null;
public function getFrontFile(): ?UploadedFile { ... }
public function setFrontFile(?UploadedFile $file): void { ... }
}
Solution
final class FormData
{
private array $uploadFiles = [];
public function getUploadFiles(): array { return $this->uploadFiles; }
public function setUploadFiles(array $files): void { ... }
}
foreach (FieldName::ALL as $fieldName) {
$builder->add($fieldName, FileUploadType::class, [
'property_path' => sprintf('uploadFiles[%s]', $fieldName),
]);
}
Quick Reference
| Rule | Principle |
|---|
Data → data_class | Never in form type options |
| Form → model conversion | DataTransformer, not the controller |
| N fields → 1 collection | property_path to an indexed array |
| Zero-option form | If the data_class carries everything, no custom options |