From 97e6267a624283a6cc420dd987cf3e13a4089bfa Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 6 Nov 2023 12:30:50 +0000
Subject: [PATCH] Issue #3398974 by alexpott, Wim Leers, borisson_: Follow-up
 for #3382510: FormStateInterface::setErrorByName() needs not #name but a
 variation

---
 core/lib/Drupal/Core/Form/ConfigFormBase.php  | 16 +++++-
 core/lib/Drupal/Core/Form/ConfigTarget.php    | 12 -----
 .../config/schema/form_test.schema.yml        |  6 +++
 .../modules/form_test/form_test.routing.yml   | 14 ++++++
 .../src/Form/IncorrectConfigTargetForm.php    | 36 +++++++++++++
 .../src/Form/TreeConfigTargetForm.php         | 49 ++++++++++++++++++
 .../src/Functional/Form/ConfigTargetTest.php  | 50 +++++++++++++++++++
 7 files changed, 169 insertions(+), 14 deletions(-)
 create mode 100644 core/modules/system/tests/modules/form_test/src/Form/IncorrectConfigTargetForm.php
 create mode 100644 core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php
 create mode 100644 core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php

diff --git a/core/lib/Drupal/Core/Form/ConfigFormBase.php b/core/lib/Drupal/Core/Form/ConfigFormBase.php
index b9af2323fc6f..7a200baf86dc 100644
--- a/core/lib/Drupal/Core/Form/ConfigFormBase.php
+++ b/core/lib/Drupal/Core/Form/ConfigFormBase.php
@@ -137,7 +137,6 @@ public function storeConfigKeyToFormElementMap(array $element, FormStateInterfac
       if (is_string($target)) {
         $target = ConfigTarget::fromString($target);
       }
-      $target->elementName = $element['#name'];
       $target->elementParents = $element['#parents'];
       $map[$target->configName . ':' . $target->propertyPath] = $target;
       $form_state->set(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP, $map);
@@ -192,7 +191,20 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
           // will not have the sequence index in it.
           $property_path = rtrim($property_path, '0123456789.');
         }
-        $form_element_name = $map["$config_name:$property_path"]->elementName;
+
+        if ($property_path === '') {
+          // There is a map to a non-existing config key. Try to work backwards.
+          $property_path = $violation->getParameters()['@key'] ?? '';
+        }
+
+        if (isset($map["$config_name:$property_path"])) {
+          $form_element_name = implode('][', $map["$config_name:$property_path"]->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;
       }
 
diff --git a/core/lib/Drupal/Core/Form/ConfigTarget.php b/core/lib/Drupal/Core/Form/ConfigTarget.php
index 45eee5799dfa..ddf7bc500071 100644
--- a/core/lib/Drupal/Core/Form/ConfigTarget.php
+++ b/core/lib/Drupal/Core/Form/ConfigTarget.php
@@ -9,18 +9,6 @@
  */
 final class ConfigTarget {
 
-  /**
-   * The name of the form element which maps to this config property.
-   *
-   * @var string
-   *
-   * @see \Drupal\Core\Form\ConfigFormBase::storeConfigKeyToFormElementMap()
-   *
-   * @internal
-   *   This property is for internal use only.
-   */
-  public string $elementName;
-
   /**
    * The parents of the form element which maps to this config property.
    *
diff --git a/core/modules/system/tests/modules/form_test/config/schema/form_test.schema.yml b/core/modules/system/tests/modules/form_test/config/schema/form_test.schema.yml
index 9af17cdd860c..dccdc5bc9363 100644
--- a/core/modules/system/tests/modules/form_test/config/schema/form_test.schema.yml
+++ b/core/modules/system/tests/modules/form_test/config/schema/form_test.schema.yml
@@ -5,3 +5,9 @@ form_test.object:
     bananas:
       type: string
       label: 'Bananas'
+    favorite_vegetable:
+      type: required_label
+      label: 'Favorite vegetable'
+    nemesis_vegetable:
+      type: required_label
+      label: 'Nemesis vegetable'
diff --git a/core/modules/system/tests/modules/form_test/form_test.routing.yml b/core/modules/system/tests/modules/form_test/form_test.routing.yml
index 9c971a727e1d..fd4db9da386f 100644
--- a/core/modules/system/tests/modules/form_test/form_test.routing.yml
+++ b/core/modules/system/tests/modules/form_test/form_test.routing.yml
@@ -528,3 +528,17 @@ form_test.javascript_states_form:
     _form: '\Drupal\form_test\Form\JavascriptStatesForm'
   requirements:
     _access: 'TRUE'
+
+form_test.tree_config_target:
+  path: '/form-test/tree-config-target'
+  defaults:
+    _form: '\Drupal\form_test\Form\TreeConfigTargetForm'
+  requirements:
+    _access: 'TRUE'
+
+form_test.incorrect_config_target:
+  path: '/form-test/incorrect-config-target'
+  defaults:
+    _form: '\Drupal\form_test\Form\IncorrectConfigTargetForm'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/form_test/src/Form/IncorrectConfigTargetForm.php b/core/modules/system/tests/modules/form_test/src/Form/IncorrectConfigTargetForm.php
new file mode 100644
index 000000000000..5296edb9cb88
--- /dev/null
+++ b/core/modules/system/tests/modules/form_test/src/Form/IncorrectConfigTargetForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\form_test\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+class IncorrectConfigTargetForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['form_test.object'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'form_test_incorrect_config_target_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['missing_key'] = [
+      '#type' => 'textfield',
+      '#title' => t('Missing key'),
+      '#config_target' => 'form_test.object:does_not_exist',
+    ];
+    return parent::buildForm($form, $form_state);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php b/core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php
new file mode 100644
index 000000000000..502fbd8f7130
--- /dev/null
+++ b/core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\form_test\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+class TreeConfigTargetForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['form_test.object'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'form_test_tree_config_target_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['vegetables'] = [
+      '#type' => 'details',
+      '#tree' => TRUE,
+      '#input' => TRUE,
+      '#title' => t('Vegetable preferences'),
+    ];
+    $form['vegetables']['favorite'] = [
+      '#type' => 'textfield',
+      '#title' => t('Favorite'),
+      '#default_value' => 'Potato',
+      '#config_target' => 'form_test.object:favorite_vegetable',
+    ];
+    $form['vegetables']['nemesis'] = [
+      '#type' => 'textfield',
+      '#title' => t('Nemesis'),
+      '#default_value' => 'Broccoli',
+      '#config_target' => 'form_test.object:nemesis_vegetable',
+    ];
+    return parent::buildForm($form, $form_state);
+  }
+
+}
diff --git a/core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php b/core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php
new file mode 100644
index 000000000000..5ab7a735956f
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Form;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests forms using #config_target.
+ *
+ * @group Form
+ */
+class ConfigTargetTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['form_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Tests #config_target where #tree is set to TRUE.
+   */
+  public function testTree(): void {
+    $this->drupalGet('/form-test/tree-config-target');
+    $page = $this->getSession()->getPage();
+    $page->fillField('Favorite', '');
+    $page->pressButton('Save configuration');
+    $assert_session = $this->assertSession();
+    $assert_session->statusMessageContains('This value should not be blank.', 'error');
+    $assert_session->elementAttributeExists('named', ['field', 'Favorite'], 'aria-invalid');
+    $assert_session->elementAttributeNotExists('named', ['field', 'Nemesis'], 'aria-invalid');
+  }
+
+  /**
+   * Tests #config_target with an incorrect key.
+   */
+  public function testIncorrectKey(): void {
+    $this->drupalGet('/form-test/incorrect-config-target');
+    $page = $this->getSession()->getPage();
+    $page->pressButton('Save configuration');
+    $assert_session = $this->assertSession();
+    $assert_session->statusMessageContains('\'does_not_exist\' is not a supported key.', 'error');
+    $assert_session->elementAttributeExists('named', ['field', 'Missing key'], 'aria-invalid');
+  }
+
+}
-- 
GitLab