Changes in the Form API in Drupal 8

Drupal 8

In my previous post, I documented the first of my Adventures in Porting a D7 Form Module to Drupal 8. In that article, I documented how I used the Drupal Module Upgrader (DMU) to convert my Drupal 7 module, Form Fun, to Drupal 8 and what I learned along the way about how Routes and Controllers replaced hook_menu, and what I gleaned from change records about other API changes. This article is a continuation of that post, so you might want to pop over and give it a read so that you're up to speed with what we're doing here.

There are still plenty of lessons to be documented, including "a-ha!" moments about how Dependency Injection is meant to be used in Drupal 8 and other insights that I will write up in future posts, so stay tuned for that. Today, however, now that we've got our Cake Page controller working, let's take a look at the "Death, or cake?" form that displays on this page and take a look at some changes in the Drupal 8 Form API in the process.

Death or Cake? form

In the DefaultController.php, the following method replaces Drupal 7's drupal_get_form:


public function form_fun_cake_page() {
    $form = \Drupal::formBuilder()->getForm('Drupal\form_fun\Form\FormFunCake');
    return $form;
  }

This tells the FormBuilder about our class, FormFunCake in the Drupal\form_fun\Form namespace, which implements the FormInterface and is defined in the file: modules/form_fun/src/Form/FormFunCake.php

Let's take a peek at that class in modules/form_fun/src/Form/FormFunCake.php. First we declare the namespace, the other classes we want to use, and extend the FormBase class. Some code:


/**
 * @file
 * Contains \Drupal\form_fun\Form\FormFunCake.
 */

namespace Drupal\form_fun\Form;

Next we use a special syntax to bring in code from some other classes, using the use PHP keyword and then the namespace, using the PSR-4 standard, which will autoload the classes in the files that correspond to these namespaces.


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

Now that we've declared our own namespace, and brought in dependencies that we'd like to use, we can declare our own class, FormFunCake that extends the class FormBase—and you guessed it, FormBase is the class we brought in as a dependency with this line: use Drupal\Core\Form\FormBase;


class FormFunCake extends FormBase {

... form methods go here ...

}

Extending FormBase

Depending on what kind of form you're building, you have a few choices of base classes that you could extend. In this porting exercise, DMU module extended the FormBase class. If you were refactoring a configuration settings form, you'd want to extend ConfigFormBase or if you had a form with a confirm step, like a "Are you sure you want to delete this?" intermediary step, you could extend ConfirmFormBase. See the handbook page Form API in Drupal 8 and the section "Defining Forms" for more on this. The point is, you don't have to start from scratch. Find the appropriate base class to extend and you're off and running—especially once you find the right interface.

Finding Methods in an Interface

At this point, we have a class structure in place, but how do I know if Drupal Module Upgrader provided the correct methods? (We are in beta, after all, and at this stage, I'm in a "question everything" frame-of-mind.) Not to worry, we can and should use an "interface" as our guide. But which interface? For this, we can get a little help from an IDE. In my case, I'm using PhpStorm. Here's what I did:

  1. In modules/form_fun/src/Form/FormFunCake.php, I highlighted the word FormBase in class FormFunCake extends FormBase. I'm looking for the interface that it is using, since no interface is mentioned in my FormFunCake class declaration. We want to dig into our FormBase dependency and find out which interface it is using.
  2. In PhpStorm's menu, I select "Navigate > Class" and select the first result: FormBase.

    Highlighting FormBase in PhpStorm

  3. With FormBase.php now opened in a new tab, I notice the following: abstract class FormBase implements FormInterface, ContainerInjectionInterface and I highlight FormInterface because that seems right. I'm looking for an interface for a form...FormInterface. Seems logical enough.
  4. Again, in PhpStorm, I select Navigate > Class and it searches for FormInterface.
    Searching for FormInterface in PhpStorm

    I select FormInterface.php in core/lib/Drupal/core/Form and it opens in a new tab. Bingo! Now I have an interface for my form.

An interface is a contract that states these are the methods that are required of anyone implementing this interface. Or, these are the methods you can assume are always available to call if you're consuming a class that implements this interface. This interface contains four methods: getFormId, buildForm, validateForm, and submitForm. These are the four methods I am required to have in my form-building class.

DMU module did a pretty good job of refactoring my form functions to methods inside a class, but the parameters of the methods were outdated or simply wrong and as a result, I got some errors. To fix these, I simply copied and pasted the methods, including all of the parameters, from FormInterface.php into my FormFunCake class in FormFunCake.php.

Notice the parentheses containing the parameters for the buildForm method? It was refactored from:


public function buildForm(&$form, &$form_state)

...to...


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

...by copying and pasting the entire FormInterface::buildForm method in core/lib/Drupal/core/FormInterface.php

You might be asking yourself, how can this code, copied straight from FormInterface.php, still work in our custom module, being so generic and without any customizations or changes? One reason is because the ID of the form has been moved out of the function name that returns the form and into a separate method, getFormID(). Second of all, this method is housed inside our own class FormFunCake, which extends the FormBase, which uses the FormInterface. If it seems a little abstract, that's because it is. But it's also what makes it so easily re-usable. We just copy it straight from the interface into the class. We don't even have to rename anything.

Give the Form an ID

We need to give our form an ID. In Drupal 7, this would be the name of the function that returns the $form renderable array. In Drupal 8, implement the method FormInterface::getFormID() inside your form class. In my case, it looks like this:


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

Building the Form

In Drupal 8, I'm still going to build a renderable form array. But, instead of putting it inside a procedural function whose name is the ID of the form, I put the form array in a method called buildForm(), like this:


public function buildForm(array $form, FormStateInterface $form_state) {
    $form['choice'] = array(
      '#type' => 'select',
      '#title' => t('Cake or death'),
      '#description' => t('You must have tea and cake with the vicar... or you die!'),
      '#options' => array(
        'cake' => t('Cake with the vicar'),
        'death' => t('Death'),
        'chicken' => t('Chicken'),
      ),
      '#default_value' => 'cake',
      '#required' => TRUE,
    );

    $form['buttons']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit'),
    );

    // Because the 'unsure' button has a #submit property, that function
    // will be called if it is clicked instead of the form's default
    // submit handler.
    $form['buttons']['unsure'] = array(
      '#type' => 'submit',
      '#value' => t('Equivocate'),
      // Call the equivocate method in this class.
      '#submit' => array('::equivocate'),

      // No validation at all is required in the equivocate case, so
      // we include this here to make it skip the form-level validator.
      '#validate' => array(),
    );
    return $form;
  }

Which gives me this:

Death or Cake? form

Other Changes for Forms in Drupal 8?

Besides the parameter changes for the buildForm() method, which changes the $form_state array to an implementation of FormStateInterface, the other change that caught my attention was how to specify a callback for a button submit action using Object-Oriented PHP.

In Drupal 7, I'd have a file full of procedural functions, one of which would be the function that determines what should happen when a button is clicked. I'd just copy the function name in that file into the #submit value for the button in my form array. But we don't have functions like this in Drupal 8, we have methods inside classes. So what's the new syntax? The callback can now be any PHP callable. There's also a special, Drupal-specific, shorthand syntax which uses a double-colon (::) plus the name of the method, whenever a form object is specifying a callback to itself. In this case, the method in my FormFunCake class that I want to call when the "Equivocate" button is clicked, is called public function equivocate. To specify that method, I'd use ::equivocate, like this:

  
    // Because the 'unsure' button has a #submit property, that function
    // will be called if it is clicked instead of the form's default
    // submit handler.
    $form['buttons']['unsure'] = array(
      '#type' => 'submit',
      '#value' => t('Equivocate'),
      // Call the equivocate method on this class.
      '#submit' => array('::equivocate'),
  
  

Validation

With the form built, we can add some validation and set an error message if the validation doesn't pass. In Drupal 7, we'd use form_set_error, but in Drupal 8, inside the method validateForm, we can do something like this:


public function validateForm(array &$form, FormStateInterface $form_state) {
    if ($form_state->getValue('choice') == 'cake') {
      $form_state->setErrorByName(
        'choice',
        $this->t("We're out of cake! We only had three bits and we didn't expect such a rush.")
      );
    }
  }

Notice also $form_state->getValue('choice'). Since we're passing in FormStateInterface $form_state, we get access to the $form_state object. The FormStateInterface has a method called getValue, which lets you easily snatch the submitted value of a particular form element, in this case, "choice," a field defined in the renderable form array in buildForm. Pretty handy, huh? This interface also has a method called setErrorByName which lets you build your error message. Don't forget to use Drupal's $this->t method to make your error message string translatable!

A Submit Method

Finally, we build the submit method. According to the FormInterface, we use: public function submitForm(array &$form, FormStateInterface $form_state). I copy this from the FormInterface straight over top what DMU did (because at the time of this writing, it was wrong) and create some redirects based on what value was chosen in the select dropdown element. To do this, I use another method in the FormStateInterface called setRedirect. So, here's my submitForm method for FormFunCake.


  public function submitForm(array &$form, FormStateInterface $form_state) {
    switch($form_state->getValue('choice')) {
      case 'cake':
        $form_state->setRedirect('form_fun.cake_image');
        break;
      case 'death':
        $form_state->setRedirect('form_fun.death_image');
        break;
      case 'chicken':
        $form_state->setRedirect('form_fun.chicken_image');
        break;
      default:
        $form_state->setRedirect('form_fun.cake_page');
        break;
    }
  }

Here's what the submit function looked like before, in the Drupal 7 version:


function form_fun_cake_submit(&$form, &$form_state) {
  $choice = $form_state['values']['choice'];
  switch ($choice) {
    case 'cake':
      $form_state['redirect'] = 'form_fun/cake_image';
      break;
    case 'death':
      $form_state['redirect'] = 'form_fun/death_image';
      break;
    case 'chicken':
      $form_state['redirect'] = 'form_fun/chicken_image';
      break;
  }
}

Lessons Learned

Just by going through the exercise of porting a Drupal 7 form module to Drupal 8, I learned about more than just the Drupal 8 Form API. I've learned how hook_menu got replaced by routes and controllers, in YAML files and Controller.php files. I also learned how to use PhpStorm to navigate classes, and find the methods I could use by looking inside interfaces. I also learned to take Drupal Module Upgrader with a grain of salt! It was a great start to porting my module, but as Drupal 8 is still in development and is a moving target, it was no surprise that there were a few hiccups along the way. That said, with the Change Records page ready to search, and interfaces and classes ready to search in my IDE, I was able to get everything refactored to Drupal 8.

There are still lessons to be learned that I didn't find in the change records, for example, how to understand and use Dependency Injection. Stay tuned for a post coming soon on that very subject! And what about multi-step forms and AJAX? All that and more is coming your way in this special blog series on the Form API and Drupal 8.

For another perspective on forms in Drupal 8, check out Joe Shindelar's blog post, Getting Started with Forms in Drupal 8. There's been a few changes in Drupal 8 since then, but it will help you understand form workflow and get you started building your first form from scratch in Drupal 8.

Resources

Related Topics: 

Comments

Thanks! As I become more familiar with the modernizations being made in D8, it's interesting how much more I come to see procedural D7 as being formless and arbitrary. It's been an awakening.

For me, too!

"This tells the FormBuilder about our class, FormFunCake in the Drupal\form_fun\Form namespace, which implements the FormInterfece and is defined in the file: modules/form_fun/src/Form/FormFunCake.php"

The FormInterfece? I don't want any more Fun Cake.

Ew! :) Fixed, and thanks!

Great walkthrough on building a form in D8. Thank you!

thank you its been very helpful! but I wonder, if I dont want a redirect on the submit, so when user submit just load the results in a table in the same form page? is that achieveable in d8? as I cant find the references

thank you

Hi good article.

Add new comment