sulu/form-bundle
SuluFormBundle lets content managers create dynamic forms directly in Sulu Admin—such as contact or sweepstake forms—position fields in a grid, and configure emails sent to visitors or themselves. Built on the Symfony Form Component and theming features.
Installation:
composer require sulu/form-bundle:^3.0
Ensure your project meets the requirements (PHP 8.2+, Symfony 6.4 / 7.x / 8.0, Sulu 3.0+).
As of 3.0.1 the bundle is compatible with Symfony 8 — if you are still on Symfony 6.4/7.x nothing changes for you, but you can now widen your symfony/* constraints and run composer update without a conflict.
Enable the Bundle:
Add to config/bundles.php:
return [
// ...
Sulu\Form\SuluFormBundle::class => ['all' => true],
];
Run Migrations:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
Generate a Form:
php bin/console sulu:form:generate
This creates a basic form structure in the Sulu Admin UI under Forms.
Note: this is a Symfony/Sulu bundle, not a Laravel package. There is no service provider, no
config/*.phparray config, and no Blade directives. Everything below maps to Symfony concepts (bundles, YAML config, Twig, DI container).
Admin UI Setup:
Text, Email, Submit) in the drag-and-drop editor.Frontend Integration: Embed the form in a Twig template:
{{ render(controller('SuluFormBundle:Form:render', {
'formId': 'your_form_id',
'action': path('sulu_form_submit', {'formId': 'your_form_id'})
})) }}
Handle Submissions: Extend the form handler to process submissions:
// config/services.yaml
Sulu\Form\Handler\FormHandlerInterface: '@App\Service\CustomFormHandler'
Success / Redirect Behaviour: In the Settings tab of the form you can pick a redirect target. Since 3.0.1 external redirect links are resolved through Sulu's link provider instead of being stored as a raw URL string. In practice: configure the target via the link picker (internal page, external URL, or any registered custom link provider) and the bundle resolves it to a URL at render/submit time. This means custom link providers you already registered for your website are now usable as form redirect targets for free.
Field Configuration:
Text, Checkbox, Media).required, email) via the UI.Email Notifications:
{{ formData.name }} to dynamically populate emails.# config/packages/sulu_form.yaml
sulu_form:
sendinblue:
enabled: true
api_key: '%env(SENDINBLUE_API_KEY)%'
Form Submission Handling:
namespace App\Service;
use Sulu\Form\Handler\FormHandlerInterface;
use Sulu\Form\Handler\FormHandlerResult;
use Sulu\Form\Handler\FormHandlerResultInterface;
class CustomFormHandler implements FormHandlerInterface
{
public function handle(array $formData, string $formId): FormHandlerResultInterface
{
// Custom logic (e.g., save to database, trigger events)
return new FormHandlerResult(true, ['message' => 'Form submitted!']);
}
}
# config/services.yaml
App\Service\CustomFormHandler:
decorates: sulu_form.handler.default
arguments: ['@.inner']
services.php) instead of XML. Your project-level overrides keep working, but if you previously reloaded/patched the bundle's XML (e.g. via a compiler pass keyed on file paths or XmlFileLoader), re-check that logic.Frontend Rendering:
{% form_theme form _self %}
{{ form_row(form.field_name) }}
sulu_form_submit route with a CSRF token:
fetch('/form/submit', {
method: 'POST',
body: JSON.stringify({ formData, _csrf_token: '{{ csrf_token('sulu_form_submit') }}' }),
headers: { 'Content-Type': 'application/json' }
});
Media Fields:
Media field type to attach files/images.Localization:
Workflows:
# config/packages/sulu_workflow.yaml
sulu_workflow:
resources:
Sulu\Form\Rest\Resource\FormResource: ~
Link Providers for Redirects:
// tag it as a Sulu link provider; it will appear in the link picker
App\Link\CampaignLinkProvider:
tags: [{ name: 'sulu.link.provider', alias: 'campaign' }]
Testing:
generate-form command to create test forms:
php bin/console sulu:form:generate --name="Test Form" --locale="en"
$client = static::createClient();
$crawler = $client->request('GET', '/form/render');
$form = $crawler->selectButton('Submit')->form([
'formData[email]' => '[email protected]',
]);
$client->submit($form);
cache:clear workarounds.CSRF Token Issues:
# config/packages/sulu_form.yaml
sulu_form:
csrf_protection:
disabled: true
Field Validation:
Hidden) and spacers (Spacer) ignore the required constraint by default.Constraints.Media Uploads:
MediaManagerInterface is properly configured in your services.yaml:
Sulu\Form\Handler\FormHandlerInterface:
arguments:
$mediaManager: '@sulu_media.media_manager'
Locale Fallbacks:
php bin/console debug:translation sulu_form
Upgrading to 3.0.1 (patch, but read this):
$request->get() call was replaced with the proper $request->query / $request->attributes accessors. If you subclassed or copied the bundle's controller code, or have your own controller reading route params via get(), migrate now — Request::get() is deprecated and gone in Symfony 8.Resources/config/*.xml paths in your own imports or build tooling.Form Submission Errors:
FormHandlerResult for errors:
$result = $formHandler->handle($formData, $formId);
if (!$result->isSuccess()) {
throw new \RuntimeException($result->getMessage());
}
config/packages/dev/sulu_form.yaml:
sulu_form:
debug: true
"Stale form" in long-running processes:
Database Issues:
php bin/console doctrine:migrations:status
php bin/console cache:clear
php bin/console debug:container --tag=sulu.link.provider
Sendinblue Integration:
sulu_form:
sendinblue:
pagination:
limit: 100
sulu_form.sendinblue.contact_created event:
$eventDispatcher->addListener(
'sulu_form.sendinblue.contact_created',
function ($event) {
// Log or debug the event data
}
);
Custom Form Types:
Sulu\Form\Type\AbstractType:
namespace App\Form\Type;
use Sulu\Form\Type\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class CustomFieldType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('custom_field', TextType::class);
}
public function getBlockPrefix(): string
{
return 'app_custom_field';
}
}
config/packages/sulu_form.yaml:
sulu_form:
types:
app_custom_field: App\Form\Type\CustomFieldType
Event Listeners:
namespace App\EventListener;
use Sulu\Form\Event\FormSubmittedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FormSubmittedListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
FormSubmittedEvent::NAME => 'onFormSubmitted',
];
}
public function onFormSubmitted(FormSubmittedEvent $event)
{
// Handle submission (e.g., log, notify)
}
}
Custom Link Providers for Redirects (new in 3.0.1):
Twig Extensions:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class FormExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('format_form_data', [$this, 'formatData']),
];
}
public function formatData(array $data): string
{
return json_encode($data, JSON_PRETTY_PRINT);
}
}
config/packages/twig.yaml:
twig:
extensions:
- App\Twig\FormExtension
Service Decoration over Replacement:
decorates: for handlers, mail helpers, and link resolution so bundle upgrades stay painless.How can I help you explore Laravel packages today?