Drupal 8 API: forms

Pre-requisites

A form is a special type of route-handling callback

Previously we learned how routing is a way of tying together on the one hand URLs that we want Drupal to start responding to, and on the other hand a class method containing the custom code that we want to be invoked to handle the response.

A routed form is a special case of this: whereas a general class for routing can have many methods with arbitrary names, one method per URL, each returning very different content; a form class must implement a set number of methods specified by a PHP interface, and one class can only return the same one form, although it could change how it appears during the process of someone submitting the form e.g. it could have several different steps (which we don't cover here.)

Form classes must implement the Drupal\Core\Form\FormInterface class: they can do this by (a) extending an existing core class, as we'll see below, then (b) defining the remaining methods that that class doesn't give you, to actually build the form.

Creating a form class

A form class inheriting from Drupal\Core\Form\FormBase implements the FormInterface described above. In addition, you must add the following methods:

  • getFormId(): return a unique string of characters identifying the form within your Drupal site. As with the previous discussion of "machine name", you should consider using form IDs which are lowercase, underscored, and begin with the machine name of your module.
  • build(): return an array of nested Form API render data. This is different from normal theme render arrays (which we'll discuss later): Drupal 8's Form API render array format is very similar to Drupal 7's and so does not have its own separate documentation.
  • submitForm(): once a submission is received and e.g. all required fields are present, you will invariably want to do something with the results. This means that you must implement this method, even if (for whatever reason) it's empty and does nothing.

In addition, the form class can (override) FormBase#validateForm(). This method checks the data submitted by the site visitor to ensure it's consistent with your custom requirements, before submitForm() is called.

Here's an example of a form class implementing the bare minimum of what's required. In the d8api module from previous tutorials, add the following class at src/Form/TutorialForm.php:

<?php
 
namespace Drupal\d8api\Form;
 
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Form: simple submit-form for tutorial.
 */
class TutorialForm extends FormBase {
 
  /**
   * {@inheritDoc}
   *
   * From FormInterface, via FormBase.
   */
  public function getFormId() {
    return 'd8api_tutorial_form';
  }
 
  /**
   * {@inheritDoc}
   *
   * From FormInterface, via FormBase.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['phone'] = [
      '#type' => 'tel',
      '#title' => $this->t('Your telephone number'),
    ];
    $form['go'] = [
      '#type' => 'submit',
      '#value' => $this->t('Go'),
    ];
 
    return $form;
  }
 
  /**
   * {@inheritDoc}
   *
   * From FormInterface, via FormBase.
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    drupal_set_message(
      $this->t('Ringing @phone', ['@phone' => $form_state->getValue('phone')])
    );
  }
}

A couple of points:

  • The build() method returns a structured array of data about the form fields. This is a longstanding data structure in Drupal, often called "Form API", and little changed in Drupal 8. More information on Form API is available here.
  • The submitForm() method returns a different structured array, a render(able) array, which we encountered previously during discussions about routing and we'll discuss later.
  • As previously, because we extend a Drupal core class (FormBase) we have access to translation services via $this->t(). Use this to wrap any "English" text you might ever want to translate in the future for the user interface.
  • Historically (on the web generally, not Drupal), simple forms could have no submit button; when you pressed enter in a textfield, it would just submit. However, not only is this considered bad practice these days (it presents accessibility and usability problems) but Drupal won't even process the submission: the submitForm() method will not get called! So always have at least one submit button on your form.
  • Not all of Drupal is object-oriented: we've sneaked in a call to the bare function drupal_set_message() in submitForm().

Wiring up the new class to the URL we've chosen

As with routing classes, on the one hand we now have our form class; on the other, we also presumably have some URL in mind, that the form will need to be accessible at. We therefore need to wire the two together.

To do this, we edit the previously defined d8api.routing.yml to include a new route specification:

d8api.form:
  path: '/tutorial/form'
  defaults:
    _title: 'Tutorial form'
    _form: '\Drupal\d8api\Form\TutorialForm'
  requirements:
    _permission: 'access content'

You can see this is slightly different from basic routing, and defines a _form key with the form class in it. This is because a form associates an entire class with each route, rather than reusing the same class with potentially a method to each route. Also, we only need specify the class name, and the routing then knows to always use the build() method for a form.

What you should see

As previously discussed, you should clear caches so that the new routing specification is recognized.

If you then navigate to /tutorial/form, you should see the form ready for input submission:

When you press the "go" button, you should see the confirmation message containing the phone number you submitted:

If you can see this then congratulations! you have created your first custom form in Drupal.

Further reading etc.