A Technical Guide to Drupal 8: Forms

  • Kevin Quillen
  • January 4, 2016

Drupal 8 was recently released after years of development and testing. A majority of the Drupal core was rewritten from the ground up in this latest version - a radical approach to modernizing the platform on various fronts and easing both development and content management.

Previous versions of Drupal were largely revised and improved upon releases of the initial build. Drupal 8, however, has cast most of this initial core aside in favor of leveraging Symfony components as a foundation.

While there have been numerous changes and your favorite functions or tools might have been updated or removed, this guide series should provide a breakdown of popular functions within the new release. Absorbing all of these new concepts at once might be intimidating to those who are used to years of the same development patterns. So we're going to ease into it with a simple example: a custom form.

Forms in Drupal - 7 vs 8

Lets assume you have a module (called testmodule) in Drupal 7 that creates a specific URL with a form that asks the user to pick their favorite fruit from a list of options. Doing this in Drupal 7 might have looked something like this:

/**
 * Implements hook_menu().
 */
function testmodule_menu() {
  $items = array();

  $items['testmodule/ask-user'] = array(
    'page callback' => 'drupal_get_form',
    'page arguments' => array('testmodule_fruit_form'),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  
  return $items;
}

/**
 * Form constructor for our fruit form.
 * @param $form
 * @param $form_state
 */
function testmodule_fruit_form($form, &$form_state) {
  $fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Orange', 'Strawberry'];

  $form['favorite_fruit'] = array(
    '#type' => 'select',
    '#title' => t('Tell us your favorite fruit.'),
    '#required' => TRUE,
    '#options' => drupal_map_assoc($fruits)
  );

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

  return $form;
}

/**
 * Form submit handler.
 * @param $form
 * @param $form_state
 */
function testmodule_fruit_form_submit($form, &$form_state) {
  drupal_set_message(t('@fruit! Wow! Nice choice! Thanks for telling us!', array('@fruit' => $form_state['values']['favorite_fruit'])));
  $form_state['redirect'] = '<front>';
}

Nothing to it, right? Many modules will either use the above or similar patterns when needing to display a form at a specific URL.

However, in Drupal 8, hook_menu and many other hooks have been removed. Additionally, we can't directly access values in the $form_state array and drupal_map_assoc has been removed. We also can't overwrite and assign a value to $form_state['redirect'] anymore.

This doesn't mean that you can't implement custom forms or related functionality with the new release. It just means the approach for doing so has changed. For long-time Drupalers the changes may feel alien but with some practice and understanding you'll see why the paradigm shift in Drupal 8 is an important one for moving forward.

First, before Drupal 8 will even recognize your module, you will need to rename your testmodule.info file to testmodule.info.yml, which should look like:

name: Test Module
type: module
description: Sample module that defines a route and form callback.
core: 8.x

Secondly - and perhaps one of the biggest changes to get accustomed to - you do not need a module file (at least not for our purposes). You would still need one if you plan to use something like hook_form_alter but the main point to keep in mind is a lot of code you will write in Drupal 8 does not live in this file anymore.

Object-Oriented Programming, Autoloading, & Namespaces

If you are unfamiliar with object-oriented programming (OOP), you will need to get acquainted with it if you'd like to write or contribute code to the Drupal platform. However the rewritten Drupal core consisting of Symfony components provides easy-to-use interfaces, plugins, services, classes, and objects for extending Drupal. As a result the code is more structured, predictable, and testable. And trust me, once you start using this new framework, you will find it hard to return to Drupal 7.

So let's start with defining the path to our form. Since we can't use hook_menu, we need to create a routing file:

testmodule.fruit_form:
  path: '/testmodule/ask-user'
  defaults:
    _title: 'Favorite Fruit'
    _form: '\Drupal\testmodule\Form\FruitForm'
  requirements:
    _permission: 'access content'

testmodule.fruit_form defines a router item and can be used internally with a direct reference. We set the page title and controller for this route to \Drupal\testmodule\Form\FruitForm. By using the _form indicator, we are informing Drupal that this is a form based controller and the route will expect a controller that implements Drupal\Core\Form\FormInterface.

Now we need to identify a new location for our code as we can no longer use the .module file. Every module in Drupal 8 is consistent with how it declares/implements interfaces, plugins, classes, entities, and more. If you are not familiar with the PSR standards, you can learn more about them here.

First, we'll start by defining our form controller. You will need to create a src directory in your module and a Form directory within that. Create your FruitForm.php file here. Since Drupal 8 implements the PSR-4 autoloading standard, it will understand this directory structure.

modules
--- / testmodule
--- / --- / src
--- / --- / --- / Form
--- / --- / --- / --- / FruitForm.php

The next thing you will need to understand is the concept of namespaces and the use keyword.

Namespaces and the USE Keyword

In a nutshell, namespaces help define paths to your code and the autoloader, and the use keyword lets you alias and/or import namespaces for reference in your code.

If you did not alias Drupal\Core\Form\FormStateInterface, for example, your code may resemble something like:

namespace Drupal\testmodule\Form;

class FruitForm extends FormBase {
  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
    // @todo: Implement buildForm() method.
  }

It is highly advised that you employ the use keyword instead:

namespace Drupal\testmodule\Form;

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

class FruitForm extends FormBase {
  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // @todo: Implement the buildForm() method.
  }

use in this context tells PHP to import those classes and you can refer to them by class name instead of their full namespace path.

While this may be a basic example it provides you with an idea of what is possible with Drupal 8. Leveraging third party libraries is a lot easier now than in Drupal 7 thanks to advances in PHP 5.5+, the PSR-4 autoloading standard, and the great work being done by the Framework InterOp Group.

Bringing it All Together

Alright, let's define that form! A form should extend the FormBase class, which implements the FormInterface interface. We need to implement 4 methods to meet the requirement of FormInterface:

namespace Drupal\testmodule\Form;

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

class FruitForm extends FormBase {
  /**
   * {@inheritDoc}
   */
  public function getFormId() {

  }

  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

  }

  /**
   * {@inheritDoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {

  }

  /**
   * {@inheritDoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {

  }
}

As you may have guessed, construction, validation, and submission are now encapsulated within your class instead of in loosely defined procedural functions. Much of the FormAPI code is the same so we're able to port it from Drupal 7 with a few changes.

namespace Drupal\testmodule\Form;

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

class FruitForm extends FormBase {
  /**
   * {@inheritDoc}
   */
  public function getFormId() {
    return 'fruitform';
  }

  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Orange', 'Strawberry'];

    $form['favorite_fruit'] = array(
      '#type' => 'select',
      '#title' => $this->t('Tell us your favorite fruit.'),
      '#required' => true,
      '#options' => array_combine($fruits, $fruits)
    );

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

    return $form;
  }

  /**
   * {@inheritDoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // @todo: Implement validateForm() method.
  }

  /**
   * {@inheritDoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    drupal_set_message($this->t('@fruit! Wow! Nice choice! Thanks for telling us!', array('@fruit' => $form_state->getValue('favorite_fruit'))));
    $form_state->setRedirect('<front>');
  }
}

The buildForm() method is similar to the previously used drupal_get_form callback with some differences.

First, the second argument is passed to the function and must be an instance of FormStateInterface. $form_state in Drupal 8 is no longer just an array, it is an object with methods and properties. Gone are the days of accessing and modifying $form_state keys. We also cannot read submitted values directly but instead, must use getValue (or getValues). Finally, we have to use the setRedirect method for the same reasons. This enforcement ensures that the code adheres to a consistent standard when dealing with any class that extends FormBase.

You'll notice that the drupal_map_assoc function is no longer present in Drupal 8. However, array_combine can be used as a substitute as it serves the same purpose of creating an associative array. Putting it all together, we see our form when we visit the testmodule/ask-user URL in Drupal 8:

Sorry guys and gals, pizza is not a fruit.

Sweet! As a bonus, let's assume we wanted to redirect the user, following their submission, to some place other than the homepage (maybe 'create an account'?) We can do this using the router item instead of a raw URL. In fact, it is the enforced behavior in Drupal 8:

/**
   * {@inheritDoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    drupal_set_message($this->t('@fruit! Wow! Nice choice! Thanks for telling us! Create an account so we can send you weekly deals on fruit!', array('@fruit' => $form_state->getValue('favorite_fruit'))));

    $form_state->setRedirect('user.register');
  }

While this looks like a subtle change on the surface, think of the implications of NOT using a raw URL value here. By referring to the router item we are letting the system resolve the path. This prevents our redirect from breaking if the URL was to change.

If you do want to redirect to a path, this option is still available. Create a node and set its path as '/testmodule/thanks'. The FormState object has a setRedirectUrl method but we must pass our argument as an instance of Drupal\Core\Url - and you guessed it, we can import it using use and pass our node URL to it. In return, we get an instance of URL, which we can pass to setRedirectUrl.

namespace Drupal\testmodule\Form;

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

class FruitForm extends FormBase {
  // code redacted...

  /**
   * {@inheritDoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    drupal_set_message($this->t('@fruit! Wow! Nice choice! Thanks for telling us!', array('@fruit' => $form_state->getValue('favorite_fruit'))));

    $url = Url::fromUserInput('/testmodule/thanks');
    $form_state->setRedirectUrl($url);
  }
}

Next Steps

    We'll be covering other Drupal 8 concepts (like testing with SimpleTest, Behat, and PHPUnit, creating custom form elements, fields and field formatters, the DrupalConsole, etc.) in future posts in this series. Join the conversation by commenting below with any questions or feedback on using these Drupal 8 constructs. Until then...

    Get the Example Code

    Feel free to download this example and give it a whirl on your machine.

    Dive Deeper with Drupal 8