Skip to main content
Version: Next

Forms Reference

Thelia's form system is built on Symfony Forms. It handles validation, CSRF protection, and template rendering.

Context-specific documentation

Creating a form​

Create a form class extending BaseForm:

<?php

declare(strict_types=1);

namespace MyModule\Form;

use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Constraints;
use Thelia\Form\BaseForm;

class MyForm extends BaseForm
{
protected function buildForm(): void
{
$this->formBuilder
->add('title', TextType::class, [
'required' => true,
'label' => 'Title',
'constraints' => [
new Constraints\NotBlank(),
new Constraints\Length(['min' => 3, 'max' => 255]),
],
])
->add('quantity', IntegerType::class, [
'required' => true,
'constraints' => [
new Constraints\NotBlank(),
new Constraints\Positive(),
],
]);
}
}

Form naming convention​

The form name is automatically generated from the fully qualified class name:

  • MyModule\Form\ConfigForm → mymodule_form_config_form
  • MyModule\Form\ProductReviewForm → mymodule_form_product_review_form

You can override this with getName(), but it's not recommended.

Validation constraints​

Use Symfony Validator constraints:

use Symfony\Component\Validator\Constraints;

$this->formBuilder
->add('email', TextType::class, [
'constraints' => [
new Constraints\NotBlank(),
new Constraints\Email(),
],
])
->add('quantity', IntegerType::class, [
'constraints' => [
new Constraints\NotBlank(),
new Constraints\Range(['min' => 1, 'max' => 100]),
],
]);

Custom validation with a callback​

use Symfony\Component\Validator\Context\ExecutionContextInterface;

$this->formBuilder
->add('code', TextType::class, [
'constraints' => [
new Constraints\Callback([$this, 'validateUniqueCode']),
],
]);

public function validateUniqueCode(mixed $value, ExecutionContextInterface $context): void
{
$existing = MyModelQuery::create()->findOneByCode($value);

if ($existing !== null) {
$context->addViolation('This code already exists');
}
}

Using forms in controllers​

<?php

declare(strict_types=1);

namespace MyModule\Controller;

use MyModule\Form\MyForm;
use Thelia\Controller\Front\BaseFrontController;

class MyController extends BaseFrontController
{
public function processAction(): mixed
{
$form = $this->createForm(MyForm::getName());

try {
$data = $this->validateForm($form)->getData();

// Process valid form data
$title = $data['title'];
$quantity = $data['quantity'];

// ... business logic

return $this->generateRedirect('/success');

} catch (\Exception $e) {
// Form validation failed
$this->setupFormErrorContext(
'My Form',
$e->getMessage(),
$form
);

return $this->generateRedirect('/form-page');
}
}
}

Available field types​

Thelia supports all Symfony Form Types:

TypeUse Case
TextTypeSingle-line text input
TextareaTypeMulti-line text input
EmailTypeEmail validation
IntegerTypeInteger numbers
NumberTypeDecimal numbers
ChoiceTypeSelect, radio, checkboxes
CheckboxTypeBoolean checkbox
HiddenTypeHidden fields
FileTypeFile uploads

CSRF protection​

Forms include CSRF protection by default. Always include hidden fields in your templates:

Smarty (legacy back-office):

{form name="mymodule_form_my_form"}
<form method="post" action="{url path='/my/action'}">
{form_hidden_fields form=$form}
{* ... form fields ... *}
</form>
{/form}

Twig (front-office and default-twig back-office):

{{ form_start(form) }}
{# CSRF token included automatically #}
{{ form_end(form) }}

Session-bound or stateless tokens​

By default a BaseForm token is bound to the visitor's session: it is generated once, stored in the session and compared on submit. This is the right choice for forms that are always rendered fresh (account pages, checkout steps).

A form rendered inside a cache must not use a session token: a Turbo Drive snapshot, a Varnish page cache, a Twig fragment cache or an ESI block will replay a token that belongs to another session, and the submit fails with The CSRF token is invalid. For those forms, switch to Symfony's stateless validation, which checks the request origin (Sec-Fetch-Site, Origin or Referer) instead of a stored value. A stateless form renders a constant token, so the cached HTML stays valid for every visitor.

Two ways to opt in:

  • pass a stateless token id when creating the form:

    $form = $this->createForm(CartAdd::class, options: ['csrf_token_id' => 'submit']);

    submit, authenticate and logout are declared stateless by the config/packages/csrf.yaml that the framework-bundle recipe installs in every project.

  • or declare the form's own name as stateless, without touching any PHP code. This is how a theme marks the forms it renders in cacheable zones:

    # config/packages/csrf.yaml
    framework:
    csrf_protection:
    stateless_token_ids:
    - thelia_cart_add
    - thelia_coupon_code

Session-bound tokens stay the default. 'csrf_protection' => false still disables the protection entirely; prefer a stateless id over disabling it.

Next steps​