Upgrading A Drupal 7 Module to Drupal 8: Configuration Forms

Published: April 20, 2017 By

In the last post in this series, we set up some routing for our module for three paths. One of those paths is to the module's main configuration form. Since this module has a Drupal 7 version, I am going to go by the old tried and true method of CDD, a.k.a Copy Driven Development. Copy, paste, cry, try to copy something else.

By the end of this post, we will have a proper configuration form that loads and saves values correctly. My form will need to interact with users and roles, but we will dip our toes into those Drupal sub-systems another time.

Form API in D8

Luckily for all of you hardcore D7 devs out there, the way forms are built in Drupal 8 hasn't changed as much as other systems, like routing, have changed.

The first thing I had to change was the "#default_value" key on my form items. Most of the time I used "variable_get()" to set default values, and as you've probably seen a thousand times already, that function has been deprecated.


// Old code...
variable_get('user_external_invite_days_valid_for', 5);

// New code...
$config = $this->config('user_external_invite.settings');
$config->get('user_external_invite_days_valid_for');

Default values are now stored in your config directory, which for me was "config/install/user_external_invite.settings.yml" from my module's root directory. All of your default values end up being plugged into that YAML file as you'd think they would.

Since my YAML experience is limited, I ran into an issue with a multi-line string I needed to enter. When trying to add the string as I had it in Drupal 7, wrapped in quotes and using default line breaks instead of "\n", I got an error when trying to install the module. StackOverflow came to the rescue once again, and while I think the syntax is a bit wonky, I can now add multi-line strings without breaking formatting.  


user_external_invite_days_valid_for: 5
user_external_invite_delete_old_invites: 2592000
user_external_invite_no_query_params: FALSE
user_external_invite_use_universal_from_email: FALSE
user_external_invite_universal_from_email:
user_external_invite_confirmation_template: |
  [user_external_invite:invite_custom]

  You have been invited to join the [site:name] website as a user with [user_external_invite:invite_role] access privileges.

You essentially just use the pipe symbol and have to make sure the multi-line string is indented two spaces from its parent YAML key.

Element Validation Handlers

I ran into another issue with a "#element_validate" key on a form element. In Drupal 7 if you need to check and see if a user entered a number, you could use a core function to do so without needing to add a custom validation handler. 


// Days invite valid for.
$form['user_external_invite_days_valid_for'] = array(
  '#type' => 'textfield',
  '#title' => t('Number of days invites are valid'),
  '#description' => t("Invites are set to expire so many days after they are created. If a user hasn't accepted the invite by that time, then you will have to send a new invite to grant that user a role."),
  '#default_value' => variable_get('user_external_invite_days_valid_for', 5),
  '#element_validate' => array('element_validate_number'),
  '#maxlength' => 3,
);

The "element_validate_number()" function was included in Drupal 7 core along with several other validation functions. If you wanted to create a custom function, you could add it in just like a submit handler. "element_validate_number" becomes "your_validation_function", and you place your validation function somewhere around your form definition. 

In Drupal 8, you need to make a slight change to how you declare custom validation handlers for elements.


// Drupal 7...
'#element_validate' => array('element_validate_number'),

// Drupal 8...
'#element_validate' => array(array($this, 'elementValidateNumber')),

In Drupal 8, you need to use a valid callback for your validation handler to work. When a user submits the form, the "call_user_func_array(callable $callback , array $param_arr )" function is called. The array within the array syntax is used to be able to call a function within a class. Since our validation function exists within our own form class, we can pass in "$this" and then the function we are calling. It is also possible to pass in another external class where your validation function might live. You can read the change record for more information and discussion on form handler changes in Drupal 8.  

For my specific case, these validation handlers aren't even needed. Since I'm wanting the user to only enter numbers, I can use the new "number" type on the form element. Once the type is set to number, a user can't even enter letters or special characters: only numerals are allowed. The "#step" key allows you to specify what values you will accept. With that key set to "1", I can't enter "3.564" and get a nice warning message before submitting the form. You should use these new form elements over text fields whenever possible so that browsers can use built-in validation checking and element rendering.  


// Days invite valid for.
$form['user_external_invite_days_valid_for'] = array(
  '#type' => 'number',
  '#title' => t('Number of days invites are valid'),
  '#description' => t("Invites are set to expire so many days after they are created. If a user hasn't accepted the invite by that time, then you will have to send a new invite to grant that user a role."),
  '#default_value' => $config->get('user_external_invite_days_valid_for'),
  '#min' => 1,
  '#step' => 1,
);

Submit Form Handlers

My next step is to save my form values so they can be used elsewhere and loaded as default values the next time the configuration form is loaded. Since "ConfigFormBase" already has a "submitForm(array &$form, FormStateInterface $form_state)" function, we can extend that to suit our purposes. 


  
  // Form definition code...
  
  // Submit button.
  $form['actions'] = ['#type' => 'actions'];
  $form['actions']['submit'] = [
    '#type' => 'submit',
    '#value' => $this->t('Save configuration'),
  ];

  return parent::buildForm($form, $form_state);
}

public function submitForm(array &$form, FormStateInterface $form_state) {
  $config = $this->config('user_external_invite.settings');
  $values = $form_state->getValues();

  // Loop through and save form values.
  foreach ($values as $key => $value) {
    // Since there are non-user input values, we need to check for the prefix
    // added to all variables to filter out values we don't want to save.
    if (strpos($key, 'user_external_invite') !== FALSE) {
      $config->set($key, $value);
    }
  }

  // Save form values to be loaded as defaults.
  $config->save();

  parent::submitForm($form, $form_state);
}

Getting the config object and form values is relatively straightforward; however, saving the values in a concise manner gets a little more involved. The values returned from "$form_state->getValues()" end up containing things like the submit object that you don't need or want to save as a value. Luckily for us procedural devs with habits of defensive function and variable naming, you can check for a prefix you placed on each variable and only save those values. Since I had prefixed my variables in Drupal 7, I didn't have to change much to save the values I cared about saving. 

Also to note, you don't need to declare "$form['actions']['submit']" for the submit button to show up because it is added by the parent function. It is always good to check what the parent function does when you are yielding control back to it. 

You should now be able to define form elements, create custom validation handlers, and save values in a standard submit handler. For most configuration forms, this is all the code knowledge you will need. For more information, please read the Drupal 8 introduction to the Form API.