Creating Custom Forms in Drupal 8

Error message

The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes.

Drupal 8 is not far off from being released, and you may have heard some chatter about the differences in how you create custom modules. The reality is that, while there are some differences, it's not really that hard to wrap your head around them. This article provides a gentle introduction to creating forms in Drupal 8, and highlights the differences and similarities to how you would do this in previous versions of the platform.

Step 1: Create the Info File

Let’s start by creating a new custom module called Amazing Forms. Under modules, let's create a folder called amazing_forms, and within it let's create a yml file. A yml file (or YAML, which stands for "YAML Ain't Markup Language") is a file that specifies the configuration for a file.

Let's create a yml file called amazing_forms.info.yml. For all practical purposes, it's very similar to the .info files we used in Drupal 7. Let's put some basic information into the file, to describe the module and tell Drupal what it is supposed to do.

amazing_forms.info.yml:

name: Amazing forms
type: module
description: 'Demo for creating Drupal 8 forms.'
package: Custom
core: 8.x
hidden: false

The basic settings we are providing in this file should be recognizable to anyone who has created a custom module in Drupal 7. While there are a lot of other settings that could go into a yml file, we are really only concerned with the basics for this demonstration.

Step 2: Create the Files for the Module and the Controller

This step is where things start to get a little more interesting. In Drupal 7, we would simply create a module file and start coding. In Drupal 8, we do things a little differently.

Start off by creating a file called amazing_forms.module, and leave it empty for now. This is the module file we all know and love, and it's where most of our code will live.

The next thing we are going to do is create our forms. Instead of defining the forms in the custom module, we are going to create a custom controller to handle how the form works in our site. While the word controller may sound unfamiliar, it's not something to be afraid of. In PHP, controllers are really just elaborate functions with some special properties that make them easier to work with. When you use a controller class, you are technically creating an object, which has it's own functions (called methods) attached that you can use to manipulate through code.

To create our controller, we are going to create some subdirectories to store the code in a way Drupal can recognize. Create a directory called src, and within there, create another directory called Form (and mind the capitalization). This is where our controllers are going to live, and it's where we will be doing most of the work to create this module.

Step 3: Create the Controller

So, finally, we get to write some code. Within the src/Forms directory, create a new file called ContributeForm.php. This file is going to store the controller for our form. The controller is going to include some unique properties, as well as some methods that are very familiar to experienced Drupalists. Here's the basic skeleton for our controller.

<?php
/**
 * @file
 * Contains \Drupal\amazing_forms\Form\ContributeForm.
 */

namespace Drupal\amazing_forms\Form;
use
Drupal\Core\Form\FormBase;
use
Drupal\Core\Form\FormStateInterface;
use
Drupal\Component\Utility\UrlHelper;

/**
 * Contribute form.
 */
class ContributeForm 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) {
  }
}
?>

So, you will notice our controller is going to include a namespace, which is a unique name assigned to our module. Namespaces have a lot of uses, but the basic one everyone needs to understand is that they ensure code works together within a website. Next, you are going to notice some use statements. These statements are there to tell Drupal to include some other controllers before this one is called, to ensure they are available inside our controller.

Any time you are creating a form in your site, you can write a use statement for Drupal\Core\Form\FormBase. Any time you are creating a configuration form (aka an admin form) you also need to include a use statement for Drupal\Core\Form\ConfigFormBase.

While these conventions may seem new, there's a few familiar things in there as well. Our controller also includes 4 methods. Look at the names - getFormId, buildForm, validateForm and submitForm. These methods are going to be familiar to anyone who already knows Drupal's Form API, they are very similar to the handlers we used to develop around forms in Drupal 7. Even the variables are similar (array &$form, FormStateInterface $form_state).

Let's look at how each of these methods can work. getFormId is where we assign a unique id to the form being created, and it might look like this:

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

buildForm would be used to actually create the form, and it could be filled out like this:

<?php
 
public function buildForm(array $form, FormStateInterface $form_state) {
   
$form['title'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Title'),
     
'#required' => TRUE,
    );
   
$form['video'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Youtube video'),
    );
   
$form['video'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Youtube video'),
    );
   
$form['develop'] = array(
     
'#type' => 'checkbox',
     
'#title' => t('I would like to be involved in developing this material'),
    );
   
$form['description'] = array(
     
'#type' => 'textarea',
     
'#title' => t('Description'),
    );
   
$form['submit'] = array(
     
'#type' => 'submit',
     
'#value' => t('Submit'),
    );
    return
$form;
  }
?>

validateForm would be used to check the values in the form before they are saved and throw errors, similar to how you see here:

<?php
 
public function validateForm(array &$form, FormStateInterface $form_state) {
   
// Validate video URL.
   
if (!UrlHelper::isValid($form_state->getValue('video'), TRUE)) {
     
$form_state->setErrorByName('video', $this->t("The video url '%url' is invalid.", array('%url' => $form_state->getValue('video'))));
    }
  }
?>

And, finally, submitForm would be used to actually carry out the form submission process:

<?php
 
public function submitForm(array &$form, FormStateInterface $form_state) {
   
// Display result.
   
foreach ($form_state->getValues() as $key => $value) {
     
drupal_set_message($key . ': ' . $value);
    }
  }
?>

So, as you can see, the process for creating a form is actually conceptually similar to what we have been doing in Drupal with the Form API in previous versions. Controllers are really just a way of packaging the hooks we would have used in a form where they are more readable and easier to work with. Instead of writing a bunch of functions with names like amazing_forms_contribution_form_submit, we are using shorter, more declarative names that exist in their own namespace.

Step 4: Add a Router and Call it a Day

Now that you have added a controller to your custom module, you are almost done. The last thing to do is create a router file.

A router file is something that tells Drupal how to find controllers in your site. It's another yml file, and it contains information about the path and location of files. You can use these files to specify permissions and variables to be passed to the theming engine. Create a file called amazing_forms.routing.yml in the amazing_forms module directory, and include the following content:

amazing_forms.routing.yml

amazing_forms_contribute:
  path: 'amazing-forms/contribute'
  defaults:
    _form: '\Drupal\amazing_forms\Form\ContributeForm'
    _title: 'Conribute page'
  requirements:
    _permission: 'access content'

This code defines the path you use to access the form through a browser, the page title, the path for your controller and the necessary permissions for accessing the form.

Final thoughts

Creating custom modules in Drupal 8 may seem hard, but there's more there that's similar than what's new. Yes, object oriented programming is a complex subject, and this article only skims the surface. Practically, in most situations, a basic understanding of controllers, paths and yml file structures will allow you to get started working with the newest version of the platform. In exchange, you gain more power and flexibility around the architecture of your sites.