sulu/sulu-form-bundle
SuluFormBundle adds dynamic form creation to the Sulu admin UI, letting content managers build forms for contact, sweepstake, or other use cases, position fields in a grid, and configure emails to visitors or themselves. Built on Symfony Form, compatible with Sulu 2.0+.
1. Install and register the bundle
composer require sulu/form-bundle
The bundle requires Sulu >= 2.0. Register Sulu\Bundle\FormBundle\SuluFormBundle in config/bundles.php (Sulu apps list bundles explicitly) and import the bundle routing under a prefix so the form submit endpoint exists:
# config/routes/sulu_form.yaml
sulu_form:
resource: "@SuluFormBundle/Resources/config/routing.xml"
prefix: /forms
2. Minimal configuration + schema
At minimum set the mail fallbacks (forms no longer require sender/receiver per form since 2.0, so these are the safety net):
# config/packages/sulu_form.yaml
sulu_form:
mail_from: "%env(SULU_ADMIN_EMAIL)%"
mail_to: "%env(SULU_ADMIN_EMAIL)%"
Forms, translations and submitted form data are Doctrine entities — create the tables with your normal Sulu setup (bin/console sulu:build dev / doctrine:schema:update --force, or a migration in prod). The full config tree (mail templates, ajax templates, recaptcha, honeypot, form data) lives in DependencyInjection/Configuration.php; the human-readable version is Resources/doc/configuration.md.
3. Where to look first
Resources/doc/index.md → installation.md, then the dynamic-form and mailer chapters.DependencyInjection/Configuration.php — the only reliable source of truth for option names in your installed version.UPGRADE.md before any version bump (2.0 renamed services, removed magic getters, changed event names).4. First use case: a contact form
In the admin, open Forms, create a form with a technical key (e.g. contact) — this key, not the numeric id, is what templates and code reference. Add fields (text, email, textarea), then place the Form content type on a page template and render it. Submitted data is written to the form-data table and appears in the admin form-data list; the notify mail goes to the form's sendTo, the confirm mail to the visitor.
Render through the content type (content-manager driven). The Form content type stores the form key; the bundle's website controller builds the Symfony form, handles the POST and sends mails. This is the right default: content managers change fields without a deploy.
Custom controller when you need behaviour. For landing pages or AJAX you build/handle the Symfony form yourself with the bundle services (sulu_form.builder and sulu_form.handler — confirm exact ids in Resources/config/services.xml):
$form = $this->formBuilder->build('contact', $request->getLocale(), ['data' => $prefill]);
if ($this->formHandler->handle($form, $request)) {
return $this->redirectToRoute('app_contact_success'); // PRG, avoid double submit
}
return $this->render('contact.html.twig', ['form' => $form->createView()]);
Inject the services via constructor (2.0 moved controllers to DI — don't use $this->get() in new code).
Hook into the lifecycle instead of forking the bundle. Listen to the save events to push submissions into a CRM, newsletter tool or custom table. Note the 2.0 rename: sulu_form.handler.saved became sulu_form.handler.save (with a post-save event); grep your listeners when upgrading. This is the cleanest extension point for side effects — keep the bundle's persistence untouched.
Static form types for developer-owned forms. Define a Symfony form type in your app, reference its service id/FQCN in the form configuration, and content managers still get the admin UI around it. The FormGeneratorCommand (see bin/console list sulu:form) can scaffold a type from a dynamic form definition — handy for "freeze" migrations where marketing wants a fixed layout.
Mail templates. Forms carry subject/recipient/template settings; the bundle renders them with Twig (Symfony form theming under the hood). Override the mail template per form for branding, or globally in sulu_form for a consistent house style. Verify the variable set exposed to the template (form data, locale) against Resources/views/ before assuming.
Field types and anti-spam. honeypot and recaptcha are ordinary field types — they only protect a form if added to its definition. Recaptcha needs keys and per-field constraints; honeypot should be hidden via CSS, not display:none on a wrapper a bot can detect. Mailchimp "list select" exists for newsletter signups (added in 2.0) and requires its own config.
Multi-locale. Forms are localized entities. Create a form in each locale (or copy the key) — a form existing only in de will not fall back silently on an en page. Always pass the request locale into the builder.
Testing. In test env, disable the mailer transport (null://) and POST to the submit route with KernelBrowser, then assert on the form-data table rather than on the mail, which is the flakiest assertion.
sendTo and no fallback mail_to will happily save and store data but send nothing. Always configure the global mail_from / mail_to and check the mailer log, not just the form config.$form->getTitle() or similar breaks. Use explicit translation accessors.UPGRADE.md, and keep custom code in listeners/form types rather than patching bundle templates so upgrades stay cheap.bin/console debug:container sulu_form --show-private to find the real service ids, debug:event-dispatcher to confirm your listener is wired to sulu_form.handler.save, and the Symfony profiler's mail panel for rendered mails.How can I help you explore Laravel packages today?