Installation:
composer require ansien/rapid-form-bundle
Ensure your config/bundles.php includes the bundle (auto-registered in Symfony 5.1+).
First Form Class:
Create a DTO class with #[Form] attribute and annotate properties with #[FormField]:
use Ansien\RapidFormBundle\Attribute\Form;
use Ansien\RapidFormBundle\Attribute\FormField;
use Symfony\Component\Form\Extension\Core\Type\TextType;
#[Form]
class UserForm {
#[FormField(TextType::class)]
public ?string $username = null;
}
Controller Integration:
Inject RapidFormBuilderInterface and create the form:
use Ansien\RapidFormBundle\Form\RapidFormBuilderInterface;
class UserController {
public function __construct(private RapidFormBuilderInterface $formBuilder) {}
public function create(Request $request): Response {
$form = $this->formBuilder->create(new UserForm())->handleRequest($request);
// ...
}
}
Twig Rendering: Pass the form view to your template:
{{ form_start(form) }}
{{ form_row(form.username) }}
<button type="submit">Submit</button>
{{ form_end(form) }}
Quick CRUD Form:
For a User entity, create a UserForm DTO with #[FormField] for each field (e.g., TextType, EmailType). The bundle auto-generates the form type, eliminating the need for a separate UserType class.
DTO-First Design:
Define forms as DTOs with #[Form] and #[FormField] attributes. Example:
#[Form]
class LoginForm {
#[FormField(TextType::class, ['label' => 'Email'])]
#[Assert\Email]
public ?string $email = null;
#[FormField(RepeatedType::class, [
'type' => PasswordType::class,
'first_options' => ['label' => 'Password'],
'second_options' => ['label' => 'Repeat Password'],
])]
public ?string $password = null;
}
Nested Forms:
Use #[FormField] with EmbeddedType for nested DTOs:
#[Form]
class ProfileForm {
#[FormField(EmbeddedType::class, ['class' => AddressForm::class])]
public ?AddressForm $address = null;
}
Dynamic Options: Pass closures for dynamic options (e.g., query-based choices):
#[FormField(ChoiceType::class, [
'choices' => fn(EntityManagerInterface $em) => $em->getRepository(User::class)->findAll(),
'choice_label' => 'username',
])]
public ?User $owner = null;
Validation Integration:
Combine Symfony’s #[Assert\*] constraints with form fields:
#[FormField(TextType::class)]
#[Assert\Length(min: 3, max: 20)]
public ?string $nickname = null;
Event Listeners:
Attach form events (e.g., PRE_SET_DATA) via Symfony’s event dispatcher:
$form = $this->formBuilder->create($data)
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$event->getData()->setDefaultValue('default');
});
Custom Form Types:
Extend AbstractType and register it as a service. Reference it in #[FormField]:
#[FormField(MyCustomType::class)]
public ?string $customField = null;
Collection Handling:
Use CollectionType for arrays of DTOs:
#[Form]
class OrderForm {
#[FormField(CollectionType::class, [
'entry_type' => ProductForm::class,
'allow_add' => true,
'allow_delete' => true,
])]
public array $products = [];
}
Form Theming:
Override Twig templates in templates/AnsienRapidFormBundle/ to customize rendering.
Attribute Targets:
Ensure #[FormField] is applied to properties, not methods. The bundle uses reflection to inspect properties only.
// ❌ Won't work (method)
#[FormField(TextType::class)]
public function getName(): string { ... }
// ✅ Works (property)
#[FormField(TextType::class)]
public ?string $name = null;
Circular References:
Avoid circular references in nested forms (e.g., UserForm containing ProfileForm which references UserForm). Use #[Assert\Valid] carefully.
RepeatedType Quirks:
For RepeatedType, ensure the same field name is used in the DTO (e.g., $password and $passwordConfirm). The bundle auto-maps the second field to {field}_confirm.
Validation Order:
Form validation runs after Symfony’s validator. Use #[Assert\Callback] for complex validation that depends on form state.
Dynamic Options Dependencies:
Closures in #[FormField] options (e.g., choices) must accept container services as arguments. Example:
#[FormField(ChoiceType::class, [
'choices' => fn(EntityManagerInterface $em) => $em->getRepository(...),
])]
Form Dump:
Use Symfony’s FormDebugBuilder to inspect the generated form structure:
$form = $this->formBuilder->create($data);
$debugBuilder = new FormDebugBuilder();
$debugBuilder->buildForm($form);
Attribute Reflection: Verify attributes are parsed correctly:
$reflection = new ReflectionClass(UserForm::class);
$attributes = $reflection->getProperty('username')->getAttributes(FormField::class);
Event Debugging: Log form events to trace issues:
$form->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
error_log('Form submitted: ' . print_r($event->getData(), true));
});
Reuse Form Logic:
Create base DTOs with common fields (e.g., BaseForm) and extend them:
#[Form]
class BaseForm {
#[FormField(TextType::class)]
public ?string $createdAt = null;
}
#[Form]
class UserForm extends BaseForm {
#[FormField(TextType::class)]
public ?string $username = null;
}
Partial Updates:
Use data_class in #[Form] to map to an entity:
#[Form(data_class: User::class)]
class UserForm {
#[FormField(TextType::class)]
public ?string $email = null;
}
Then bind the form to an entity:
$form = $this->formBuilder->create(new UserForm(), $userEntity);
Performance: Cache form builders for complex forms:
$form = $this->formBuilder->create($data, [
'cache_key' => 'user_form_' . $user->getId(),
]);
Testing:
Mock RapidFormBuilderInterface in tests:
$builder = $this->createMock(RapidFormBuilderInterface::class);
$builder->method('create')->willReturn($form);
$controller = new UserController($builder);
Symfony 7+:
Leverage Symfony’s new #[AsneakPreview] attributes for future-proofing:
#[Form]
#[AsneakPreview('form_builder_v2')]
class UserForm { ... }
Extension Points: Override the bundle’s compiler pass to customize form generation:
# config/services.yaml
Ansien\RapidFormBundle\Form\Compiler\FormCompilerPass:
tags: [kernel.event_subscriber]
arguments:
$customOptions: ['your_value']
How can I help you explore Laravel packages today?