I’m working on a Sulu CMS project. I was asked to use the FormBundle and HeadlessBundle to create a contact form accessible at /contact
.
I’ve set up the form, template, and page in the Sulu Admin UI, and I can submit data via POST to /contact
(or /contact.json
for JSON). The submissions are saved in the database (visible at Forms > Dynamic Form > Data), but Sulu did not return success or error messages in the response, which is critical for frontend feedback.
My requirements:
x-www-form-urlencoded
(for simpler integrations) and JSON payloads (for headless JavaScript frontends).formId: 1
) without affecting other forms.I’ve struggled with Sulu’s sparse documentation and found no clear examples for custom form handling with JSON. How can I extend Sulu’s FormBundle to meet these needs? Are there specific Symfony components (e.g., event listeners) I should use? Any guidance or examples would be appreciated!
To handle dynamic form submissions in Sulu CMS with x-www-form-urlencoded
or JSON payloads, including success/error messages, you can extend Sulu’s RequestListener
with a custom EventListener
. I developed a solution after hours and hours of trial and error, addressing the lack of documentation for Sulu’s FormBundle in headless scenarios. Below, I share a concise implementation for both submission types, with links to detailed guides.
Sulu’s FormBundle saves submissions but doesn’t provide response feedback. I created a SpecialFormHandler
that:
Listens to kernel.request
and kernel.response
events.
Validates requests for the contact form (formId: 1
).
Returns JSON responses (HTTP 201 for success, 400 for errors).
Preserves Sulu’s default behavior for other forms.
The solution supports:
x-www-form-urlencoded: For traditional form submissions to /contact
.
JSON: For headless submissions to /contact.json
with robust validation.
Sulu CMS with FormBundle and HeadlessBundle.
Contact form set up in the Admin UI (see Setting Up Dynamic Forms).
Symfony knowledge.
Use a SpecialFormHandler
to process POST requests to /contact
.
Core Code (src/EventListener/SpecialFormHandler.php
):
namespace App\EventListener;
use App\Services\Form\CustomRequestValidator as RequestValidator;
use Sulu\Bundle\FormBundle\Configuration\FormConfigurationFactory;
use Sulu\Bundle\FormBundle\Entity\Dynamic;
use Sulu\Bundle\FormBundle\Event\RequestListener as BaseRequestListener;
use Sulu\Bundle\FormBundle\Form\BuilderInterface;
use Sulu\Bundle\FormBundle\Form\HandlerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
class SpecialFormHandler extends BaseRequestListener
{
private $formValidator;
private $processed = false;
public function __construct(
BuilderInterface $formBuilder,
HandlerInterface $formHandler,
FormConfigurationFactory $formConfigurationFactory,
EventDispatcherInterface $eventDispatcher,
RequestValidator $formValidator
) {
parent::__construct($formBuilder, $formHandler, $formConfigurationFactory, $eventDispatcher);
$this->formValidator = $formValidator;
}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
if (!$this->shouldProcess($request)) {
parent::onKernelRequest($event);
return;
}
try {
$form = $this->formBuilder->buildByRequest($request);
if (!$form || !$form->isSubmitted()) {
return;
}
$formEntity = $form->getConfig()->getOption('formEntity');
if (!$this->formValidator->isRegisteredForm($formEntity->getId())) {
return;
}
if (!$form->isValid()) {
$request->attributes->set('_special_form', [
'errors' => $this->getFormErrors($form),
'form_id' => $formEntity->getId()
]);
$this->processed = true;
return;
}
$dynamic = $form->getData();
$locale = $dynamic->getLocale() ?? 'en';
$configuration = $this->formConfigurationFactory->buildByDynamic($dynamic);
$dynamic->setLocale($locale);
if ($this->formHandler->handle($form, $configuration)) {
$translation = $formEntity->getTranslation($locale);
$request->attributes->set('_special_form', [
'success' => true,
'message' => $translation->getSuccessText() ?? 'Thank you!',
'form_id' => $formEntity->getId()
]);
$this->processed = true;
}
} catch (\Exception $e) {
$request->attributes->set('_special_form', [
'error' => $e->getMessage(),
'form_id' => $formEntity->getId() ?? null
]);
$this->processed = true;
}
}
public function onKernelResponse(ResponseEvent $event): void
{
if (!$this->processed) {
return;
}
$request = $event->getRequest();
$formData = $request->attributes->get('_special_form');
if (isset($formData['errors'])) {
$event->setResponse(new JsonResponse([
'success' => false,
'form_type' => $this->formValidator->getFormType($formData['form_id']),
'errors' => $formData['errors']
], 400));
} elseif (isset($formData['error'])) {
$event->setResponse(new JsonResponse([
'success' => false,
'form_type' => $this->formValidator->getFormType($formData['form_id']),
'error' => 'Form processing failed'
], 500));
} elseif (isset($formData['success'])) {
$event->setResponse(new JsonResponse([
'success' => true,
'form_type' => $this->formValidator->getFormType($formData['form_id']),
'message' => $formData['message']
], 201));
}
}
private function shouldProcess(Request $request): bool
{
return $request->isMethod('POST') && $this->formValidator->isSpecialFormRequest($request);
}
private function getFormErrors($form): array
{
$errors = [];
foreach ($form->getErrors(true) as $error) {
$errors[$error->getOrigin() ? $error->getOrigin()->getName() : '_global'] = $error->getMessage();
}
return $errors;
}
}
Validator (src/Services/Form/CustomRequestValidator.php
):
namespace App\Services\Form;
use Symfony\Component\HttpFoundation\Request;
class CustomRequestValidator
{
private const REGISTERED_FORMS = [1 => 'contact_form'];
private const SPECIAL_FORM_ROUTES = ['/contact'];
public function isSpecialFormRequest(Request $request): bool
{
return $this->isFormApiRoute($request) || $this->containsRegisteredFormId($request);
}
public function isFormApiRoute(Request $request): bool
{
if (!$request->isMethod('POST')) {
return false;
}
$currentPath = $request->getPathInfo();
foreach (self::SPECIAL_FORM_ROUTES as $route) {
if (str_starts_with($currentPath, $route)) {
return true;
}
}
return false;
}
public function containsRegisteredFormId(Request $request): bool
{
try {
$formId = (int) $request->request->get('formId');
return isset(self::REGISTERED_FORMS[$formId]);
} catch (\Exception $e) {
return false;
}
}
public function isRegisteredForm(int $formId): bool
{
return isset(self::REGISTERED_FORMS[$formId]);
}
public function getFormType(int $formId): ?string
{
return self::REGISTERED_FORMS[$formId] ?? null;
}
}
For service configuration and full details, see: Handling Form Submissions with x-www-form-urlencoded.
Example Request:
curl -X POST http://your-site/contact \https://github.com/yaovicoder/sulu-adventures/wiki/Handling-Form-Submissions-with-x-www-form-urlencoded
-H "Content-Type: application/x-www-form-urlencoded" \
-d "formId=1&name=John&email=john@example.com&message=Hello"
Response (HTTP 201):
{"success":true,"form_type":"contact_form","message":"Thank you!"}
For JSON submissions to /contact.json
, enhance SpecialFormHandler
with validators to handle JSON payloads.
Core Code (Key Methods, src/EventListener/SpecialFormHandler.php
):
// Same namespace, imports, and constructor as above, with additional dependencies
use App\Request\JsonRequestProcessor;
use App\Services\Form\FormRequestValidator as FormValidator;
use App\Services\Form\FormRequestPreparator;
use Symfony\Component\HttpFoundation\Response;
class SpecialFormHandler extends BaseRequestListener
{
private $formValidator;
private $formPreparator;
private $jsonProcessor;
private $processed = false;
public function __construct(
BuilderInterface $formBuilder,
HandlerInterface $formHandler,
FormConfigurationFactory $formConfigurationFactory,
EventDispatcherInterface $eventDispatcher,
FormValidator $formValidator,
FormRequestPreparator $formPreparator,
JsonRequestProcessor $jsonProcessor
) {
parent::__construct($formBuilder, $formHandler, $formConfigurationFactory, $eventDispatcher);
$this->formValidator = $formValidator;
$this->formPreparator = $formPreparator;
$this->jsonProcessor = $jsonProcessor;
}
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
if (!$this->formValidator->isFormApiRoute($request)) {
parent::onKernelRequest($event);
return;
}
if (!$this->jsonProcessor->isJsonRequest($request)) {
$request->attributes->set('_special_form', [
'error' => 'Only application/json supported',
'form_id' => null,
'error_code' => Response::HTTP_UNSUPPORTED_MEDIA_TYPE
]);
$this->processed = true;
return;
}
try {
$request = $this->formPreparator->prepareFormRequest($request);
$form = $this->formBuilder->buildByRequest($request);
if (!$form) {
$request->attributes->set('_special_form', ['error' => 'Form could not be built', 'form_id' => null]);
$this->processed = true;
return;
}
$formEntity = $form->getConfig()->getOption('formEntity');
if (!$form->isValid()) {
$request->attributes->set('_special_form', [
'errors' => $this->getFormErrors($form),
'form_id' => $formEntity->getId()
]);
$this->processed = true;
return;
}
$dynamic = $form->getData();
$locale = $dynamic->getLocale() ?? 'en';
$configuration = $this->formConfigurationFactory->buildByDynamic($dynamic);
$dynamic->setLocale($locale);
if ($this->formHandler->handle($form, $configuration)) {
$translation = $formEntity->getTranslation($locale);
$request->attributes->set('_special_form', [
'success' => true,
'message' => $translation->getSuccessText() ?? 'Thank you!',
'form_id' => $formEntity->getId()
]);
$this->processed = true;
}
} catch (\Exception $e) {
$request->attributes->set('_special_form', ['error' => $e->getMessage(), 'form_id' => null]);
$this->processed = true;
}
}
// onKernelResponse and other methods same as x-www-form-urlencoded
}
The JSON solution requires additional validators (FormRequestValidator
, FormRequestPreparator
, etc.). For complete code, see: Enabling Headless JSON Form Submissions.
Example Request:
curl -X POST http://your-site/contact.json \
-H "Content-Type: application/json" \
-d '{"dynamic_form_contact1":{"formId":"1","formName":"form_contact","type":"contact","typeId":"1","checksum":"abc123","locale":"en","name":"John Doe","email":"john@example.com","message":"Hello"}}'
Response (HTTP 201):
{"success":true,"form_type":"contact_form","message":"Thank you!"}
I’ve documented a comprehensive series on Sulu CMS
dynamic form submissions, covering setup, x-www-form-urlencoded
, and JSON handling.
See the index: [Gist].
Full series: on my sulu-adventures wiki.
Use event listener priority 0
to align with Sulu’s FormBundle.
JSON handling requires careful validation of content type and form data.
Sulu’s documentation gaps make community solutions essential.
This approach meets all requirements while maintaining compatibility with Sulu’s default form handling.