Drupal 8 OOP Part 2: Creating an Admin Form

Posted By Hillary Lewandowski on Wednesday, November 4, 2015 - 14:24

In part 2 of my Object Oriented Programming (OOP) for Drupal 8 series, we are going to create an administration form. If you missed part 1, I talked about how to make a simple custom block in D8, which is something we do here at Commercial Progression to brand our sites. I chose a custom block to show that the new changes in D8 aren’t so scary, and to introduce object oriented concepts and definitions with a real-world example.

Another common example of custom Drupal code is to create a short administration form so that configurations can be easily modified in the UI. As well as incorporating an OOP lesson, this example will also illustrate how Symfony routing works (the hook_menu() replacement), and how the variables system will be replaced in Drupal 8.

Compro Custom: D7 Style

The following code is part of our Compro Custom module that saves a title for the site logo.

  1. /**
  2.  * Implements hook_menu().
  3.  */
  4. function compro_custom_menu() {
  5.  $items['admin/config/system/compro-custom'] = array(
  6.    'title' => 'Compro Custom',
  7.    'page callback' => 'drupal_get_form',
  8.    'page arguments' => array('compro_custom_admin'),
  9.    'access arguments' => array('access administration pages'),
  10.  );
  11.  return $items;
  12. }
  13.  
  14. /**
  15.  * Menu callback function for compro_custom administration.
  16.  */
  17. function compro_custom_admin($form, &$form_state) {
  18.  // Load module vars
  19.  $compro_custom = variable_get('compro_custom', '');
  20.  $site_name = filter_xss_admin(variable_get('site_name', 'Drupal'));
  21.  
  22.  // Logo settings for theme override.
  23.  $form['compro_custom']['logo'] = array(
  24.  'title' => array(
  25.    '#type' => 'textfield',
  26.    '#title' => t('Title text'),
  27.    '#maxlength' => 255,
  28.    '#default_value' => isset($compro_custom['logo']['title']) ? $compro_custom['logo']['title'] : $site_name . ' home',
  29.    '#description' => t('What the tooltip should say when you hover on the logo.'),
  30.  ),
  31.  
  32.  );
  33.  return system_settings_form($form);
  34. }

Before diving right into the same admin form code for D8, I’ll describe the changes to the Menu API.

Routing

Here’s where we get a glimpse into the Symfony world. A common feature in a lot of OO web frameworks is the implementation of a routing system. This is a separate file that links a path to its Controller (as part of the Model-View-Controller pattern). Instead of using hook_menu() to set up a URL path, we put it in our compro_custom.routing.yml file. It uses a lot of the same settings that we are used to: a path, title, permission, and the form we use.

  1. compro_custom.compro_custom_form:
  2.  path: '/admin/config/system/compro-custom'
  3.  defaults:
  4.    _form: 'Drupal\compro_custom\Form\ComproCustomForm'
  5.    _title: 'Compro Custom'
  6.  requirements:
  7.    _permission: 'administer site configuration'

Menu API

In order to see a link to our form under Configuration in the admin toolbar, we need to add another file called compro_custom.links.menu.yml to define the menu link.

  1. compro_custom.compro_custom_form:
  2.  title: 'Compro Custom'
  3.  parent: system.admin_config_development
  4.  route_name: compro_custom.compro_custom_form
  5.  weight: 10

We are referencing back to the first line of our routing file with route_name to give our link a direction. Using the parent attribute, we can specify that we want to nest it under the system.admin_config_development menu item. If we wanted to nest another link under this menu item, the parent attribute would be the first line of this file: compro_custom.compro_custom_form.

The D8 Form API

Here are the contents of ComproCustomForm.php. A lot of these methods use {@inheritdoc} in the PHPDoc comment, but I’ve supplied the actual comment they’re inheriting for clarity.

  1. /**
  2.  * @file
  3.  * Contains \Drupal\compro_custom\Form\ComproCustomForm.
  4.  */
  5.  
  6. namespace Drupal\compro_custom\Form;
  7. use Drupal\Core\Config\ConfigFactoryInterface;
  8. use Drupal\Core\Form\FormStateInterface;
  9. use Drupal\Core\Form\ConfigFormBase;
  10.  
  11. /**
  12.  * Configure custom settings for this site.
  13.  */
  14. class ComproCustomForm extends ConfigFormBase {
  15.  
  16. /**
  17.  * Constructor for ComproCustomForm.
  18.  *
  19.  * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  20.  *   The factory for configuration objects.
  21.  */
  22.  public function __construct(ConfigFactoryInterface $config_factory) {
  23.    parent::__construct($config_factory);
  24.  }
  25.  
  26. /**
  27.  * Returns a unique string identifying the form.
  28.  *
  29.  * @return string
  30.  *   The unique string identifying the form.
  31.  */
  32.  public function getFormId() {
  33.    return 'compro_custom_admin_form';
  34.  }
  35.  
  36. /**
  37.  * Gets the configuration names that will be editable.
  38.  *
  39.  * @return array
  40.  *   An array of configuration object names that are editable if called in
  41.  *   conjunction with the trait's config() method.
  42.  */
  43.  protected function getEditableConfigNames() {
  44.    return ['config.compro_custom'];
  45.  }
  46.  
  47. /**
  48.  * Form constructor.
  49.  *
  50.  * @param array $form
  51.  *   An associative array containing the structure of the form.
  52.  * @param \Drupal\Core\Form\FormStateInterface $form_state
  53.  *   The current state of the form.
  54.  *
  55.  * @return array
  56.  *   The form structure.
  57.  */
  58.  public function buildForm(array $form, FormStateInterface $form_state) {
  59.    $compro_custom = $this->config('config.compro_custom');
  60.    $site_name = $this->config('system.site')->get('name');
  61.  
  62.    // Logo settings for theme override.
  63.    $form['compro_custom']['logo'] = array(
  64.      'title' => array(
  65.        '#type' => 'textfield',
  66.        '#title' => t('Title text'),
  67.        '#maxlength' => 255,
  68.        '#default_value' => $compro_custom->get('logo_title') ? $compro_custom->get('logo_title') : $site_name . ' home',
  69.        '#description' => t('What the tooltip should say when you hover on the logo.'),
  70.      ),
  71.    );
  72.    return parent::buildForm($form, $form_state);
  73.  }
  74.  
  75. /**
  76.  * Form submission handler.
  77.  *
  78.  * @param array $form
  79.  *   An associative array containing the structure of the form.
  80.  * @param \Drupal\Core\Form\FormStateInterface $form_state
  81.  *   The current state of the form.
  82.  */
  83.  public function submitForm(array &$form, FormStateInterface $form_state) {
  84.    $this->config('config.compro_custom')
  85.      ->set('logo_title', $form_state->getValue(array('compro_custom', 'logo', 'title')))
  86.      ->save();
  87.    parent::submitForm($form, $form_state);
  88.  }
  89. }

We can see that the hooks are gone, and instead we are implementing an entirely new class for a system admin form. We still build the form in the same array structure we are used to, but now we are implementing the method buildForm(). In Drupal 8 we are also able to set our own form ID as part of building a system form.

Another big change to admin forms in particular is that we can no longer rely on a built-in form submission with system_settings_form($form); we have to implement our own submitForm() method. In this method, we map the values from $form_state into a configuration object. Our $form_state object allows us to call it’s getValue() method, and pass it an array indicating the form value we want. Because we nested our logo title in $form['compro_custom']['logo']['title'], we can retrieve that value by passing in the multi-element array('compro_custom', 'logo', 'title').

Configuration System

The configuration system (as well as the state system) has replaced the variable system in D8. This means there is no more variable table, and no more variable_get() or variable_set() calls. In our form class, we extended the ConfigFormBase class so that we can save our data in the config table. When we __construct() our form we accept a ConfigFactoryInterface, and we also implement a getEditableConfigNames() method to set up our configuration object before saving these values during submit.

One more step we must take to ensure that our configurations are stored correctly is to provide a mapping schema for the configuration system. The schema describes the overall structure of the configuration object, and its data types. It is also useful for describing which of its values is translatable, but that is not shown in this example. We will put this in the compro_custom.schema.yml file.

  1. compro_custom.logo:
  2.  type: config_object
  3.  label: 'Custom header logo settings'
  4.  mapping:
  5.    title:
  6.      type: text
  7.      label: 'Title text'

Interfaces

You might have noticed that our __construct(ConfigFactoryInterface $config_factory) method looks really strange - why is our parameter an interface? In part 1, I explained very briefly that an interface was a list of methods its classes were required to implement. An interface doesn’t contain any implementation, just method signatures. When we include an interface as a parameter, it doesn’t really mean it needs an interface, but instead it uses a concrete class that implements ConfigFactoryInterface. At the time of this writing, the only class that implements ConfigFactoryInterface is ConfigFactory. Passing in an interface instead of the concrete class is useful if you wanted to implement your own config factory class without rewriting every class that will be passing it as a parameter.

Factory Classes

The purpose of a factory class is to create an object. Factory classes are a design pattern that allow a client to create objects without knowledge of how they are made - the client just accesses the methods defined in the factory interface to produce the desired object. In our example, the job of ConfigFactory is to initialize a configuration object from the schema we defined in our compro_custom.schema.yml file.

Polymorphism

Polymorphism is an advanced OOP practice, but this is a great example of how it's done. Polymorphism is a very broad term that refers to interacting with different classes in the same way. Using an interface is one of the most common methods of achieving polymorphism. One of the big advantages of object oriented programming is the multitude of ways in which code can be reused.

Longer, But Better

Unfortunately in this example, the object oriented Drupal 8 implementation produced quite a few more lines of code than the procedural Drupal 7 version. But we also saw that in Drupal 8 we are seeing more separation of concerns between the menu structure, system configurations, et al. Drupal 8’s OOP practices will also help future development go smoothly with more abstract code that is harder to break (if used correctly).

Drupal 8 website design and development by Commercial Progression