Introduction to Form API

Last updated on
15 February 2024

Forms handled by classes

A form in Drupal is managed by a class that implements the \Drupal\Core\Form\FormInterface. The basic workflow of a form is defined by the buildForm, validateForm, and submitForm methods of the interface. When a form is requested it's defined as a renderable array often referred to as a Form API array or simply $form array. The $form array is converted to HTML by the render process and displayed to the end user. When a user submits a form the request is made to the same URL that the form was displayed on, Drupal notices the incoming HTTP POST data in the request and this time–instead of building the form and displaying it as HTML–it builds the form and then proceeds to call the applicable validation and submission handlers.

Defining forms as structured arrays, instead of as straight HTML, has many advantages including:

  • Consistent HTML output for all forms.
  • Forms provided by one module can be easily altered by another without complex search and replace logic.
  • Complex form elements like file uploads and voting widgets can be encapsulated in reusable bundles that include both display and processing logic.

There are several types of forms commonly used in Drupal. Each has a base class which you can extend in your own custom module.

First, identify the type of form you need to build:

  1. A generic form. Extend FormBase.
  2. A configuration form that enables administrators to update a module's settings. Extend ConfigFormBase.
  3. A form for deleting content or configuration which provides a confirmation step. Extend ConfirmFormBase.

FormBase implements FormInterface, and both ConfigFormBase and ConfirmFormBase extend FormBase, therefore any forms that extend these classes must implement a few required methods.

Required Methods

FormBase implements FormInterface, and therefore any form that has FormBase in its hierarchy is required to implement a few methods:

getFormId()

public function getFormId()

This needs to return a string that is the unique ID of your form. Namespace the form ID based on your module's name.

Example:

  public function getFormId() {
    return 'mymodule_settings';
  }

buildForm()

public function buildForm(array $form, FormStateInterface $form_state)

This returns a Form API array that defines each of the elements your form is composed of.

Example:

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['phone_number'] = [
      '#type' => 'tel',
      '#title' => $this->t('Example phone'),
    ];
        
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

Note that the buildForm method needs to hold the logic needed to build dynamic forms. The previous example is just a simple form that always has the same elements, but a dynamic form like a node form will most likely have different elements depending on the data of the node. The buildForm method will add the required form elements depending on the node data. This applies to regular forms and AJAX forms.

Validating Forms

After a user fills out the form and clicks the submit button it's common to want to perform some sort of validation on the data that's being collected. To do this with Drupal's Form API we simply implement the validateForm method from \Drupal\Core\Form\FormInterface in our ExampleForm class.

Form values submitted by the user are contained in the $form_state object at $form_state->getValue('field_id'), where 'field_id' is the key used when adding the form element to the $form array in ExampleForm::buildForm(). We can perform our custom validation on this value. If you need to get all submitted values, you can do so by using $form_state->getValues().

Form validation methods can use any PHP processing necessary to validate that the field contains the desired value and raise an error in the event that it is an invalid value. In this case since we're extending the \Drupal\Core\Form\FormBase class we can use \Drupal\Core\Form\FormStateInterface::setErrorByName() to register an error on a specific form element and provide an associated message explaining the error.

When a form is submitted, Drupal runs through all validation handlers for the form, both the default validation handlers and any validation handlers added by developers. If there are errors, the form's HTML is rebuilt, error messages are shown, and fields with errors are highlighted. This allows the user to correct any errors and re-submit the form. If no errors are present, the submit handlers for the form are executed.

The following is an example of a simple validateForm() method:

/**
 * {@inheritdoc}
 */
public function validateForm(array &$form, FormStateInterface $form_state) {
  if (strlen($form_state->getValue('phone_number')) < 3) {
    $form_state->setErrorByName('phone_number', $this->t('The phone number is too short. Please enter a full phone number.'));
  }
}

If no errors are registered during form validation, Drupal continues processing the form. At this point it is assumed that values within $form_state->getValues() are valid and ready to be processed and used in whatever way our module needs to make use of the data.

Submitting Forms / Processing Form Data

Finally, we're ready to make use of the collected data and do things, like save the data to the database, send an email, or any number of other operations. To do this with Drupal's Form API we need to implement the submitForm method from \Drupal\Core\Form\FormInterface in our ExampleForm class.

As with the validation method above, the values collected when the form was submitted are in $form_state->getValues() and at this point we can assume the data have been validated and are ready for use. For example, accessing the value of our 'phone_number' field can be done by accessing $form_state->getValue('phone_number').

Here's an example of a simple submitForm method which displays the value of the 'phone_number' field on the page using the messenger service:

/**
 * {@inheritdoc}
 */
public function submitForm(array &$form, FormStateInterface $form_state) {
  $this->messenger()->addStatus($this->t('Your phone number is @number', ['@number' => $form_state->getValue('phone_number')]));
}

This is a simple example of handling submitted form data. For more complex examples, take a look at some of the classes that extend FormBase in core.

Here is a complete example of a form class:

File contents of /modules/example/src/Form/ExampleForm.php if the module is in /modules/example:

<?php

namespace Drupal\example\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements an example form.
 */
class ExampleForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'example_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['phone_number'] = [
      '#type' => 'tel',
      '#title' => $this->t('Your phone number'),
    ];
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Save'),
      '#button_type' => 'primary',
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    if (strlen($form_state->getValue('phone_number')) < 3) {
      $form_state->setErrorByName('phone_number', $this->t('The phone number is too short. Please enter a full phone number.'));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addStatus($this->t('Your phone number is @number', ['@number' => $form_state->getValue('phone_number')]));
  }

}

The id of the form is returned by the getFormId() method on the form class. The builder method is called buildForm() and contains dedicated methods for validation and submission.

Integrate the form in a request

The routing system allows form classes to be provided as route handlers, in which case the route system takes care of instantiating this class and invoking the proper methods. To integrate this form into a Drupal site's URI structure, use a route like the following:

File contents for /modules/example/example.routing.yml if the module is in /modules/example:

example.form:
  path: '/example-form'
  defaults:
    _title: 'Example form'
    _form: '\Drupal\example\Form\ExampleForm'
  requirements:
    _permission: 'access content'

The _form key tells the routing system that the provided class name is a form class to be instantiated and handled as a form.

Note that the form class and the routing entry are the only two pieces required to make this form work; there is no other wrapper code to write.

Retrieving this form outside of routes

Although Drupal 7's drupal_get_form() is gone in Drupal 8+, there is a FormBuilder service that can be used to retrieve and process forms. The Drupal 8+ equivalent of drupal_get_form() is the following:

$form = \Drupal::formBuilder()->getForm('Drupal\example\Form\ExampleForm');

The argument passed to the getForm() method is the name of the class that defines your form and is an implementation of \Drupal\Core\Form\FormInterface. If you need to pass any additional parameters to the form, pass them on after the class name.

Example:

$extra = '612-123-4567';
$form = \Drupal::formBuilder()->getForm('Drupal\mymodule\Form\ExampleForm', $extra);
...
public function buildForm(array $form, FormStateInterface $form_state, $extra = NULL)
  $form['phone_number'] = [
    '#type' => 'tel',
    '#title' => $this->t('Your phone number'),
    '#value' => $extra,
  ];
  return $form;
}

In some special cases, you may need to manipulate the $form object before the FormBuilder calls your class's buildForm() method, in which case you can do $form_object = new \Drupal\mymodule\Form\ExampleForm($something_special);

$form_builder->getForm($form_object);

Altering this form

Altering forms is where the Drupal 8+ Form API reaches into basically the same hook-based approach as Drupal 7. You can use hook_form_alter() and/or hook_form_FORM_ID_alter() to alter the form, where the ID is the form ID you provided when defining the form previously.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function example2_form_example_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $form['phone_number']['#description'] = t('Start with + and your country code.');
}

We named the hook_form_FORM_ID_alter() implementation after our module name (example2) including the form ID (example_form). This is the same pattern used in Drupal 7 form alters.

Gotchas

When using the #ajax property with your form and the form's class uses private properties, there are issues with serialization. See Considerations for Serialization.

See also

Help improve this page

Page status: No known problems

You can: