Last updated December 15, 2014. Created on October 22, 2013.
Edited by Amber Himes Matz, romainj, abhishek.kumar, rakesh.gectcr. Log in to edit this page.

The Drupal 8 Form API is largely similar to the Drupal 7 Form API. The forms are still represented with nested render array structures and there is a separate validation and submission step. There are some new (HTML 5) elements available and the integration of these components into the rest of the Drupal system changed a bit.

New (HTML 5) elements

Check out namespace Drupal\Core\Render\Element for all the core provided elements. There are new HTML 5 elements like '#type' => 'tel', '#type' => 'email', '#type' => 'number', '#type' => 'date', '#type' => 'url', '#type' => 'search', '#type' => 'range', etc. Using these elements as opposed to requesting data in plain textfields is preferable because devices can pull up the proper input methods for them, such as when a telephone number is requested, the dialpad would show up on a device.

There are also other elements added to help structure your forms. The '#type' => 'details' element is a grouping element with a summary. The '#type' => 'language_select' element is a language selector to make it easy to put language configuration on forms. The '#type' => 'dropbutton' and '#type' => 'operations' elements are the dropdown link/operations list that you may know from Views, now used consistently for operations in Drupal 8.

Overview

Form classes implement the \Drupal\Core\Form\FormBuilderInterface and 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 builds the form and then proceeds to call the applicable validation and submission handlers.

Defining forms as structured arrays instead of 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.

Defining forms

Forms are defined by implementing the \Drupal\Core\Form\FormBuilderInterface. Form classes handle creation, submission, and validation of a form so that all form related logic is grouped together. Drupal 8 provides base classes that you can extend for easy form creation.

There are a few different base classes to choose from depending on the type of form you are creating. In most cases you'll likely start by extending one of these when creating your own forms.

  • ConfigFormBase - For creating system configuration forms like the one found at admin/config/system/site-information.
  • ConfirmFormBase - For providing users with a form to confirm an action such as deleting a piece of content.
  • FormBase - The most generic base class for generating forms.

Whichever base class you choose to extend the following methods are the ones you're most likely to implement first.

public function getFormId()

Which will simply needs to return a string that is the unique ID of your form. Best practice is to namespace the form based on your module's name.

Example:

<?php
 
public function getFormId() {
    return
'mymodule_settings';
  }
?>

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

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

Example:

<?php
 
public function buildForm(array $form, FormStateInterface $form_state) {
   
$form['my_text_field'] = array(
     
'#type' => 'textfield',
     
'#title' => 'Example',
    );
    return
$form;
  }
?>

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\FormBuilderInterface in our ExampleForm class.

User submitted values from the form are contained in the $form_state array at $form_state['values'], where $form_state['values'] is an array containing the value for each individual field from the form keyed based on the key used when adding the form element to the $form array in FormExample::buildForm(). In this case the value of our phone number field will be accessible in $form_state['values']['phone_number'] and we can perform our custom validation on this value.

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 and Drupal performs both it's own and our validation all errors that are registered during the process are collected, the HTML for the form is rebuilt and fields with errors are highlighted. This allows the user to correct any errors and re-submit the form.

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

<?php
/**
 * {@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 then Drupal continues on with processing the form. At this point it is assumed that that values within the $form_state['values'] array 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 data that we've collected and do things like save it to the database or 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\FormBuilderInterface in our ExampleForm class.

Just like in the validation method above the values collected from the user when the form was submitted are in $form_state['values'] and at this point we can assume they've been validated and are ready for us to make use of. Accessing the value of our 'phone_number' field can be done by accessing $form_state['values']['phone_number'].

Here's an example of a simple submitForm method which displays the value of the 'phone_number' field no the page using drupal_set_message():

<?php
/**
 * {@inheritdoc}
 */
public function submitForm(array &$form, FormStateInterface $form_state) {
 
drupal_set_message($this->t('Your phone number is @number', array('@number' => $form_state->getValue('phone_number'))));
}
?>

This is a really 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
/**
 * @file
 * Contains \Drupal\example\Form\ExampleForm.
 */

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'] = array(
     
'#type' => 'tel',
     
'#title' => $this->t('Your phone number')
    );
   
$form['actions']['#type'] = 'actions';
   
$form['actions']['submit'] = array(
     
'#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) {
   
drupal_set_message($this->t('Your phone number is @number', array('@number' => $form_state->getValue('phone_number'))));
  }

}
?>

While in Drupal 7 the form builder function name itself was the form ID, in Drupal 8, the id of the form is returned by the getFormId() method on the form class. The builder method is called buildForm() and there are dedicated methods for validation and submission. The logic and form array structure / form processing you use in these methods is very similar to how Drupal 7 forms worked.

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:

<?php
$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\FormBuilderInterface. If you need to pass any additional parameters to the form, pass them on after the class name.

Example:

<?php
$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'] = array(
   
'#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 classes buildForm() method, in which case you can do

<?php
 $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 approach where Drupal 7 is. Given that you provided a form ID for your form, altering is based on that. Use hook_form_alter() and/or hook_form_FORM_ID_alter() to alter the form.

<?php
/**
 * Implements hook_form_FORM_ID_alter().
 */
function example2_form_example_form_alter(&$form, &$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). If you built Drupal 7 form alters, you can see this is the same behaviour as there.

See also

Looking for support? Visit the Drupal.org forums, or join #drupal-support in IRC.

Comments

Torenware’s picture

In the examples for buildForm(), verifyForm() and submitForm(), there is no call to the parent functions to invoke logic from FormBase.

I'm not sure if this is an error or not, although I suspect it's bad practice.

Rob Thorne
Torenware Networks
http://www.torenware.com

tz_earl’s picture

I tried to call parent::buildForm() in a class derived from FormBase and got a syntax error because FormBase has no implementation of that abstract method, which is inherited from an interface. Looking at the code for FormBase, it looks like that's also the case for submitForm(). FormBase does contain an empty implementation for validateForm() that does nothing.

My take is that calling the parent functions is a good practice if you have an ancestral class that implements them.

sebto’s picture

Useful examples of D8 FPI in Example module.

Morbus Iff’s picture

I'm by no means intelligible on Drupal 8, but the following:

<?php
public function buildForm(array $form, FormStateInterface $form_state, $extra = NULL)
?>

throws the following error:

PHP Fatal error:  Declaration of buildForm() must be compatible with Drupal\Core\Form\FormInterface::buildForm()

To get around this (potentially ignorantly so), I removed the extra parameter from the declaration and replaced it with:

<?php
    $entity
= $form_state->getBuildInfo()['args'][0];
?>