fasano / forms-bundle
Generate Symfony forms from your DTOs.
Requires
- php: >=8.4
- fasano/phprimitives: ^1.0
- fasano/typedocs: ^1.0
- symfony/config: ^8.0
- symfony/dependency-injection: ^8.0
- symfony/form: ^8.0
- symfony/http-kernel: ^8.0
- symfony/intl: ^8.0
- symfony/routing: ^8.0
- symfony/validator: ^8.0
Requires (Dev)
- phpunit/phpunit: ^13.0
- symfony/translation: ^8.0
- symfony/twig-bridge: ^8.0
- symfony/yaml: ^8.0
- twig/twig: ^3.24
Suggests
- symfony/mime: Required when using Assert\File with mimeTypes or Assert\Image constraints.
README
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.
Same form. Less boilerplate.
Declare the form once on the DTO and skip the hand-written Symfony FormType.
✅ With the bundle
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.
❌ Without the bundle
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], 'required' => true, 'label' => 'Name', ]) ->add('email', EmailType::class, [ 'required' => true, 'label' => 'Email', ]) ->add('message', TextareaType::class, [ 'attr' => ['maxlength' => 1000], 'required' => true, 'label' => 'Message', ]) ->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, 'constraints' => [new Valid()], 'empty_data' => fn () => new ContactRequest('', '', null), ]); } }
Quick Start
1. Install the bundle
composer require fasano/forms-bundle
Register the bundle (if not using Symfony Flex):
// config/bundles.php return [ // ... Fasano\FormsBundle\FormsBundle::class => ['all' => true], ];
2. Define a DTO
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; }
3. Create and use the form
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.
How It Works
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.
Documentation
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
