Forms
Warning: You are browsing the documentation for Symfony 3.x, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
Screencast
Do you prefer video tutorials? Check out the Symfony Forms screencast series.
Dealing with HTML forms is one of the most common - and challenging - tasks for a web developer. Symfony integrates a Form component that helps you dealing with forms. In this article, you'll build a complex form from the ground up, learning the most important features of the form library along the way.
Note
The Symfony Form component is a standalone library that can be used outside of Symfony projects. For more information, see the Form component documentation on GitHub.
Creating a Simple Form
Suppose you're building a simple todo list application that will need to
display "tasks". Because your users will need to edit and create tasks, you're
going to need to build a form. But before you begin, first focus on the generic
Task
class that represents and stores the data for a single task:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;
class Task
{
protected $task;
protected $dueDate;
public function getTask()
{
return $this->task;
}
public function setTask($task)
{
$this->task = $task;
}
public function getDueDate()
{
return $this->dueDate;
}
public function setDueDate(\DateTime $dueDate = null)
{
$this->dueDate = $dueDate;
}
}
This class is a "plain-old-PHP-object" because, so far, it has nothing
to do with Symfony or any other library. It's a normal PHP object that
directly solves a problem inside your application (i.e. the need to
represent a task in your application). By the end of this article,
you'll be able to submit data to a Task
instance (via an HTML form), validate
its data and persist it to the database.
Building the Form
Now that you've created a Task
class, the next step is to create and
render the actual HTML form. In Symfony, this is done by building a form
object and then rendering it in a template. For now, this can all be done
from inside a controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;
use AppBundle\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
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;
class DefaultController extends Controller
{
public function newAction(Request $request)
{
// creates a task and gives it some dummy data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));
$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, ['label' => 'Create Task'])
->getForm();
return $this->render('default/new.html.twig', [
'form' => $form->createView(),
]);
}
}
Tip
This example shows you how to build your form directly in the controller. Later, in the "Forms" section, you'll learn how to build your form in a standalone class, which is recommended as your form becomes reusable.
Creating a form requires relatively little code because Symfony form objects are built with a "form builder". The form builder's purpose is to allow you to write simple form "recipes" and have it do all the heavy-lifting of actually building the form.
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 "type" (e.g. TextType
and DateType
),
represented by its fully qualified class name. Among other things, it determines
which HTML form tag(s) is rendered for that field.
Finally, you added a submit button with a custom label for submitting the form to the server.
Symfony comes with many built-in types that will be discussed shortly (see Forms).
Rendering the Form
Now that the form has been created, the next step is to render it. This is
done by passing a special form "view" object to your template (notice the
$form->createView()
in the controller above) and using a set of form
helper functions:
1 2 3 4
{# app/Resources/views/default/new.html.twig #}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
Note
This example assumes that you submit the form in a "POST" request and to the same URL that it was displayed in. You will learn later how to change the request method and the target URL of the form.
That's it! Just three lines are needed to render the complete form:
form_start(form)
- Renders the start tag of the form, including the correct enctype attribute when using file uploads.
form_widget(form)
- Renders all the fields, which includes the field element itself, a label and any validation error messages for the field.
form_end(form)
- Renders the end tag of the form and any fields that have not yet been rendered, in case you rendered each field yourself. This is useful for rendering hidden fields and taking advantage of the automatic CSRF Protection.
See also
As short as this is, it's not very flexible. Usually, you'll want to render each form field individually so you can control how the form looks. You'll learn how to do that in the "How to Control the Rendering of a Form" section.
Before moving on, 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 the Form component 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()
).
Handling Form Submissions
By default, the form will submit a POST request back to the same controller that renders it.
Here, the second job of a form is 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. Add the following functionality to your controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
// ...
use Symfony\Component\HttpFoundation\Request;
public function newAction(Request $request)
{
// just setup a fresh $task object (remove the dummy data)
$task = new Task();
$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, ['label' => 'Create Task'])
->getForm();
$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
// for example, if Task is a Doctrine entity, save it!
// $entityManager = $this->getDoctrine()->getManager();
// $entityManager->persist($task);
// $entityManager->flush();
return $this->redirectToRoute('task_success');
}
return $this->render('default/new.html.twig', [
'form' => $form->createView(),
]);
}
Caution
Be aware that the createView()
method should be called after handleRequest()
is called. Otherwise, changes done in the *_SUBMIT
events aren't applied to the
view (like validation errors).
This controller follows a common pattern for handling forms and has three possible paths:
- When initially loading the page in a browser, the form is created and
rendered. handleRequest()
recognizes that the form was not submitted and does nothing.
isSubmitted() returns
false
if the form was not submitted. - When the user submits the form, handleRequest()
recognizes this and immediately writes the submitted data back into the
task
anddueDate
properties of the$task
object. Then this object is validated. If it is invalid (validation is covered in the next section), isValid() returnsfalse
and the form is rendered again, but now with validation errors; When the user submits the form with valid data, the submitted data is again written into the form, but this time 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 prevents the user from being able to hit the "Refresh" button of their browser and re-post the data.
See also
If you need more control over exactly when your form is submitted or which data is passed to it, you can use the submit() method. Read more about it How to Use the submit() Function to Handle Form Submissions.
Form Validation
In the previous section, you learned how a form can be submitted with valid
or invalid data. In Symfony, validation is applied to the underlying object
(e.g. Task
). In other words, the question isn't whether the "form" is
valid, but whether or not the $task
object 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.
Validation is done by adding a set of rules (called constraints) to a class. To
see this in action, add validation constraints so that the task
field cannot
be empty and the dueDate
field cannot be empty and must be a valid DateTime
object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Task
{
/**
* @Assert\NotBlank
*/
public $task;
/**
* @Assert\NotBlank
* @Assert\Type("\DateTime")
*/
protected $dueDate;
}
1 2 3 4 5 6 7 8
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Task:
properties:
task:
- NotBlank: ~
dueDate:
- NotBlank: ~
- Type: \DateTime
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<!-- src/AppBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
<class name="AppBundle\Entity\Task">
<property name="task">
<constraint name="NotBlank"/>
</property>
<property name="dueDate">
<constraint name="NotBlank"/>
<constraint name="Type">\DateTime</constraint>
</property>
</class>
</constraint-mapping>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/AppBundle/Entity/Task.php
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Mapping\ClassMetadata;
class Task
{
// ...
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint('task', new NotBlank());
$metadata->addPropertyConstraint('dueDate', new NotBlank());
$metadata->addPropertyConstraint(
'dueDate',
new Type(\DateTime::class)
);
}
}
That's it! If you re-submit the form with invalid data, you'll see the corresponding errors printed out with the form.
Validation is a very powerful feature of Symfony and has its own dedicated article.
HTML5 Validation
Thanks to HTML5, many browsers can natively enforce certain validation constraints
on the client side. The most common validation is activated by rendering
a required
attribute on fields that are required. For browsers that
support HTML5, this will result in a native browser message being displayed
if the user tries to submit the form with that field blank.
Generated forms take full advantage of this new feature by adding sensible
HTML attributes that trigger the validation. The client-side validation,
however, can be disabled by adding the novalidate
attribute to the
form
tag or formnovalidate
to the submit tag. This is especially
useful when you want to test your server-side validation constraints,
but are being prevented by your browser from, for example, submitting
blank fields.
1 2 3 4
{# app/Resources/views/default/new.html.twig #}
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
Built-in Field Types
Symfony comes standard with a large group of field types that cover all of the common form fields and data types you'll encounter:
Base Fields
You can also create your own custom field types. See How to Create a Custom Form Field Type for info.
Field Type Options
Each field type has a number of options that can be used to configure it.
For example, the dueDate
field is currently being rendered as 3 select
boxes. However, the DateType can be
configured to be rendered as a single text box (where the user would enter
the date as a string in the box):
1
->add('dueDate', DateType::class, ['widget' => 'single_text'])
Each field type has a number of different options that can be passed to it. Many of these are specific to the field type and details can be found in the documentation for each type.
The required
Option
The most common option is the required
option, which can be applied to
any field. By default, the required
option is set to true
, meaning
that HTML5-ready browsers will apply client-side validation if the field
is left blank. If you don't want this behavior, either
disable HTML5 validation
or set the required
option on your field to false
:
1 2 3 4
->add('dueDate', DateType::class, [
'widget' => 'single_text',
'required' => false
])
Also note that setting the required
option to true
will not
result in server-side validation to be applied. In other words, if a
user submits a blank value for the field (either with an old browser
or web service, for example), it will be accepted as a valid value unless
you use Symfony's NotBlank
or NotNull
validation constraint.
In other words, the required
option is "nice", but true server-side
validation should always be used.
The label
Option
The label for the form field can be set using the label
option,
which can be applied to any field:
1 2 3 4
->add('dueDate', DateType::class, [
'widget' => 'single_text',
'label' => 'Due Date',
])
The label for a field can also be set in the template rendering the
form, see below. If you don't need a label associated to your input,
you can disable it by setting its value to false
.
Field Type Guessing
Now that you've added validation metadata to the Task
class, Symfony
already knows a bit about your fields. If you allow it, Symfony can "guess"
the type of your field and set it up for you. In this example, Symfony can
guess from the validation rules that both the task
field is a normal
TextType
field and the dueDate
field is a DateType
field:
1 2 3 4 5 6 7 8 9 10
public function newAction()
{
$task = new Task();
$form = $this->createFormBuilder($task)
->add('task')
->add('dueDate', null, ['widget' => 'single_text'])
->add('save', SubmitType::class)
->getForm();
}
The "guessing" is activated when you omit the second argument to the add()
method (or if you pass null
to it). If you pass an options array as the
third argument (done for dueDate
above), these options are applied to
the guessed field.
Caution
If your form uses a specific validation group, the field type guesser will still consider all validation constraints when guessing your field types (including constraints that are not part of the validation group(s) being used).
Field Type Options Guessing
In addition to guessing the "type" for a field, Symfony can also try to guess the correct values of a number of field options.
Tip
When these options are set, the field will be rendered with special HTML
attributes that provide for HTML5 client-side validation. However, it
doesn't generate the equivalent server-side constraints (e.g. Assert\Length
).
And though you'll need to manually add your server-side validation, these
field type options can then be guessed from that information.
required
-
The
required
option can be guessed based on the validation rules (i.e. is the fieldNotBlank
orNotNull
) or the Doctrine metadata (i.e. is the fieldnullable
). This is very useful, as your client-side validation will automatically match your validation rules. maxlength
-
If the field is some sort of text field, then the
maxlength
option attribute can be guessed from the validation constraints (ifLength
orRange
is used) or from the Doctrine metadata (via the field's length).
Caution
These field options are only guessed if you're using Symfony to guess
the field type (i.e. omit or pass null
as the second argument to add()
).
If you'd like to change one of the guessed values, you can override it by passing the option in the options field array:
1
->add('task', null, ['attr' => ['maxlength' => 4]])
Creating Form Classes
As you've seen, a form can be created and used directly in a controller. However, a better practice is to build the form in a separate, standalone PHP class, which can then be reused anywhere in your application. Create a new class that will house the logic for building the task form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('dueDate', null, ['widget' => 'single_text'])
->add('save', SubmitType::class)
;
}
}
This new class contains all the directions needed to create the task form. It can be used to quickly build a form object in the controller:
1 2 3 4 5 6 7 8 9 10
// src/AppBundle/Controller/DefaultController.php
use AppBundle\Form\TaskType;
public function newAction()
{
$task = ...;
$form = $this->createForm(TaskType::class, $task);
// ...
}
Placing the form logic into its own class means that the form can be reused elsewhere in your project. This is the best way to create forms, but the choice is ultimately up to you.
Setting the data_class
Every form needs to know the name of the class that holds the underlying
data (e.g. AppBundle\Entity\Task
). Usually, this is just guessed
based off of the object passed to the second argument to createForm()
(i.e. $task
). Later, when you begin 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:
1 2 3 4 5 6 7 8 9 10 11
// src/AppBundle/Form/TaskType.php
use AppBundle\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
Tip
When mapping forms to objects, all fields are mapped. Any fields on the form that do not exist on the mapped object will cause an exception to be thrown.
In cases where you need extra fields in the form (for example: a "do you
agree with these terms" checkbox) that will not be mapped to the underlying
object, you need to set the mapped
option to false
:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('dueDate')
->add('agreeTerms', CheckboxType::class, ['mapped' => false])
->add('save', SubmitType::class)
;
}
Additionally, if there are any fields on the form that aren't included in
the submitted data, those fields will be explicitly set to null
.
The field data can be accessed in a controller with:
1
$form->get('agreeTerms')->getData();
In addition, the data of an unmapped field can also be modified directly:
1
$form->get('agreeTerms')->setData(true);
Note
The form name is automatically generated from the type class name. If you want to modify it, use the createNamed() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/AppBundle/Controller/DefaultController.php
use AppBundle\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class DefaultController extends AbstractController
{
public function newAction()
{
$task = ...;
$form = $this->get('form.factory')->createNamed('name', TaskType::class, $task);
// ...
}
}
You can even suppress the name completely by setting it to an empty string.
Final Thoughts
When building forms, keep in mind that the first goal of a form is to translate data
from an object (Task
) to an HTML form so that the user can modify that data.
The second goal of a form is to take the data submitted by the user and to re-apply
it to the object.
There's a lot more to learn and a lot of powerful tricks in the form system.
Learn more
- How to Change the Action and Method of a Form
- Bootstrap 4 Form Theme
- How to Choose Validation Groups Based on the Clicked Button
- How to Create a Custom Form Field Type
- How to Create a Form Type Extension
- How to Implement CSRF Protection
- How to Choose Validation Groups Based on the Submitted Data
- When and How to Use Data Mappers
- How to Use Data Transformers
- How to Use the submit() Function to Handle Form Submissions
- How to Disable the Validation of Submitted Data
- How to Dynamically Modify Forms Using Form Events
- How to Embed Forms
- Form Events
- How to Embed a Collection of Forms
- How to Customize Form Rendering
- How to Access Services or Config from Inside a Form
- How to Work with Form Themes
- How to Reduce Code Duplication with "inherit_data"
- How to Submit a Form with Multiple Buttons
- How to Control the Rendering of a Form
- Creating a custom Type Guesser
- How to Unit Test your Forms
- How to Configure empty Data for a Form Class
- How to Dynamically Configure Form Validation Groups
- How to Define the Validation Groups to Use
- How to Use a Form without a Data Class
- How to Upload Files
- Form Types Reference
- Caching Pages that Contain CSRF Protected Forms