Form API
Drupal's Form API defines each form as one PHP class and supplies the shared parts: CSRF tokens, required-field checks and alter hooks for other modules.
The Form API is the part of Drupal core that builds, validates and processes HTML forms. A developer describes a form as a PHP class that returns a nested array; Drupal turns the array into markup, checks the submitted values and calls the form's submit handler. Core's Basic site settings page is one such form, and so are the forms in custom modules.
How the Form API works in Drupal 11
A form is a class that implements Drupal\Core\Form\FormInterface. The interface has four methods: getFormId(), buildForm(), validateForm() and submitForm(). FormBase is the most commonly used base class, and in it validation is optional. ConfigFormBase is for settings pages and ConfirmFormBase is for confirmation steps.
The $form array is a render array. Keys that start with # are properties such as #type, #title and #required; other keys are child elements. The Render API turns it into HTML.
You show a form by pointing a route at it with the _form key, or by calling \Drupal::formBuilder()->getForm(). Extra arguments to getForm() are passed on to buildForm().
The FormBuilder service runs the process. It calls buildForm(), adds the hidden form_build_id and form_id fields, and runs the alter hooks. On submission it validates required elements, runs each element's #element_validate callbacks, then the form-level #validate handlers. If no errors were set, the #submit handlers run. When no redirect is set, Drupal redirects back to the current path with a 303 status. A handler that calls $form_state->setRebuild() keeps the user on the form, which is how multistep forms work.
Forms shown to authenticated users carry a token tied to the session, a protection against cross-site request forgery. Core skips the token for anonymous users and for forms that use GET.
Example
A route in my_module.routing.yml points to the form class:
my_module.subscribe:
path: '/subscribe'
defaults:
_form: '\Drupal\my_module\Form\SubscribeForm'
_title: 'Subscribe'
requirements:
_permission: 'access content'
<?php
namespace Drupal\my_module\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class SubscribeForm extends FormBase {
public function getFormId(): string {
return 'my_module_subscribe';
}
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['email'] = [
'#type' => 'email',
'#title' => $this->t('Email address'),
'#required' => TRUE,
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Subscribe'),
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->messenger()->addStatus($this->t('Thanks, @mail.', ['@mail' => $form_state->getValue('email')]));
}
}
Common mistakes
- Using Drupal 7 habits.
drupal_get_form()no longer exists. The FormBuilder service replaced it. - Leaving out
#title. The validator builds its "field is required" message from the title. When a visible title is unwanted, the validator code recommends setting#titleanyway and hiding it with#title_displayset toinvisible. - Altering with the wrong hook.
hook_form_alter()runs for every form. For one form,hook_form_FORM_ID_alter()avoids a long list ofifchecks on the form ID.
For marketing and content teams
When a developer says a form needs a different button label, an extra rule or a hidden field, that is Form API work done in a module, not a setting you can change in the interface. For a settings page, such as Configuration › System › Basic site settings, the form class is ConfigFormBase, so the saved values are configuration that can be exported and deployed.