Generate Symfony forms directly from your DTOs using PHP attributes - no more hand-writing FormType classes.
FormTypes are mostly derived information. Your property types determine the form type, nullability determines required, and validator constraints already encode things like email format, length bounds, and file types.
FormsBundle inspects your DTO's public properties, type hints, and attributes to automatically generate and cache fully functional Symfony forms.
Declare the form once on the DTO and skip the hand-written Symfony FormType.
use Fasano\FormsBundle\Attribute\Form;
use Fasano\FormsBundle\Attribute\Field;
use Symfony\Component\Validator\Constraints as Assert;
#[Form\Options(action: 'app.contact', method: 'POST')]
#[Form\Button(label: 'Request')]
class ContactRequest
{
#[Assert\Length(min: 2, max: 100)]
public string $name;
#[Assert\Email]
public string $email;
#[Assert\NotBlank]
#[Assert\Length(max: 1000)]
#[Field\Type(TextareaType::class)]
public string $message;
}Only your DTO remains - the bundle removes the extra boilerplate.
use App\Dto\ContactRequest;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Validator\Constraints\Valid;
class ContactFormType extends AbstractType
{
public function __construct(
private UrlGeneratorInterface $urlGenerator
) {}
public function buildForm(
FormBuilderInterface $builder,
array $options
): void {
$builder
->add('name', TextType::class, [
'attr' => ['minlength' => 2, 'maxlength' => 100],
])
->add('email', EmailType::class)
->add('message', TextareaType::class, [
'attr' => ['maxlength' => 1000],
])
->add('submit', SubmitType::class, [
'label' => 'Request',
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'action' => $this->urlGenerator->generate('app.contact'),
'method' => 'POST',
'data_class' => ContactRequest::class,
]);
}
}composer require fasano/forms-bundleRegister the bundle (if not using Symfony Flex):
// config/bundles.php
return [
// ...
Fasano\FormsBundle\FormsBundle::class => ['all' => true],
];use Fasano\FormsBundle\Attribute\Form;
use Fasano\FormsBundle\Attribute\Field;
use Symfony\Component\Validator\Constraints as Assert;
#[Form\Options(action: 'app.contact', method: 'POST')]
#[Form\Button(label: 'Request')]
class ContactRequest
{
#[Assert\Length(min: 2, max: 100)]
public string $name;
#[Assert\Email]
public string $email;
#[Assert\NotBlank]
#[Assert\Length(max: 1000)]
#[Field\Type(TextareaType::class)]
public string $message;
}use Fasano\FormsBundle\FormTypeFactory;
#[Route('/contact', name: 'app.contact', methods: ['GET', 'POST'])]
class ContactController extends AbstractController
{
public function __construct(
private FormTypeFactory $formFactory,
) {}
public function __invoke(Request $request): Response
{
$form = $this->formFactory->createForm(ContactRequest::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData(); // <- ContactRequest
// ...
}
return $this->render('contact.html.twig', ['form' => $form]);
}
}That's it - no ContactFormType class needed.
FormTypeFactory reflects on your DTO, generates an AbstractType class, and caches it into %kernel.cache_dir%/forms/. The generated form type is built by deriving information from four data sources in sequence:
| Layer | Source | Derived information |
|---|---|---|
| Typesystem | PHP types | input type, label, required |
| Constraints | #[Assert\...] attributes |
input type, HTML attributes |
| TypeDocs | #[Name], #[Description], #[Example] |
label, help, placeholder |
| FormsBundle | #[Field\...], #[Form\...] attributes |
anything, always wins |
Caching is disabled when APP_DEBUG is on, so forms regenerate on every request during development.
Reference
- Features - type inference, nested DTOs, available attributes, constraint enrichment
- Attributes - full
#[Form\...]and#[Field\...]reference with parameters and examples
Integrations
- Symfony Validator - how constraints drive type selection and HTML attributes (72 constraints documented)
- TypeDocs - automatic labels, help text, and placeholders from type annotations
- PHPrimitives - value object support with automatic data transformers
Going deeper
- Extensibility - custom field configurators and constraint configurators
