Skip to main content
Skip to main content

Hi, I'm Karim Boudjema. I'm a Senior Backend Developer living in Montréal, Canada, passionate about Drupal, AI and automated testing.

Create a config form in Drupal 11

Back in Drupal 7 we managed system variables with variable_get() / variable_set() / variable_del(), storing them in the variable table. Since Drupal 8 that job belongs to the Configuration system: a central place for modules to store settings that can be exported to YAML on disk and synchronised between environments. Configuration still lives in the database at runtime, but it can be deployed as files, the pattern this whole site is built on.

In this post we'll build a configuration form that stores one value, an external API key, and then read it back in a controller. Seven short sections:

  1. scaffold the module and the form,
  2. declare the config schema,
  3. write the config form,
  4. add the route,
  5. inspect the stored configuration,
  6. read the value back in a controller,
  7. bonus: read it in a hook.

1. Scaffold the module and the form

Drupal Console is gone; these days Drush 13 ships the code generator. Let's create the module and a configuration form:

drush generate module
drush generate form:config

The form:config generator writes the form class under src/Form/, adds a route in ex08.routing.yml, and (importantly for Drupal 11) reminds you to declare a config schema. Since Drupal 10, ConfigFormBase validates saved values against a schema, so a config form without one throws an error on submit.

2. The config schema

Create config/schema/ex08.schema.yml describing our configuration object and its single key:

ex08.externalapikey:
  type: config_object
  label: 'External API key settings'
  mapping:
    your_external_api_key:
      type: string
      label: 'Your external API key'

3. The config form

Here is the whole form in Drupal 11. It extends ConfigFormBase and overrides just four methods. Let's look at the full class first, then walk through the key ones.

<?php

declare(strict_types=1);

namespace Drupal\ex08\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Stores and edits the external API key.
 */
final class ExternalApiKeyForm extends ConfigFormBase {

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames(): array {
    return ['ex08.externalapikey'];
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'external_api_key_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['your_external_api_key'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Your external API key'),
      '#description' => $this->t('Store the external API key.'),
      '#maxlength' => 64,
      '#size' => 64,
      '#default_value' => $this->config('ex08.externalapikey')->get('your_external_api_key'),
    ];
    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $this->config('ex08.externalapikey')
      ->set('your_external_api_key', $form_state->getValue('your_external_api_key'))
      ->save();
    parent::submitForm($form, $form_state);
  }

}

ExternalApiKeyForm extends the abstract ConfigFormBase. Its four methods:

  • getEditableConfigNames(): the configuration object(s) this form may write; here ex08.externalapikey.
  • getFormId(): the form's unique ID, external_api_key_form.
  • buildForm(): builds the render array. Our single field, your_external_api_key, seeds its #default_value from the current config so the form shows what is already stored.
  • submitForm(): writes the submitted value back into config and calls parent::submitForm(), which shows the standard "configuration saved" message.

We no longer override validateForm() just to call the parent: that boilerplate is unnecessary. Note also that in Drupal 11 ConfigFormBase no longer needs a hand-written constructor to inject config.factory; the base class handles it. Fairly simple, isn't it?

4. The route

The generator wrote a route for the form in ex08.routing.yml:

ex08.external_api_key_form:
  path: '/admin/config/ex08/externalapikey'
  defaults:
    _form: 'Drupal\ex08\Form\ExternalApiKeyForm'
    _title: 'External API key'
  requirements:
    _permission: 'access administration pages'
  options:
    _admin_route: TRUE

path is where the form lives, _form names the class that builds it, _title is the page title, and _permission restricts access to users who may reach administration pages. Visit /admin/config/ex08/externalapikey to test it.

5. Inspect the stored configuration

With Drush 13 you read configuration objects straight from the CLI, no Drupal Console needed:

# List config object names that contain "ex08".
drush config:status
drush config:get ex08.externalapikey

6. Read the value back in a controller

To read a configuration value in a class we need the config.factory service. We inject it with the core AutowireTrait, so no hand-written create() method is required.

<?php

declare(strict_types=1);

namespace Drupal\ex08\Controller;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\AutowireTrait;

/**
 * Displays the stored external API key.
 */
final class ExternalApiKeyController extends ControllerBase {

  use AutowireTrait;

  public function __construct(
    protected readonly ConfigFactoryInterface $configFactory,
  ) {}

  /**
   * Shows the value stored under the "your_external_api_key" key.
   */
  public function showKey(): array {
    $key = $this->configFactory->get('ex08.externalapikey')->get('your_external_api_key');
    return [
      '#markup' => $this->t('The external API key is: @key', ['@key' => $key]),
    ];
  }

}

The original Drupal 8 example type-hinted Drupal\webprofiler\Config\ConfigFactoryWrapper, a debugging wrapper that should never appear in real code. The correct type is \Drupal\Core\Config\ConfigFactoryInterface, which is what we autowire here.

Add the controller route to ex08.routing.yml:

ex08.external_api_key_controller_show_key:
  path: '/ex08/show-key'
  defaults:
    _controller: 'Drupal\ex08\Controller\ExternalApiKeyController::showKey'
    _title: 'External API key'
  requirements:
    _permission: 'access content'

7. Bonus: read the value inside a hook

Sometimes you need a config value where dependency injection is awkward, for example inside a procedural hook. There the \Drupal static service wrapper is the pragmatic choice:

function ex08_form_alter(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, string $form_id): void {
  $key = \Drupal::config('ex08.externalapikey')->get('your_external_api_key');
  // ...
}

(In Drupal 11 you can also move hooks into an OOP class with the #[Hook] attribute and inject config.factory properly, but the static wrapper stays fine for quick reads.)

Recap. We generated a module and a ConfigFormBase form with drush generate, declared the mandatory config schema, stored a key/value pair in the ex08.externalapikey object, inspected it with drush config:get, and read it back in a controller through the autowired config.factory service.

How do you store your own API keys and secrets in Drupal? Let me know in the comments, I'm always curious how other people handle it.