Forms
A form in Drupal is described with an array, not with markup. From that description the system renders the fields, checks required values, protects against request forgery and calls your validation and submit methods. Let us make a form for subscribing to new blog articles.
Where a form lives
Each form is a separate class in the src/Form folder of the module. Create the folder and the file by hand.
The form class
// web/modules/custom/blog/src/Form/SubscribeForm.php
namespace Drupal\blog\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class SubscribeForm extends FormBase
{
public function getFormId(): string
{
return 'blog_subscribe_form';
}
public function buildForm(array $form, FormStateInterface $form_state): array
{
$form['email'] = [
'#type' => 'email',
'#title' => $this->t('Email'),
'#required' => TRUE,
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Subscribe'),
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state): void
{
$email = $form_state->getValue('email');
$this->messenger()->addStatus($this->t('The address @email is subscribed.', ['@email' => $email]));
}
}| Method | What it does |
|---|---|
getFormId() | Returns the unique name of the form |
buildForm() | Describes the fields and buttons |
submitForm() | Runs after a successful submission |
Fields are the same render arrays with a #type property. The values of the submitted form are kept in the $form_state object.
The form route
A form needs no controller: the route names _form instead of _controller:
# web/modules/custom/blog/blog.routing.yml
blog.subscribe:
path: '/blog/subscribe'
defaults:
_form: '\Drupal\blog\Form\SubscribeForm'
_title: 'Subscription'
requirements:
_permission: 'access content'After drush cr open /blog/subscribe: a field and a button appear. The browser does not send an empty form, and after a submission with an address a green message appears at the top of the page.
Validation
Your own validation rules go into the validateForm() method:
// web/modules/custom/blog/src/Form/SubscribeForm.php
public function validateForm(array &$form, FormStateInterface $form_state): void
{
if (str_ends_with($form_state->getValue('email'), '@example.com')) {
$form_state->setErrorByName('email', $this->t('Enter a real address.'));
}
}setErrorByName() marks the field with an error: the form returns to the visitor with a red message and submitForm() does not run. Drupal checked the address format and the required flag itself — from #type and #required.
Form elements
| Type | Field |
|---|---|
textfield | A single line |
textarea | Multi-line text |
email, tel, url, number | Fields with format validation |
select | A list: options in #options |
checkbox, checkboxes, radios | Checkboxes and radio buttons |
date | A date |
submit | The submit button |
Common properties: #title, #description, #default_value, #required.
After submission
// web/modules/custom/blog/src/Form/SubscribeForm.php
$this->messenger()->addStatus($this->t('Done.'));
$this->messenger()->addError($this->t('Could not save.'));
$form_state->setRedirect('blog.list');A message is shown once — on the next page. setRedirect() sends the visitor to a route by name; without it the visitor stays on the form page. The form starts saving the address to the database in the lesson about the database.
A form inside a page
A form can be output as a part of a render array in a controller:
// web/modules/custom/blog/src/Controller/BlogController.php
$build['subscribe'] = $this->formBuilder()->getForm(\Drupal\blog\Form\SubscribeForm::class);Common mistake
Do not read data from $_POST: it holds values before validation and without forgery protection. Use $form_state->getValue('email') — it holds only the fields described in the form that passed validation. And do not output a form with plain HTML tags: without the Form API it has no protection token.