Forms ===== .. admonition:: Screencast :class: screencast Do you prefer video tutorials? Check out the `Symfony Forms screencast series`_. Creating and processing HTML forms is hard and repetitive. You need to deal with rendering HTML form fields, validating submitted data, mapping the form data into objects and a lot more. Symfony includes a powerful form feature that provides all these features and many more for truly complex scenarios. Installation ------------ In applications using :ref:`Symfony Flex `, run this command to install the form feature before using it: .. code-block:: terminal $ composer require symfony/form Understanding How Forms Work ---------------------------- Before diving into the code, it's helpful to understand the mental model behind Symfony forms. Think of a form as a **bidirectional mapping layer** between your PHP objects (or arrays) and HTML forms. This mapping works in two directions: #. **Object to HTML**: When rendering a form, Symfony reads data from your object and turns it into HTML fields that users can edit; #. **HTML to Object**: When processing a submission, Symfony takes the raw values from the HTTP request (typically strings) and converts them back into the appropriate PHP types on your object. This flow is the core of the Form component. From simple text fields to complex nested collections, everything follows the same pattern. .. _form-data-lifecycle: The Data Transformation Lifecycle ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Data in a form goes through three representations, often called **data layers**: **Model Data** The data in the format your application uses. For example, a ``DateTime`` object, a Doctrine entity, or a custom value object. This is what you pass to ``createForm()`` and what you get back after a successful submission via ``getData()``. **Normalized Data** An intermediate representation that normalizes the model data. For most field types, this is identical to the model data. However, for some types it's different. For example, ``DateType`` is normalized as an array with ``year``, ``month``, and ``day`` keys. **View Data** The format used to populate HTML form fields and received from user submissions. In most cases, this is string-based (or arrays of strings), because browsers submit text. Some fields may use other representations or remain empty for security reasons (for example, file inputs). High-level flow: **Form Rendering** #. Start with model data from your object. #. Model transformers convert it to normalized data. #. View transformers convert it to view data (typically strings). #. Symfony renders the corresponding HTML widgets. **Form Submission** #. Symfony reads raw values from the HTTP request (typically strings). #. View transformers reverse the data into normalized data. #. Model transformers reverse the data into model data. #. The data is written back to the underlying object or array. For a ``DateType`` field configured to render as three ```` form field is a "form type" (e.g. ``TextType``); * a group of several HTML fields used to input a postal address is a "form type" (e.g. ``PostalAddressType``); * an entire ``
`` with multiple fields to edit a user profile is a "form type" (e.g. ``UserProfileType``). This unified concept makes the Form component more **flexible**. You can compose complex forms from simpler types, embed forms within forms, and reuse the same type definition across your application. **The Form Type Hierarchy** Every form type has a parent type. The parent determines the base behavior, options, and rendering that your type inherits. Here's a simplified view: .. code-block:: text FormType (the root parent of all types) ├─ TextType (renders a text input) │ ├─ EmailType │ ├─ PasswordType │ ├─ ... │ └─ UrlType ├─ ChoiceType (renders select, radio, or checkboxes) │ ├─ CountryType │ ├─ EntityType │ └─ ... ├─ DateType (renders single or multiple fields for date input) │ └─ ... └─ ... When you create a custom form type and specify a parent (via ``getParent()``), your type inherits options, template blocks, and behavior from that parent. This is why ``EmailType`` reuses the rendering and options from ``TextType``. There are tens of :doc:`form types provided by Symfony ` and you can also :doc:`create your own form types
`. .. tip:: You can use the ``debug:form`` to list all the available types, type extensions and type guessers in your application: .. code-block:: terminal $ php bin/console debug:form # pass the form type FQCN to only show the options for that type, its parents and extensions # For built-in types, you can pass the short classname instead of the FQCN $ php bin/console debug:form BirthdayType # pass also an option name to only display the full definition of that option $ php bin/console debug:form BirthdayType label_attr Building Forms -------------- Symfony provides a "form builder" object which allows you to describe the form fields using a fluent interface. Later, this builder creates the actual form object used to render and process contents. .. _creating-forms-in-controllers: Creating Forms in Controllers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your controller extends from the :ref:`AbstractController `, use the ``createFormBuilder()`` helper:: // src/Controller/TaskController.php namespace App\Controller; use App\Entity\Task; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { public function new(Request $request): Response { // creates a task object and initializes some data for this example $task = new Task(); $task->setTask('Write a blog post'); $task->setDueDate(new \DateTimeImmutable('tomorrow')); $form = $this->createFormBuilder($task) ->add('task', TextType::class) ->add('dueDate', DateType::class) ->add('save', SubmitType::class, ['label' => 'Create Task']) ->getForm(); // ... } } If your controller does not extend from ``AbstractController``, you'll need to :ref:`fetch services in your controller ` and use the ``createBuilder()`` method of the ``form.factory`` service. In this example, you've added two fields to your form - ``task`` and ``dueDate`` - corresponding to the ``task`` and ``dueDate`` properties of the ``Task`` class. You've also assigned each a :ref:`form type ` (e.g. ``TextType`` and ``DateType``), represented by its fully qualified class name. Finally, you added a submit button with a custom label for submitting the form to the server. .. _creating-forms-in-classes: Creating Form Classes ~~~~~~~~~~~~~~~~~~~~~ Symfony recommends putting as little logic as possible in controllers. That's why it's better to move complex forms to dedicated classes instead of defining them in controller actions. Besides, forms defined in classes can be reused in multiple actions and services. Form classes are :ref:`form types ` that implement :class:`Symfony\\Component\\Form\\FormTypeInterface`. However, it's better to extend from :class:`Symfony\\Component\\Form\\AbstractType`, which already implements the interface and provides some utilities:: // src/Form/Type/TaskType.php namespace App\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('task', TextType::class) ->add('dueDate', DateType::class) ->add('save', SubmitType::class) ; } } .. tip:: Install the `MakerBundle`_ in your project to generate form classes using the ``make:form`` and ``make:registration-form`` commands. The form class contains all the directions needed to create the task form. In controllers extending from the :ref:`AbstractController `, use the ``createForm()`` helper (otherwise, use the ``create()`` method of the ``form.factory`` service):: // src/Controller/TaskController.php namespace App\Controller; use App\Form\Type\TaskType; // ... class TaskController extends AbstractController { public function new(): Response { // creates a task object and initializes some data for this example $task = new Task(); $task->setTask('Write a blog post'); $task->setDueDate(new \DateTimeImmutable('tomorrow')); $form = $this->createForm(TaskType::class, $task); // ... } } .. _form-data-class: Every form needs to know the name of the class that holds the underlying data (e.g. ``App\Entity\Task``). Usually, this is guessed based on the object passed to the second argument to ``createForm()`` (i.e. ``$task``). Later, when you begin :doc:`embedding forms `, this will no longer be sufficient. So, while not always necessary, it's generally a good idea to explicitly specify the ``data_class`` option by adding the following to your form type class:: // src/Form/Type/TaskType.php namespace App\Form\Type; use App\Entity\Task; use Symfony\Component\OptionsResolver\OptionsResolver; // ... class TaskType extends AbstractType { // ... public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => Task::class, ]); } } .. _form-property-path: Mapping Fields to Object Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, a form field named ``dueDate`` reads and writes the ``dueDate`` property on your object. This uses the :doc:`PropertyAccess component `, which can work with public properties and common accessor names (``get*()``, ``is*()``, ``has*()``, ``set*()``). The ``property_path`` option lets you customize this mapping. **Mapping to a Different Property** If your form field name doesn't match the object property:: $builder->add('deadline', DateType::class, [ // this field will read/write the 'dueDate' property 'property_path' => 'dueDate', ]); **Mapping to Nested Properties** You can access nested object properties using dot notation:: // assuming Task::getCategory() returns a Category object with getName()/setName() $builder->add('categoryName', TextType::class, [ 'property_path' => 'category.name', ]); For fields that shouldn't be written back to the underlying data, use :ref:`unampped fields `. .. _form-injecting-services: Injecting Services in Form Classes .................................. Form classes are regular services, which means you can inject other services using :doc:`autowiring `:: // src/Form/Type/TaskType.php namespace App\Form\Type; use App\Repository\CategoryRepository; use Symfony\Component\Form\AbstractType; // ... class TaskType extends AbstractType { public function __construct( private CategoryRepository $categoryRepository, ) { } public function buildForm(FormBuilderInterface $builder, array $options): void { // use $this->categoryRepository to access the repository } } If you're using the :ref:`default services.yaml configuration `, this works automatically. See :doc:`/form/create_custom_field_type` for more information about injecting services in custom form types. .. _rendering-forms: Rendering Forms --------------- Now that the form has been created, the next step is to render it:: // src/Controller/TaskController.php namespace App\Controller; use App\Entity\Task; use App\Form\Type\TaskType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class TaskController extends AbstractController { public function new(Request $request): Response { $task = new Task(); // ... $form = $this->createForm(TaskType::class, $task); return $this->render('task/new.html.twig', [ 'form' => $form, ]); } } Internally, the ``render()`` method calls ``$form->createView()`` to transform the form into a *form view* instance. Then, use some :ref:`form helper functions ` to render the form contents: .. code-block:: twig {# templates/task/new.html.twig #} {{ form(form) }} That's it! The :ref:`form() function ` renders all fields *and* the ``
`` start and end tags. By default, the form method is ``POST`` and the target URL is the same that displayed the form, but :ref:`you can change both `. Notice how the rendered ``task`` input field has the value of the ``task`` property from the ``$task`` object (i.e. "Write a blog post"). This is the first job of a form: to take data from an object and translate it into a format that's suitable for being rendered in an HTML form. .. tip:: The form system is smart enough to access the value of the protected ``task`` property via the ``getTask()`` and ``setTask()`` methods on the ``Task`` class. Unless a property is public, it *must* have a "getter" and "setter" method so that Symfony can get and put data onto the property. For a boolean property, you can use an "isser" or "hasser" method (e.g. ``isPublished()`` or ``hasReminder()``) instead of a getter (e.g. ``getPublished()`` or ``getReminder()``). As short as this rendering is, it's not very flexible. Usually, you'll need more control about how the entire form or some of its fields look. For example, thanks to the :doc:`Bootstrap 5 integration with Symfony forms ` you can set this option to generate forms compatible with the Bootstrap 5 CSS framework: .. configuration-block:: .. code-block:: yaml # config/packages/twig.yaml twig: form_themes: ['bootstrap_5_layout.html.twig'] .. code-block:: xml bootstrap_5_layout.html.twig .. code-block:: php // config/packages/twig.php use Symfony\Config\TwigConfig; return static function (TwigConfig $twig): void { $twig->formThemes(['bootstrap_5_layout.html.twig']); // ... }; The :ref:`built-in Symfony form themes ` include Bootstrap 3, 4 and 5, Foundation 5 and 6, as well as Tailwind 2. You can also :ref:`create your own Symfony form theme `. In addition to form themes, Symfony allows you to :doc:`customize the way fields are rendered ` with multiple functions to render each field part separately (widgets, labels, errors, help messages, etc.) .. _processing-forms: Processing Forms ---------------- The :ref:`recommended way of processing forms ` is to use a single action for both rendering the form and handling the form submit. You can use separate actions, but using one action simplifies everything while keeping the code concise and maintainable. Processing a form means to translate user-submitted data back to the properties of an object. To make this happen, the submitted data from the user must be written into the form object:: // src/Controller/TaskController.php // ... use Symfony\Component\HttpFoundation\Request; class TaskController extends AbstractController { public function new(Request $request): Response { // set up a fresh $task object (remove the example data) $task = new Task(); $form = $this->createForm(TaskType::class, $task); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // $form->getData() holds the submitted values // but, the original `$task` variable has also been updated $task = $form->getData(); // ... perform some action, such as saving the task to the database return $this->redirectToRoute('task_success'); } return $this->render('task/new.html.twig', [ 'form' => $form, ]); } } This controller follows a common pattern for handling forms and has three possible paths: #. When initially loading the page in a browser, the form hasn't been submitted yet and ``$form->isSubmitted()`` returns ``false``. So, the form is created and rendered; #. When the user submits the form, :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` recognizes this and immediately writes the submitted data back into the ``task`` and ``dueDate`` properties of the ``$task`` object. Then this object is validated (validation is explained in the next section). If it is invalid, :method:`Symfony\\Component\\Form\\FormInterface::isValid` returns ``false`` and the form is rendered again, but now with validation errors. By passing ``$form`` to the ``render()`` method (instead of ``$form->createView()``), the response code is automatically set to `HTTP 422 Unprocessable Content`_. This ensures compatibility with tools relying on the HTTP specification, like `Symfony UX Turbo`_; #. When the user submits the form with valid data, the submitted data is again written into the form, but this time :method:`Symfony\\Component\\Form\\FormInterface::isValid` returns ``true``. Now you have the opportunity to perform some actions using the ``$task`` object (e.g. persisting it to the database) before redirecting the user to some other page (e.g. a "thank you" or "success" page); .. note:: Redirecting a user after a successful form submission is a best practice that prevents the user from being able to hit the "Refresh" button of their browser and re-post the data. Accessing Form Data ~~~~~~~~~~~~~~~~~~~ You'll use the ``getData()`` method most often to access the form's data, but Symfony forms also provide methods to access data at :ref:`each layer `: ``getData()`` Returns the **model data**. This is the method you'll use most often. After submission, it returns the populated object (or array) with all the submitted values transformed into their proper PHP types. ``getNormData()`` Returns the **normalized data**. Useful when debugging transformer issues or when you need the intermediate representation. ``getViewData()`` Returns the **view data**. This is what gets rendered into HTML fields and what comes back from user submissions (before transformation). .. seealso:: When adding :ref:`extra fields ` to the form, you can also use the ``getExtraData()`` method to get any submitted data that doesn't correspond to a form field. Example showing these methods in action:: // after form submission $form->handleRequest($request); if ($form->isSubmitted()) { // the populated Task object $task = $form->getData(); // for a DateType field, this might be ['year' => 2024, 'month' => 6, ...] $normData = $form->get('dueDate')->getNormData(); // the raw submitted values (usually strings): ['year' => '2024', 'month' => '6', ...] $viewData = $form->get('dueDate')->getViewData(); } If a transformer fails, the form (or the affected field) may be marked as not synchronized. Check ``isSynchronized()`` and inspect field errors to understand what went wrong. .. _processing-forms-submit-method: Using the submit() Method ~~~~~~~~~~~~~~~~~~~~~~~~~ The :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` method is the recommended way to process forms. However, you can also use the :method:`Symfony\\Component\\Form\\FormInterface::submit` method for finer control over when exactly your form is submitted and what data is passed to it:: use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; // ... public function new(Request $request): Response { $task = new Task(); $form = $this->createForm(TaskType::class, $task); if ($request->isMethod('POST')) { $form->submit($request->getPayload()->get($form->getName())); if ($form->isSubmitted() && $form->isValid()) { // perform some action... return $this->redirectToRoute('task_success'); } } return $this->render('task/new.html.twig', [ 'form' => $form, ]); } The list of fields submitted with the ``submit()`` method must be the same as the fields defined by the form class. Otherwise, you'll see a form validation error:: public function new(Request $request): Response { // ... if ($request->isMethod('POST')) { // '$json' represents payload data sent by React/Angular/Vue // the merge of parameters is needed to submit all form fields $form->submit(array_merge($json, $request->getPayload()->all())); // ... } // ... } .. tip:: Forms consisting of nested fields expect an array in :method:`Symfony\\Component\\Form\\FormInterface::submit`. You can also submit individual fields by calling :method:`Symfony\\Component\\Form\\FormInterface::submit` directly on the field:: $form->get('firstName')->submit('Fabien'); .. tip:: When submitting a form via a "PATCH" request, you may want to update only a few submitted fields. To achieve this, you may pass an optional second boolean argument to ``submit()``. Passing ``false`` will remove any missing fields within the form object. Otherwise, the missing fields will be set to ``null``. .. warning:: When the second parameter ``$clearMissing`` is ``false``, like with the "PATCH" method, the validation will only apply to the submitted fields. If you need to validate all the underlying data, add the required fields manually so that they are validated:: // 'email' and 'username' are added manually to force their validation $form->submit(array_merge( ['email' => null, 'username' => null], $request->getPayload()->all() ), false); .. _processing-forms-multiple-buttons: Handling Multiple Submit Buttons ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When your form contains more than one submit button, you'll want to check which of the buttons was clicked to adapt the program flow in your controller. For example, if you add a second button with the caption "Save and Add" to your form:: $form = $this->createFormBuilder($task) ->add('task', TextType::class) ->add('dueDate', DateType::class) ->add('save', SubmitType::class, ['label' => 'Create Task']) ->add('saveAndAdd', SubmitType::class, ['label' => 'Save and Add']) ->getForm(); In your controller, use the button's :method:`Symfony\\Component\\Form\\ClickableInterface::isClicked` method for querying if the "Save and Add" button was clicked:: if ($form->isSubmitted() && $form->isValid()) { // ... perform some action, such as saving the task to the database $nextAction = $form->get('saveAndAdd')->isClicked() ? 'task_new' : 'task_success'; return $this->redirectToRoute($nextAction); } Alternatively you can use the :method:`Symfony\\Component\\Form\\Form::getClickedButton` method to get the clicked button's name:: if ($form->getClickedButton() && 'saveAndAdd' === $form->getClickedButton()->getName()) { // ... } // when using nested forms, two or more buttons can have the same name; // in those cases, compare the button objects instead of the button names if ($form->getClickedButton() === $form->get('saveAndAdd')) { // ... } .. _validating-forms: Validating Forms ---------------- In the previous section, you learned how a form can be submitted with valid or invalid data. In Symfony, the question isn't whether the "form" is valid, but whether or not the underlying object (``$task`` in this example) is valid after the form has applied the submitted data to it. Calling ``$form->isValid()`` is a shortcut that asks the ``$task`` object whether or not it has valid data. Before using validation, add support for it in your application: .. code-block:: terminal $ composer require symfony/validator Validation is done by adding a set of rules, called (validation) constraints, to a class. You can add them either to the entity class or by using the :ref:`constraints option ` of form types. To see the first approach - adding constraints to the entity - in action, add the validation constraints, so that the ``task`` field cannot be empty, and the ``dueDate`` field cannot be empty, and must be a valid ``DateTimeImmutable`` object. .. configuration-block:: .. code-block:: php-attributes // src/Entity/Task.php namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; class Task { #[Assert\NotBlank] public string $task; #[Assert\NotBlank] #[Assert\Type(\DateTimeInterface::class)] protected \DateTimeInterface $dueDate; } .. code-block:: yaml # config/validator/validation.yaml App\Entity\Task: properties: task: - NotBlank: ~ dueDate: - NotBlank: ~ - Type: \DateTimeInterface .. code-block:: xml \DateTimeInterface .. code-block:: php // src/Entity/Task.php namespace App\Entity; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Mapping\ClassMetadata; class Task { // ... public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('task', new Assert\NotBlank()); $metadata->addPropertyConstraint('dueDate', new Assert\NotBlank()); $metadata->addPropertyConstraint( 'dueDate', new Assert\Type(\DateTimeInterface::class) ); } } That's it! If you re-submit the form with invalid data, you'll see the corresponding errors printed out with the form. To see the second approach - adding constraints to the form - refer to :ref:`this section `. Both approaches can be used together. .. _form-disabling-validation: Disabling Validation ~~~~~~~~~~~~~~~~~~~~ Sometimes it's useful to suppress the validation of a form altogether. For these cases, set the ``validation_groups`` option to ``false``:: use Symfony\Component\OptionsResolver\OptionsResolver; public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'validation_groups' => false, ]); } Note that when you do that, the form will still run basic integrity checks, for example whether an uploaded file was too large or whether non-existing fields were submitted. The submission of extra form fields can be controlled with the :ref:`allow_extra_fields config option ` and the maximum upload file size should be handled via your PHP and web server configuration. You can also disable validation for specific submit buttons using ``'validation_groups' => false``. This is useful in multi-step forms when you want a "Previous" button to save data without running validation:: $form = $this->createFormBuilder($task) // ... ->add('nextStep', SubmitType::class) ->add('previousStep', SubmitType::class, [ 'validation_groups' => false, ]) ->getForm(); The form will still validate basic integrity constraints even when clicking "previousStep". Other Common Form Features -------------------------- Passing Options to Forms ~~~~~~~~~~~~~~~~~~~~~~~~ If you :ref:`create forms in classes `, when building the form in the controller you can pass custom options to it as the third optional argument of ``createForm()``:: // src/Controller/TaskController.php namespace App\Controller; use App\Form\Type\TaskType; // ... class TaskController extends AbstractController { public function new(): Response { $task = new Task(); // use some PHP logic to decide if this form field is required or not $dueDateIsRequired = ...; $form = $this->createForm(TaskType::class, $task, [ 'require_due_date' => $dueDateIsRequired, ]); // ... } } If you try to use the form now, you'll see an error message: *The option "require_due_date" does not exist.* That's because forms must declare all the options they accept using the ``configureOptions()`` method:: // src/Form/Type/TaskType.php namespace App\Form\Type; use Symfony\Component\OptionsResolver\OptionsResolver; // ... class TaskType extends AbstractType { // ... public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ // ..., 'require_due_date' => false, ]); // you can also define the allowed types, allowed values and // any other feature supported by the OptionsResolver component $resolver->setAllowedTypes('require_due_date', 'bool'); } } Now you can use this new form option inside the ``buildForm()`` method:: // src/Form/Type/TaskType.php namespace App\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\FormBuilderInterface; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder // ... ->add('dueDate', DateType::class, [ 'required' => $options['require_due_date'], ]) ; } // ... } Form Type Options ~~~~~~~~~~~~~~~~~ Each :ref:`form type ` has a number of options to configure it, as explained in the :doc:`Symfony form types reference `. Two commonly used options are ``required`` and ``label``. The ``required`` Option ....................... The most common option is the ``required`` option, which can be applied to any field. By default, this option is set to ``true``, meaning that HTML5-ready browsers will require you to fill in all fields before submitting the form. If you don't want this behavior, either :ref:`disable client-side validation ` for the entire form or set the ``required`` option to ``false`` on one or more fields:: ->add('dueDate', DateType::class, [ 'required' => false, ]) The ``required`` option does not perform any server-side validation. If a user submits a blank value for the field (either with an old browser or a web service, for example), it will be accepted as a valid value unless you also use Symfony's ``NotBlank`` or ``NotNull`` validation constraints. The ``label`` Option .................... By default, the label of form fields are the *humanized* version of the property name (``user`` -> ``User``; ``postalAddress`` -> ``Postal Address``). Set the ``label`` option on fields to define their labels explicitly:: ->add('dueDate', DateType::class, [ // set it to FALSE to not display the label for this field 'label' => 'To Be Completed Before', ]) .. tip:: By default, ``