Build your first module 4: configuration and schema
Hard-coded behavior makes a demo; configurable behavior makes a module. This part gives Event RSVP its settings: which content types accept RSVPs, and what the confirmation message says. You ship defaults, describe their structure with configuration schema, and build the admin form to change them.
What you’ll build
Section titled “What you’ll build”Part 3 left one visible gap: the empty confirmation message after submitting an RSVP. This part fills it with four files: config/install/event_rsvp.settings.yml (the defaults imported at install), config/schema/event_rsvp.schema.yml (the typed description of those settings), src/Form/RsvpSettingsForm.php (the form at /admin/config/services/event-rsvp, on the route you declared back in part 2), and event_rsvp.links.menu.yml (the link that lists the form on the Configuration page).
Prerequisites
Section titled “Prerequisites”Step 1: Ship default configuration
Section titled “Step 1: Ship default configuration”Simple configuration is named key-value data, stored by name. This module owns one config object, event_rsvp.settings. Whatever a module ships in config/install/ gets imported once, when the module is installed; the file name is the config name. Create web/modules/custom/event_rsvp/config/install/event_rsvp.settings.yml:
# Default configuration, imported once when the module is installed.# The language the translatable values below are written in.langcode: en# Machine names of the content types that accept RSVPs.enabled_types: - article# Message shown after a successful RSVP.confirmation_message: 'Thanks! Your RSVP has been recorded.'langcode records the language the translatable values are written in; Drupal requires it on any config object that holds translatable text. enabled_types holds content type machine names; article matches the node you have been RSVPing to. confirmation_message is the text part 3’s submit handler already reads.
“Imported once” is the operative phrase. After installation, the active configuration lives in the database, owned by the site, editable by administrators, and exportable for deployment. Drupal never re-reads the file, and editing it on an installed site does nothing. That split between a module’s defaults and a site’s live config is the heart of the configuration system; Config, content, state, and sessions draws the full map.
Step 2: Describe the configuration with schema
Section titled “Step 2: Describe the configuration with schema”Configuration files are YAML, and YAML cannot say what anything means: is enabled_types a list of strings or a single string? Is confirmation_message translatable? Configuration schema answers those questions. Create web/modules/custom/event_rsvp/config/schema/event_rsvp.schema.yml:
# Configuration schema describes the structure and types of the module's# configuration so it can be validated, translated and typed.event_rsvp.settings: type: config_object label: 'Event RSVP settings' mapping: enabled_types: type: sequence label: 'Content types that accept RSVPs' sequence: type: string label: 'Content type machine name' confirmation_message: type: label label: 'Confirmation message'The top-level key matches the config name. Under it:
type: config_objectmarks this as a root simple-config object.mappingdeclares the allowed keys and a type for each. Unknown keys in the config become schema errors.enabled_typesis asequence(a list) whose items are typestring.confirmation_messageis typelabel: a short, translatable, single-line string. Choosinglabeloverstringis what tells the interface-translation system this text is translatable.
These types come from core’s data type registry and are resolved by the \Drupal\Core\Config\TypedConfigManager, the service behind typed configuration. Schema is not paperwork; it is enforced. The kernel and functional tests you write in part 7 fail if the module saves configuration that does not match its schema, the settings form base class uses the typed manager (step 4), and validation constraints attach to schema types. Undescribed config is a bug you have not met yet.
Step 3: Reinstall to import the defaults
Section titled “Step 3: Reinstall to import the defaults”Like hook_schema() in part 3, config/install/ only takes effect at install time. Reinstall the module:
ddev drush pm:uninstall -y event_rsvpddev drush pm:install -y event_rsvpUninstalling deletes the module’s configuration and drops its table, so your test RSVP from part 3 is gone too; the table comes back empty. RSVP to DrupalCon again at /node/1/rsvp and notice the difference: the green status box now says “Thanks! Your RSVP has been recorded.” The submit handler’s $this->config('event_rsvp.settings')->get('confirmation_message') finally has something to read.
Step 4: Build the settings form
Section titled “Step 4: Build the settings form”Administrators should not need Drush to change a message. The event_rsvp.settings route from part 2 points at Drupal\event_rsvp\Form\RsvpSettingsForm; create src/Form/RsvpSettingsForm.php:
<?php
declare(strict_types=1);
namespace Drupal\event_rsvp\Form;
use Drupal\Core\Config\ConfigFactoryInterface;use Drupal\Core\Config\TypedConfigManagerInterface;use Drupal\Core\Entity\EntityTypeManagerInterface;use Drupal\Core\Form\ConfigFormBase;use Drupal\Core\Form\ConfigTarget;use Drupal\Core\Form\FormStateInterface;use Symfony\Component\DependencyInjection\ContainerInterface;
/** * Configures which content types accept RSVPs. */final class RsvpSettingsForm extends ConfigFormBase {
/** * Constructs the settings form. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager * The typed config manager. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The entity type manager, used to list content types. */ public function __construct( ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, protected readonly EntityTypeManagerInterface $entityTypeManager, ) { parent::__construct($config_factory, $typedConfigManager); }
/** * {@inheritdoc} */ public static function create(ContainerInterface $container): static { return new static( $container->get('config.factory'), $container->get('config.typed'), $container->get('entity_type.manager'), ); }
/** * {@inheritdoc} */ public function getFormId(): string { return 'event_rsvp_settings_form'; }
/** * {@inheritdoc} */ protected function getEditableConfigNames(): array { return ['event_rsvp.settings']; }
/** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state): array { $options = []; $node_types = $this->entityTypeManager->getStorage('node_type')->loadMultiple(); foreach ($node_types as $type) { $options[$type->id()] = $type->label(); }
$form['enabled_types'] = [ '#type' => 'checkboxes', '#title' => $this->t('RSVP-enabled content types'), '#description' => $this->t('Visitors can RSVP to content of these types.'), '#options' => $options, // Checkboxes submit [type => type|0]; store only the checked ones. '#config_target' => new ConfigTarget( 'event_rsvp.settings', 'enabled_types', toConfig: fn (array $value): array => array_keys(array_filter($value)), ), ]; $form['confirmation_message'] = [ '#type' => 'textfield', '#title' => $this->t('Confirmation message'), '#config_target' => 'event_rsvp.settings:confirmation_message', '#required' => TRUE, ];
return parent::buildForm($form, $form_state); }
}This form differs from part 3’s RsvpForm in instructive ways:
- It extends
\Drupal\Core\Form\ConfigFormBase, whose constructor takes the config factory and the typed config manager. The promoted third parameter,EntityTypeManagerInterface, is this form’s own addition, used to list the site’s content types, and the constructor passes the first two up withparent::__construct(). When you extend a base class with its own dependencies, yourcreate()supplies theirs and yours. getEditableConfigNames()declares which config objects the form may write.- Each element maps itself to a config value with
#config_target. The string'event_rsvp.settings:confirmation_message'names the config object and the key. From that mapping,ConfigFormBaseloads the default value from active config, validates the submitted value against the schema from step 2, and saves it, so the form has nosubmitForm()of its own. Mapped elements are also override-aware: a value forced insettings.phpis flagged to the administrator. - The checkboxes element needs a transformation on the way in, because it submits unchecked types as
0values. The\Drupal\Core\Form\ConfigTargetobject’stoConfigcallback filters the submission down to the checked machine names before they are stored. buildForm()ends by callingparent::buildForm(), which adds the Save configuration button and the standard config-form theming. The parent’s submit handler saves the mapped values and shows the standard “The configuration options have been saved.” message.
Administrators find settings forms through the Configuration page, which lists menu links rather than routes. Give the form one in web/modules/custom/event_rsvp/event_rsvp.links.menu.yml:
event_rsvp.settings: title: 'Event RSVP settings' description: 'Choose which content types accept RSVPs.' route_name: event_rsvp.settings parent: system.admin_config_servicesparent: system.admin_config_services files the link under the Web services section; Menus and links covers the rest of the format.
Rebuild caches so the form class and the menu link are found, then visit the form:
ddev drush cache:rebuildGo to Administer → Configuration → Web services → Event RSVP settings/admin/config/services/event-rsvp. You see “Article” checked, the other content types unchecked, and the confirmation message from your defaults. This is also the moment part 1’s configure: event_rsvp.settings info-file line pays off: the Extend page now shows a Configure link for Event RSVP.
Troubleshooting
Section titled “Troubleshooting”config:get says the configuration does not exist
Section titled “config:get says the configuration does not exist”The module was not reinstalled after you created config/install/, or the file is misnamed. The path must be exactly config/install/event_rsvp.settings.yml. Repeat step 3.
Installing the module fails with “configuration objects … already exist”
Section titled “Installing the module fails with “configuration objects … already exist””Configuration left behind from an earlier attempt. ddev drush config:delete event_rsvp.settings, then install again.
The settings form 404s
Section titled “The settings form 404s”The route exists since part 2, but the class behind it was only created now; run ddev drush cache:rebuild. If it still 404s, re-check the class name and file path against the route’s _form value.
Every content type shows unchecked after saving
Section titled “Every content type shows unchecked after saving”You probably saved with nothing checked: array_filter() keeps only checked boxes, so an all-unchecked submission stores an empty list and disables RSVPs everywhere. Re-check “Article” and save.
Next steps
Section titled “Next steps”- Part 5: a block plugin with cache tags puts the RSVP count on the page and makes caching work for you.
- Config, content, state, and sessions explains which storage each kind of data belongs in.
- Configuration management workflow covers exporting, importing, and deploying configuration.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.