class ConfigFormBase
Same name in other branches
- 9 core/lib/Drupal/Core/Form/ConfigFormBase.php \Drupal\Core\Form\ConfigFormBase
- 8.9.x core/lib/Drupal/Core/Form/ConfigFormBase.php \Drupal\Core\Form\ConfigFormBase
- 10 core/lib/Drupal/Core/Form/ConfigFormBase.php \Drupal\Core\Form\ConfigFormBase
Base class for implementing system configuration forms.
Subclasses of this form can choose to use config validation instead of form- -specific validation logic. To do that, override copyFormValuesToConfig().
Hierarchy
- class \Drupal\Core\Form\FormBase implements \Drupal\Core\Form\FormInterface, \Drupal\Core\DependencyInjection\ContainerInjectionInterface uses \Drupal\Core\DependencyInjection\DependencySerializationTrait, \Drupal\Core\Logger\LoggerChannelTrait, \Drupal\Core\Messenger\MessengerTrait, \Drupal\Core\Routing\RedirectDestinationTrait, \Drupal\Core\StringTranslation\StringTranslationTrait
- class \Drupal\Core\Form\ConfigFormBase extends \Drupal\Core\Form\FormBase uses \Drupal\Core\Form\ConfigFormBaseTrait
Expanded class hierarchy of ConfigFormBase
38 files declare their use of ConfigFormBase
- AccountSettingsForm.php in core/
modules/ user/ src/ AccountSettingsForm.php - AdvancedSettingsForm.php in core/
modules/ views_ui/ src/ Form/ AdvancedSettingsForm.php - BasicSettingsForm.php in core/
modules/ views_ui/ src/ Form/ BasicSettingsForm.php - BookSettingsForm.php in core/
modules/ book/ src/ Form/ BookSettingsForm.php - ConfigTargetTest.php in core/
tests/ Drupal/ Tests/ Core/ Form/ ConfigTargetTest.php
File
-
core/
lib/ Drupal/ Core/ Form/ ConfigFormBase.php, line 21
Namespace
Drupal\Core\FormView source
abstract class ConfigFormBase extends FormBase {
use ConfigFormBaseTrait;
/**
* The $form_state key which stores a map of config keys to form elements.
*
* This map is generated and stored by ::storeConfigKeyToFormElementMap(),
* which is one of the form's #after_build callbacks.
*
* @see ::storeConfigKeyToFormElementMap()
*
* @var string
*/
protected const CONFIG_KEY_TO_FORM_ELEMENT_MAP = 'config_targets';
/**
* Constructs a \Drupal\system\ConfigFormBase object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
* The typed config manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager) {
$this->setConfigFactory($config_factory);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('config.factory'), $container->get('config.typed'));
}
/**
* Returns the typed config manager service.
*
* @return \Drupal\Core\Config\TypedConfigManagerInterface
* The typed config manager service.
*/
protected function typedConfigManager() : TypedConfigManagerInterface {
if ($this->typedConfigManager instanceof TypedConfigManagerInterface) {
return $this->typedConfigManager;
}
return \Drupal::service('config.typed');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save configuration'),
'#button_type' => 'primary',
];
// By default, render the form using system-config-form.html.twig.
$form['#theme'] = 'system_config_form';
// Load default values from config into any element with a #config_target
// property.
$form['#process'][] = '::loadDefaultValuesFromConfig';
$form['#after_build'][] = '::storeConfigKeyToFormElementMap';
$form['#after_build'][] = '::checkConfigOverrides';
return $form;
}
/**
* Process callback to recursively load default values from #config_target.
*
* @param array $element
* The form element.
*
* @return array
* The form element, with its default value populated.
*/
public function loadDefaultValuesFromConfig(array $element) : array {
if (array_key_exists('#config_target', $element) && !array_key_exists('#default_value', $element)) {
$target = $element['#config_target'];
if (is_string($target)) {
$target = ConfigTarget::fromString($target);
}
$config = $this->configFactory()
->getEditable($target->configName);
$element['#default_value'] = $target->getValue($config);
}
foreach (Element::children($element) as $key) {
$element[$key] = $this->loadDefaultValuesFromConfig($element[$key]);
}
return $element;
}
/**
* #after_build callback which stores a map of element names to config keys.
*
* This will store an array in the form state whose keys are strings in the
* form of `CONFIG_NAME:PROPERTY_PATH`, and whose values are instances of
* \Drupal\Core\Form\ConfigTarget.
*
* This callback is run in the form's #after_build stage, rather than
* #process, to guarantee that all of the form's elements have their final
* #name and #parents properties set.
*
* @param array $element
* The element being processed.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* The processed element.
*
* @see \Drupal\Core\Form\ConfigFormBase::buildForm()
*/
public function storeConfigKeyToFormElementMap(array $element, FormStateInterface $form_state) : array {
// Empty the map to ensure the information is always correct after
// rebuilding the form.
$form_state->set(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP, []);
return $this->doStoreConfigMap($element, $form_state);
}
/**
* Helper method for #after_build callback ::storeConfigKeyToFormElementMap().
*
* @param array $element
* The element being processed.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* The processed element.
*
* @see \Drupal\Core\Form\ConfigFormBase::storeConfigKeyToFormElementMap()
*/
protected function doStoreConfigMap(array $element, FormStateInterface $form_state) : array {
if (array_key_exists('#config_target', $element)) {
$map = $form_state->get(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP) ?? [];
/** @var \Drupal\Core\Form\ConfigTarget|string $target */
$target = $element['#config_target'];
if (is_string($target)) {
$target = ConfigTarget::fromString($target);
}
elseif ($target->toConfig instanceof \Closure || $target->fromConfig instanceof \Closure) {
// If the form is using closures as toConfig or fromConfig callables
// then form cannot be cached.
$form_state->disableCache();
}
foreach ($target->propertyPaths as $property_path) {
if (isset($map[$target->configName][$property_path])) {
throw new \LogicException(sprintf('Two #config_targets both target "%s" in the "%s" config: `%s` and `%s`.', $property_path, $target->configName, '$form[\'' . implode("']['", $map[$target->configName][$property_path]) . '\']', '$form[\'' . implode("']['", $element['#array_parents']) . '\']'));
}
$map[$target->configName][$property_path] = $element['#array_parents'];
}
$form_state->set(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP, $map);
}
foreach (Element::children($element) as $key) {
$element[$key] = $this->doStoreConfigMap($element[$key], $form_state);
}
return $element;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$map = $form_state->get(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP) ?? [];
foreach (array_keys($map) as $config_name) {
$config = $this->configFactory()
->getEditable($config_name);
static::copyFormValuesToConfig($config, $form_state, $form);
$typed_config = $this->typedConfigManager()
->createFromNameAndData($config_name, $config->getRawData());
$violations = $typed_config->validate();
// Rather than immediately applying all violation messages to the
// corresponding form elements, first collect the messages. The structure
// of the form may cause a single form element to contain multiple config
// property paths thanks to `type: sequence`. Common example: a <textarea>
// with one line per sequence item.
// @see \Drupal\Core\Config\Schema\Sequence
// @see \Drupal\Core\Config\Schema\SequenceDataDefinition
$violations_per_form_element = [];
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
foreach ($violations as $violation) {
$property_path = $violation->getPropertyPath();
// Default to index 0.
$index = 0;
// Detect if this is a sequence item property path, and if so, attempt
// to fall back to the containing sequence's property path.
if (!isset($map[$config_name][$property_path]) && preg_match("/.*\\.(\\d+)\$/", $property_path, $matches) === 1) {
$index = intval($matches[1]);
// The property path as known in the config key-to-form element map
// will not have the sequence index in it.
$property_path = rtrim($property_path, '0123456789.');
}
if (isset($map[$config_name][$property_path])) {
$config_target = ConfigTarget::fromForm($map[$config_name][$property_path], $form);
$form_element_name = implode('][', $config_target->elementParents);
}
else {
// We cannot determine where to place the violation. The only option
// is the entire form.
$form_element_name = '';
}
$violations_per_form_element[$form_element_name][$index] = $violation;
}
// Now that we know how many constraint violation messages exist per form
// element, set them. This is crucial because FormState::setErrorByName()
// only allows a single validation error message per form element.
// @see \Drupal\Core\Form\FormState::setErrorByName()
foreach ($violations_per_form_element as $form_element_name => $violations) {
// When only a single message exists, just set it.
if (count($violations) === 1) {
$form_state->setErrorByName($form_element_name, reset($violations)->getMessage());
continue;
}
// However, if multiple exist, that implies it's a single form element
// containing a `type: sequence`.
$form_state->setErrorByName($form_element_name, $this->formatMultipleViolationsMessage($form_element_name, $violations));
}
}
}
/**
* Formats multiple violation messages associated with a single form element.
*
* Validation constraints only know the internal data structure (the
* configuration schema structure), but this need not be a disadvantage:
* rather than informing the user some values are wrong, it is possible
* guide them directly to the Nth entry in the sequence.
*
* To further improve the user experience, it is possible to override
* method in subclasses to use specific knowledge about the structure of the
* form and the nature of the data being validated, to instead generate more
* precise and/or shortened violation messages.
*
* @param string $form_element_name
* The form element for which to format multiple violation messages.
* @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
* The list of constraint violations that apply to this form element.
*
* @return \Drupal\Component\Render\MarkupInterface|\Stringable
* The rendered HTML.
*/
protected function formatMultipleViolationsMessage(string $form_element_name, array $violations) : MarkupInterface|\Stringable {
$transformed_message_parts = [];
foreach ($violations as $index => $violation) {
// Note that `@validation_error_message` (should) already contain a
// trailing period, hence it is intentionally absent here.
$transformed_message_parts[] = $this->t('Entry @human_index: @validation_error_message', [
// Humans start counting from 1, not 0.
'@human_index' => $index + 1,
// Translators may not necessarily know what "violation constraint
// messages" are, but they definitely know "validation errors".
'@validation_error_message' => $violation->getMessage(),
]);
}
// We use \Drupal\Core\Render\Markup::create() here as it is safe,
// rather than use t() because all input has been escaped by t().
return Markup::create(implode("\n", $transformed_message_parts));
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$map = $form_state->get(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP) ?? [];
foreach (array_keys($map) as $config_name) {
$config = $this->configFactory()
->getEditable($config_name);
static::copyFormValuesToConfig($config, $form_state, $form);
$config->save();
}
$this->messenger()
->addStatus($this->t('The configuration options have been saved.'));
}
/**
* Copies form values to Config keys.
*
* This should not change existing Config key-value pairs that are not being
* edited by this form.
*
* @param \Drupal\Core\Config\Config $config
* The configuration being edited.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $form
* The form array.
*
* @see \Drupal\Core\Entity\EntityForm::copyFormValuesToEntity()
*/
private static function copyFormValuesToConfig(Config $config, FormStateInterface $form_state, array $form) : void {
$map = $form_state->get(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP);
foreach ($map[$config->getName()] as $array_parents) {
$target = ConfigTarget::fromForm($array_parents, $form);
if ($target->configName !== $config->getName()) {
continue;
}
$value = $form_state->getValue($target->elementParents);
$target->setValue($config, $value, $form_state);
}
}
/**
* Form #after_build callback: Adds message if overrides exist.
*/
public function checkConfigOverrides(array $form, FormStateInterface $form_state) : array {
// Determine which of those editable config keys have overrides.
$override_links = [];
$map = $form_state->get(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP) ?? [];
foreach ($map as $config_name => $config_keys) {
$stored_config = $this->configFactory
->get($config_name);
if (!$stored_config->hasOverrides()) {
// The config has no overrides at all. Can be skipped.
continue;
}
foreach ($config_keys as $key => $array_parents) {
if ($stored_config->hasOverrides($key)) {
$element = NestedArray::getValue($form, $array_parents);
$override_links[] = [
'attributes' => [
'title' => $this->t("'@title' form element", [
'@title' => $element['#title'],
]),
],
'url' => Url::fromUri("internal:#{$element['#id']}"),
'title' => $element['#title'],
];
}
}
}
if (!empty($override_links)) {
$override_output = [
'#theme' => 'links__config_overrides',
'#heading' => [
'text' => $this->t('These values are overridden. Changes on this form will be saved, but overrides will take precedence. See <a href="https://www.drupal.org/docs/drupal-apis/configuration-api/configuration-override-system">configuration overrides documentation</a> for more information.'),
'level' => 'div',
],
'#links' => $override_links,
];
$form['config_override_status_messages'] = [
'message' => [
'#theme' => 'status_messages',
'#message_list' => [
'status' => [
$override_output,
],
],
'#status_headings' => [
'status' => $this->t('Status message'),
],
],
// Ensure that the status message is at the top of the form.
'#weight' => array_reduce(Element::children($form), fn(int $carry, string $key) => min($form[$key]['#weight'] ?? 0, $carry), 0) - 1,
];
}
return $form;
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overriden Title | Overrides |
---|---|---|---|---|---|
ConfigFormBase::buildForm | public | function | Form constructor. | Overrides FormInterface::buildForm | 36 |
ConfigFormBase::checkConfigOverrides | public | function | Form #after_build callback: Adds message if overrides exist. | ||
ConfigFormBase::CONFIG_KEY_TO_FORM_ELEMENT_MAP | protected | constant | The $form_state key which stores a map of config keys to form elements. | ||
ConfigFormBase::copyFormValuesToConfig | private static | function | Copies form values to Config keys. | ||
ConfigFormBase::create | public static | function | Instantiates a new instance of this class. | Overrides FormBase::create | 18 |
ConfigFormBase::doStoreConfigMap | protected | function | Helper method for #after_build callback ::storeConfigKeyToFormElementMap(). | ||
ConfigFormBase::formatMultipleViolationsMessage | protected | function | Formats multiple violation messages associated with a single form element. | 1 | |
ConfigFormBase::loadDefaultValuesFromConfig | public | function | Process callback to recursively load default values from #config_target. | ||
ConfigFormBase::storeConfigKeyToFormElementMap | public | function | #after_build callback which stores a map of element names to config keys. | ||
ConfigFormBase::submitForm | public | function | Form submission handler. | Overrides FormInterface::submitForm | 24 |
ConfigFormBase::typedConfigManager | protected | function | Returns the typed config manager service. | ||
ConfigFormBase::validateForm | public | function | Form validation handler. | Overrides FormBase::validateForm | 13 |
ConfigFormBase::__construct | public | function | Constructs a \Drupal\system\ConfigFormBase object. | 17 | |
ConfigFormBaseTrait::config | protected | function | Retrieves a configuration object. | ||
ConfigFormBaseTrait::getEditableConfigNames | abstract protected | function | Gets the configuration names that will be editable. | 37 | |
DependencySerializationTrait::$_entityStorages | protected | property | |||
DependencySerializationTrait::$_serviceIds | protected | property | |||
DependencySerializationTrait::__sleep | public | function | 1 | ||
DependencySerializationTrait::__wakeup | public | function | 2 | ||
FormBase::$configFactory | protected | property | The config factory. | 2 | |
FormBase::$requestStack | protected | property | The request stack. | 1 | |
FormBase::$routeMatch | protected | property | The route match. | ||
FormBase::configFactory | protected | function | Gets the config factory for this form. | 2 | |
FormBase::container | private | function | Returns the service container. | ||
FormBase::currentUser | protected | function | Gets the current user. | 2 | |
FormBase::getRequest | protected | function | Gets the request object. | ||
FormBase::getRouteMatch | protected | function | Gets the route match. | ||
FormBase::logger | protected | function | Gets the logger for a specific channel. | ||
FormBase::redirect | protected | function | Returns a redirect response object for the specified route. | ||
FormBase::resetConfigFactory | public | function | Resets the configuration factory. | ||
FormBase::setConfigFactory | public | function | Sets the config factory for this form. | ||
FormBase::setRequestStack | public | function | Sets the request stack object to use. | ||
FormInterface::getFormId | public | function | Returns a unique string identifying the form. | 283 | |
LoggerChannelTrait::$loggerFactory | protected | property | The logger channel factory service. | ||
LoggerChannelTrait::getLogger | protected | function | Gets the logger for a specific channel. | ||
LoggerChannelTrait::setLoggerFactory | public | function | Injects the logger channel factory. | ||
MessengerTrait::$messenger | protected | property | The messenger. | 16 | |
MessengerTrait::messenger | public | function | Gets the messenger. | 16 | |
MessengerTrait::setMessenger | public | function | Sets the messenger. | ||
RedirectDestinationTrait::$redirectDestination | protected | property | The redirect destination service. | 2 | |
RedirectDestinationTrait::getDestinationArray | protected | function | Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url. | ||
RedirectDestinationTrait::getRedirectDestination | protected | function | Returns the redirect destination service. | ||
RedirectDestinationTrait::setRedirectDestination | public | function | Sets the redirect destination service. | ||
StringTranslationTrait::$stringTranslation | protected | property | The string translation service. | 3 | |
StringTranslationTrait::formatPlural | protected | function | Formats a string containing a count of items. | ||
StringTranslationTrait::getNumberOfPlurals | protected | function | Returns the number of plurals supported by a given language. | ||
StringTranslationTrait::getStringTranslation | protected | function | Gets the string translation service. | ||
StringTranslationTrait::setStringTranslation | public | function | Sets the string translation service to use. | 2 | |
StringTranslationTrait::t | protected | function | Translates a string to the current language or to a given language. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.