Commit 7af03479 authored by catch's avatar catch
Browse files

Issue #3408120 by alexpott, Wim Leers, lauriii, longwave, tim.plunkett,...

Issue #3408120 by alexpott, Wim Leers, lauriii, longwave, tim.plunkett, effulgentsia: \Drupal\Core\Form\ConfigTarget is not fully serializable

(cherry picked from commit 1630c1a9)
parent af6ac45e
Loading
Loading
Loading
Loading
Loading
+30 −1
Original line number Diff line number Diff line
@@ -139,8 +139,31 @@ public function loadDefaultValuesFromConfig(array $element): array {
   *
   * @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) ?? [];

@@ -149,6 +172,12 @@ public function storeConfigKeyToFormElementMap(array $element, FormStateInterfac
      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`.',
@@ -163,7 +192,7 @@ public function storeConfigKeyToFormElementMap(array $element, FormStateInterfac
      $form_state->set(static::CONFIG_KEY_TO_FORM_ELEMENT_MAP, $map);
    }
    foreach (Element::children($element) as $key) {
      $element[$key] = $this->storeConfigKeyToFormElementMap($element[$key], $form_state);
      $element[$key] = $this->doStoreConfigMap($element[$key], $form_state);
    }
    return $element;
  }
+6 −6
Original line number Diff line number Diff line
@@ -36,20 +36,20 @@ final class ConfigTarget {
  /**
   * Transforms a value loaded from config before it gets displayed by the form.
   *
   * @var \Closure|null
   * @var callable|null
   *
   * @see ::getValue()
   */
  public readonly ?\Closure $fromConfig;
  public readonly mixed $fromConfig;

  /**
   * Transforms a value submitted by the form before it is set in the config.
   *
   * @var \Closure|null
   * @var callable|null
   *
   * @see ::setValue()
   */
  public readonly ?\Closure $toConfig;
  public readonly mixed $toConfig;

  /**
   * Constructs a ConfigTarget object.
@@ -86,8 +86,8 @@ public function __construct(
    ?callable $fromConfig = NULL,
    ?callable $toConfig = NULL,
  ) {
    $this->fromConfig = $fromConfig ? $fromConfig(...) : NULL;
    $this->toConfig = $toConfig ? $toConfig(...) : NULL;
    $this->fromConfig = $fromConfig;
    $this->toConfig = $toConfig;

    if (is_string($propertyPath)) {
      $propertyPath = [$propertyPath];
+6 −0
Original line number Diff line number Diff line
@@ -533,6 +533,8 @@ form_test.nested_config_target:
  path: '/form-test/nested-config-target'
  defaults:
    _form: '\Drupal\form_test\Form\NestedConfigTargetForm'
  options:
    _admin_route: TRUE
  requirements:
    _access: 'TRUE'

@@ -540,6 +542,8 @@ form_test.tree_config_target:
  path: '/form-test/tree-config-target'
  defaults:
    _form: '\Drupal\form_test\Form\TreeConfigTargetForm'
  options:
    _admin_route: TRUE
  requirements:
    _access: 'TRUE'

@@ -547,5 +551,7 @@ form_test.incorrect_config_target:
  path: '/form-test/incorrect-config-target'
  defaults:
    _form: '\Drupal\form_test\Form\IncorrectConfigTargetForm'
  options:
    _admin_route: TRUE
  requirements:
    _access: 'TRUE'
+35 −0
Original line number Diff line number Diff line
@@ -43,7 +43,42 @@ public function buildForm(array $form, FormStateInterface $form_state) {
      '#title' => t('Nemesis'),
      '#config_target' => 'form_test.object:nemesis_vegetable',
    ];

    $form['test1'] = [
      '#type' => 'select',
      '#title' => $this->t('Test 1'),
      '#options' => [
        'option1' => $this->t('Option 1'),
        'option2' => $this->t('Option 2'),
      ],
      '#ajax' => [
        'callback' => '::updateOptions',
        'wrapper' => 'edit-test1-wrapper',
      ],
      '#prefix' => '<div id="edit-test1-wrapper">',
      '#suffix' => '</div>',
    ];
    return parent::buildForm($form, $form_state);
  }

  /**
   * Updates the options of a select list.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The updated form element.
   */
  public function updateOptions(array $form, FormStateInterface $form_state) {
    $form['test1']['#options']['option1'] = $this->t('Option 1!!!');
    $form['test1']['#options'] += [
      'option3' => $this->t('Option 3'),
      'option4' => $this->t('Option 4'),
    ];
    return $form['test1'];
  }

}
+86 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Tests\system\FunctionalJavascript\Form;

use Drupal\FunctionalJavascriptTests\WebDriverTestBase;

/**
 * Tests forms using #config_target and #ajax together.
 *
 * @group Form
 */
class ConfigTargetTest extends WebDriverTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['form_test'];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * Tests #config_target with no callbacks.
   *
   * If a #config_target has no callbacks, the form can be cached.
   */
  public function testTree(): void {
    /** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */
    $key_value_expirable = \Drupal::service('keyvalue.expirable')->get('form');

    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();

    $this->drupalGet('/form-test/tree-config-target');
    $this->assertCount(0, $key_value_expirable->getAll());
    $page->fillField('Nemesis', 'Test');
    $assert_session->pageTextNotContains('Option 3');
    $page->selectFieldOption('test1', 'Option 2');
    $assert_session->waitForText('Option 3');
    $assert_session->pageTextContains('Option 3');
    // The ajax request should result in the form being cached.
    $this->assertCount(1, $key_value_expirable->getAll());

    $page->pressButton('Save configuration');
    $assert_session->pageTextContains('The configuration options have been saved.');
    $assert_session->fieldValueEquals('Nemesis', 'Test');

    // The form cache will be deleted after submission.
    $this->assertCount(0, $key_value_expirable->getAll());
  }

  /**
   * Tests #config_target with callbacks.
   *
   * If a #config_target has closures as callbacks, form cache will be disabled.
   */
  public function testNested(): void {
    /** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */
    $key_value_expirable = \Drupal::service('keyvalue.expirable')->get('form');
    $page = $this->getSession()->getPage();
    $assert_session = $this->assertSession();

    $this->drupalGet('/form-test/nested-config-target');
    $this->assertCount(0, $key_value_expirable->getAll());
    $page->fillField('First choice', 'Apple');
    $page->fillField('Second choice', 'Kiwi');

    $assert_session->pageTextNotContains('Option 3');
    $page->selectFieldOption('test1', 'Option 2');
    $assert_session->waitForText('Option 3');
    $assert_session->pageTextContains('Option 3');
    $this->assertCount(0, $key_value_expirable->getAll());

    $page->pressButton('Save configuration');
    $assert_session->statusMessageContains('The configuration options have been saved.', 'status');

    $assert_session->fieldValueEquals('First choice', 'Apple');
    $assert_session->fieldValueEquals('Second choice', 'Kiwi');
    $this->assertCount(0, $key_value_expirable->getAll());
  }

}
Loading