From Backend UI to Symfony Forms: Building Complex, Reusable Forms in Contao
Stop writing PHP form boilerplate in Contao. Use the Form Generator to declare fields, validation, and actions from the backend, and extend it with custom field types when needed. This guide walks through a multi‑step event registration form and explains when you’ll need to drop into Symfony code.
04 Dec 2025, 05:15 UTC

Why you should stop writing PHP form code by hand
In Contao, many developers still create custom Symfony forms directly in PHP to add validation, file uploads, or email notifications. The result is repetitive boilerplate, hard‑to‑maintain code, and a steep learning curve for new team members. Contao’s built‑in Form Generator solves this by letting you declare a form entirely from the backend UI, while the system automatically generates a Symfony form class behind the scenes. This approach speeds up development, guarantees consistency, and keeps your codebase clean.
Section 1: The Form Generator UI – A Declarative Interface
Once you enable the Form Generator module in Contao 5.x, navigating to System → Forms opens a wizard‑style editor. Here you can:
- Define fields – choose from a list of field types (text, email, checkbox, file upload, etc.). Each field maps to a Symfony form type.
- Group fields – create fieldsets to organize the layout.
- Configure validation – set required flags, regex patterns, or custom validators.
- Set submit actions – add email notifications, file storage, or redirect URLs.
- Add conditional logic – show/hide fields based on previous answers.
The UI is intentionally simple: you drag, drop, and type. Behind the scenes, Contao writes a PHP class in /var/cache/dev/appContaoFormGenerator.php that extends Symfony\\Component\\Form\\AbstractType. You can inspect that file to see the generated code, which is useful for debugging or for learning how the generator works.
Section 2: Extending the Generator – Custom Field Types and Validation
While the default field set covers most use cases, you may need a field that isn’t shipped with Contao. Extending the generator is straightforward thanks to Contao’s extension system. The process is:
- Create a new bundle that implements
Contao\\FormExtensionInterface. - Register your custom field type by returning an array of
Contao\\FormFieldTypeobjects ingetFormFieldTypes(). - Define the Symfony form type class for your field.
- Optionally add a custom validation constraint.
Example: adding a colorPicker field.
// src/Extension/ColorPickerExtension.php
namespace App\\Extension;
use Contao\\FormExtensionInterface;
use Contao\\FormFieldType;
class ColorPickerExtension implements FormExtensionInterface
{
public function getFormFieldTypes(): array
{
return [
new FormFieldType(
'colorPicker',
'Color Picker',
'App\\Form\\Type\\ColorPickerType',
'color_picker.svg'
),
];
}
}
After clearing the cache, the new field appears in the Form Generator UI. You can then use it like any other field, and the generated Symfony form will include the custom type automatically.
Worked Example: Multi‑Step Event Registration Form
Let’s walk through creating a three‑step form that collects user details, event preferences, and a profile picture. The form will:
- Upload a profile image (file field).
File size limited to 2 MB, only JPEG/PNG allowed. - Show a conditional field – if the user selects “VIP”, an extra text area appears for special requests.
- Send an email confirmation upon submission.
Steps in the backend:
- Open
System → Forms → New Form. - Give it a name:
Event Registration. - Add a File Upload field:
- Label:
Profile Picture - File size limit:
2 - Allowed extensions:
jpg,png - Required:
Yes
- Label:
- Add a Text field for
Full Nameand anEmailfield for contact. - Add a Checkbox field named
VIPwith labelVIP Ticket. - Add a Text Area field named
Special Requests. - Under
Conditional LogicforSpecial Requests, setShow if VIP is checked. - Under
Submit Actions, add anEmailaction:- Recipient:
{{email}}(the user’s email field). - Subject:
Registration Confirmation for {{fullName}} - Template: use a simple plain text template or point to a custom Twig file.
- Recipient:
- Save and publish the form.
Once published, the form appears on the frontend via the form page type. When a user submits, Contao writes the file to files/, triggers the email, and validates all fields automatically. No PHP code was written for validation or file handling.
Trade‑off: When the Generator Falls Short
Contao’s Form Generator covers the majority of typical use cases, but there are scenarios where you’ll need to drop into custom Symfony code:
- Complex JavaScript interactions – e.g., a date picker that loads options via AJAX based on a previous selection. The generator can add a
textfield, but the dynamic logic must be added manually through a frontend bundle. - Non‑standard data persistence – if you need to store form data in a custom database table or trigger external APIs, you’ll need to write a custom form handler or extend the form event listeners.
- Advanced validation logic – while you can add custom constraints, the generator’s UI for complex rules is limited. Writing a validator class gives full control.
In these cases, you can still start with the generator to scaffold the form and then override or extend the generated Symfony class in src/Form/Type or by creating a custom bundle that listens to FormEvents::POST_SUBMIT.
Actionable Closing – Try It Today
1. Enable the Form Generator module in your Contao 5.x installation.
2. Create a simple test form with a text field and an email action.
3. Inspect the generated class at /var/cache/dev/appContaoFormGenerator.php.
4. If you need a custom field, follow the extension guide above and clear the cache.
5. Deploy the form on a staging page, submit a test entry, and verify the email and file upload.
By leveraging Contao’s Form Generator, you can shift focus from boilerplate to business logic, keep your codebase DRY, and still have the option to extend when the built‑in features aren’t enough.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.