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 php-prg-pattern), form validation rules, general Symfony questions.
From phpnpx claudepluginhub fabiensalles/claude-marketplace --plugin phpThis skill uses the workspace's default tool permissions.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Designs KPI dashboards with metrics selection (MRR, churn, LTV/CAC), visualization best practices, real-time monitoring, and hierarchy for executives, operations, and product teams.
Transforms raw data into narratives with story structures, visuals, and frameworks for executive presentations, analytics reports, and stakeholder communications.
data_class Is the Single Source of TruthAll form data must flow through the data_class, not through options.
Options configure the form type behavior (labels, choices, validation rules), not transport data.
// ❌ AVOID - Data passed as options
$this->formFactory->create(IdentityDocumentType::class, $data, [
'customer_birth_date' => $birthDate,
'existing_files' => $existingFiles,
]);
// In configureOptions:
$resolver->setRequired('customer_birth_date');
$resolver->setRequired('existing_files');
// ✅ CORRECT - Everything is in the data_class
$this->formFactory->create(IdentityDocumentType::class, new IdentityDocument(
customerBirthDate: $birthDate,
existingFiles: $existingFiles,
));
// The FormType accesses data via $options['data']
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$identityDocument = $options['data'];
$existingFiles = $identityDocument->existingFiles;
}
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.
// ❌ AVOID - Conversion in the controller
foreach ($fieldNames as $fieldName) {
$file = $formData->getFileByFieldName($fieldName);
if ($file instanceof UploadedFile) {
$uploadFile = new UploadFile(
file_get_contents($file->getPathname()),
$file->getClientOriginalName(),
);
}
}
// ✅ CORRECT - DataTransformer in the FormType
$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,
));
// The controller only manipulates model objects
foreach ($formData->getUploadFiles() as $file) {
// $file is already a model object
}
property_path for Non-Standard MappingsUse property_path to map multiple fields to an indexed collection, instead of creating a getter/setter per field.
// ❌ AVOID - One property per file
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 { ... }
// ... x3 getters/setters
}
// ✅ CORRECT - A collection with property_path
final class FormData
{
private array $uploadFiles = [];
public function getUploadFiles(): array { return $this->uploadFiles; }
public function setUploadFiles(array $files): void { ... }
}
// In the FormType:
foreach (FieldName::ALL as $fieldName) {
$builder->add($fieldName, FileUploadType::class, [
'property_path' => sprintf('uploadFiles[%s]', $fieldName),
]);
}
| 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 |