Introduce a new #config_target Form API property to make it super simple to use validation constraints on simple config forms, and adopt it in several core config forms

Created on 22 August 2023, 10 months ago
Updated 5 November 2023, 8 months ago

Problem/Motivation

Postponed on โœจ Add optional validation constraint support to ConfigFormBase Fixed .

Based on @effulgentsia's proposal at #3364506-79: Add optional validation constraint support to ConfigFormBase โ†’ , which @lauriii, @borisson_ and I preferred to handle in a follow-up because it A) won't work for every config form, B) there's a lot of nitpicking potential ๐Ÿค“

I'd like to propose that instead of that, perhaps it would be better DX for the form to implement just a static property? Like so:

protected const CONFIG_NAME = 'update.settings';

protected static $formElementNamesToConfigKeys = [
  'update_check_disabled' => [self::CONFIG_NAME, 'check.disabled_extensions'],
  'update_check_frequency' => [self::CONFIG_NAME, 'check.interval_days'],
  'update_notify_emails' => [self::CONFIG_NAME, 'notification.emails', ['transform' => MultilineToSequence::class]],
  'update_notification_threshold' => [self::CONFIG_NAME, 'notification.threshold'],
];

Then individual forms wouldn't need to implement mapFormValuesToConfig() or mapConfigPropertyPathToFormElementName() unless they're doing something exotic that can't be expressed declaratively in $formElementNamesToConfigKeys, because ConfigFormBase's implementations could implement what is declared in $formElementNamesToConfigKeys.

In the above example, MultilineToSequence might be a class that's in \Drupal\Core\Form\Confg\ and that implements something like \Drupal\Core\Form\Confg\FormValueToConfigValueInterface containing 3 methods:

  function getConfigValueFromFormValue($form_value)
  function getFormValueFromConfigValue($config_value)
  function getSingleViolationMessage($violation_messages)

As we convert other forms, we'll probably discover additional form value <=> config value transformations that we'll need to create the corresponding classes for that implement this interface.

As a bonus, in the future (not necessarily in this issue), a declarative $formElementNamesToConfigKeys property as opposed to an imperative mapFormValuesToConfig() method would allow us to change:

$form['update_check_frequency'] = [
  ...
  '#default_value' => $config->get('check.interval_days'),

to:

$form['update_check_frequency'] = [
  ...
  '#default_value' => $this->getConfigValueFor('update_check_frequency'),

or even remove that line entirely and have ConfigFormBase automatically populate it :)

Steps to reproduce

N/A

Proposed resolution

(See #13 for where this proposal surfaced from @phenaproxima.)

Since the goal of this issue is to make it very, very easy for simple config forms to adopt validation constraints, we should introduce a new Form API property called #config_target, which allows developers to define the one-to-one mapping of a form element to a config property.

Here's a simple example, from \Drupal\update\UpdateSettingsForm:

    $form['update_check_disabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Check for updates of uninstalled modules and themes'),
      '#config_target' => 'update.settings:check.disabled_extensions',
    ];

This tells ConfigFormBase to load the element's default value from the check.disabled_extensions property of the update.settings config, and to put the element's submitted value there during form validation and submission. If the element had a #default_value key already, then it would "win" instead of #config_target.

If the value needs to be transformed during form build (when the value is being read from config) or submit (when the value is being saved to config), it is possible to specify transformation callbacks as needed. Here's another example from UpdateSettingsForm:

use Drupal\Core\Form\ConfigTarget;

    $form['update_notify_emails'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Email addresses to notify when updates are available'),
      '#rows' => 4,
      '#config_target' => new ConfigTarget(
        'update.settings',
        'notification.emails',
        fromConfig: static::class . '::arrayToMultiLineString',
        toConfig: static::class . '::multiLineStringToArray'
      ),
      '#description' => $this->t('Whenever your site checks for available updates and finds new releases, it can notify a list of users via email. Put each address on a separate line. If blank, no emails will be sent.'),
    ];

  public static function multiLineStringToArray(string $value): array {
    return array_map('trim', explode("\n", trim($value)));
  }

  public static function arrayToMultiLineString(array $value): string {
    return implode("\n", $value);
  }

The callback must be a public static method, or a procedural function. It receives only one argument -- the value to transform.

Since transformation callbacks are a bit more of an advanced case, the syntax for defining them (using the lightweight ConfigTarget class) is a bit more verbose. But that's a deliberate trade-off made in the name of clarity.

From this #config_target property, we can infer (during the form's #process and #after_build stages) which elements are responsible for which config properties, and we can store that mapping and use it for both validation and final submission.

Remaining tasks

None.

User interface changes

None.

API changes

Data model changes

None.

Release notes snippet

TBD

๐Ÿ“Œ Task
Status

Fixed

Version

10.2 โœจ

Component
Configurationย  โ†’

Last updated less than a minute ago

Created by

๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

Live updates comments and jobs are added and updated live.
  • Needs subsystem maintainer review

    It is used to alert the maintainer(s) of a particular core subsystem that an issue significantly impacts their subsystem, and their signoff is needed (see the governance policy draft for more information). Also, if you use this tag, make sure the issue component is set to the correct subsystem. If an issue significantly impacts more than one subsystem, use needs framework manager review instead.

Sign in to follow issues

Comments & Activities

  • Issue created by @Wim Leers
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium borisson_ Mechelen, ๐Ÿ‡ง๐Ÿ‡ช
  • ๐Ÿ‡ซ๐Ÿ‡ฎFinland lauriii Finland
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Added a working PoC implementation to the IS ๐Ÿ˜Š

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • Open on Drupal.org โ†’
    Environment: PHP 8.2 & MySQL 8
    last update 10 months ago
    Not currently mergeable.
  • @wim-leers opened merge request.
  • Assigned to Wim Leers
  • Status changed to Active 10 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • Issue was unassigned.
  • Status changed to Needs review 10 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Did what I said in #6.

    Observations:

    1. we do need transformation methods like @effulgentsia originally proposed, because for example:
      1. in MediaSettingsForm, iframe_domain must either be a valid URI or null, NOT the empty string
      2. in BookSettingsForm, #type => checkboxes is used, and that results in '0' in form values for the unchecked checkboxes โ€ฆ so it MUST be filtered away
    2. In BookSettingsForm::submitForm() there was some weird logic from almost exactly a decade ago ( #1933548: Book allowed_types settings repetitive and in under certain conditions can change unexpectedly โ†’ ) โ€ฆ that I was able to remove by specifying orderby: key instead (see #2361539: Config export key order is not predictable for sequences, add orderby property to config schema โ†’ ).
    3. BookSettingsForm::validateForm() cannot be deleted here yet because it needs that validation logic to be converted to a validation constraint. That'd be too much out of scope here. So I'd say we should either revert BookSettingsForm changes or create a follow-up and point to that.

    Curious what y'all think.

  • last update 10 months ago
    30,136 pass
  • Status changed to Needs work 10 months ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    I like the concept here, but I have some questions about the implementation...

  • last update 9 months ago
    30,164 pass
  • Status changed to Needs review 9 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    I spent most of the day thinking about this issue and, long story short, I think we should change the approach here and go for syntax like this (proposed in another issue, but I'm not sure which):

    $form['security']['iframe_domain'] = [
          '#type' => 'url',
          '#title' => $this->t('iFrame domain'),
          '#size' => 40,
          '#maxlength' => 255,
          '#config_target' => 'media.settings:iframe_domain',
          '#description' => $this->t('Enter a different domain from which to serve oEmbed content, including the <em>http://</em> or <em>https://</em> prefix. This domain needs to point back to this site, or existing oEmbed content may not display correctly, or at all.'),
        ];
    

    (This example comes from MediaSettingsForm.) Let me explain more about how this could work:

    When loading the default value from config: ConfigFormBase::buildForm() could recursively loop through all the form's elements, and load the config value for any element which has a #config_target key, but not a #default_value key. Most (?) config forms call parent::buildForm(), so they would be automatically opted into this behavior.

    When saving the value to config: We could loop through the entire form recursively (starting from $form_state->getCompleteForm()), and for any element which defines a #config_target key, we could use the element's #parents property to get the submitted value from $form_state->getValue(). Then we could set that value in the specified config object and property path.

    But what if we need to transform the value before saving? Well, #config_target would make an allowance for that. It could either be a string, if there is no transformation to be done:

    '#config_target' => 'media.settings:iframe_domain'

    Or, if there is a transformation needed, it could be an array whose second element is the name of a string callable which will transform it:

    '#config_target' => ['media.settings:iframe_domain', '::nonEmptyStringOrNull']

    It could be a fully-qualified static callable as a string:

    '#config_target' => ['media.settings:iframe_domain', HelperClass::class . '::transformationMethod']

    If it was not fully qualified, as in the first example, we would expect it to be the name of a trusted callable. That would be enforced by DoTrustedCallbackTrait.

    I think that this approach, although old-school, will have these benefits:

    • The goal of this issue is to make it super-duper drop-dead easy to adopt config validation. To that end, I think we want to minimize the effort it takes contrib to adopt it. Core will adopt what it needs to, but contrib is the challenge here. Adding a new FAPI property, even though it's kind of old-school, is a very familiar pattern for contrib and custom module/theme developers. The value of the property is relatively simple (at least if there's no transformation needed). There's no need for an arcane syntax in a static array. So, to me, this approach greases the slide to contrib adoption, and that's the main reason to do it.
    • But if you're not convinced, there's another reason: this means we can easily support hook_form_alter (and all of its variations). Yes, some modules want to alter other modules' config forms to add additional properties -- Automatic Updates is an example of this. It's a common pattern. Using a static array of the form class is un-alterable, and would leave all modules which need to alter another module's config forms out in the cold.
    • Since the priority is to get people to use config validation, I think that adopting the more old-school FAPI property with relative "ease of understanding" and broad compatibility is an appropriate trade-off. When everybody's using config validation for their config forms, we can talk about deprecating the new FAPI property and moving to something more modern.
  • Assigned to phenaproxima
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    The value of the property is relatively simple (at least if there's no transformation needed).

    This is a fairly big assumption. Transformations will be necessary in many cases. So it will be more complex than presented here in many cases.

    It's a common pattern.

    It's not common for simple config forms though. Even more so because the only reasons for contrib modules to alter a simple config form are to:

    1. inject a form to add third party settings to the edited simple config
    2. inject a form whose values are saved elsewhere (in which case itโ€™s completely irrelevant anyway)
    3. do cosmetic changes

    But you're right it MUST be supported! ๐Ÿ‘

    Automatic Updates is an example of this.

    Except that it's not a good example ๐Ÿ˜…, because it applies the second pattern I mentioned above: it alters the form provided by UpdateSettingsForm for update.settings, but stores its values in automatic_updates.settings! So โ€ฆ config validation is irrelevant in that case anyway, since \Drupal\update\UpdateSettingsForm::getEditableConfigNames() says:

      protected function getEditableConfigNames() {
        return ['update.settings'];
      }
    

    (The only way it could add config validation is to subclass UpdateSettingsForm, add automatic_updates.settings to that and override the update.settings route definition. Which is fine โ€ฆ except that it then prevents other modules from doing the same! Horizontal extensibility bites once again!)

    Based on a private Slack between you, @effulgentsia and I, AFAICT @effulgentsia is on board with this change in direction. The primary reason being: if it's a static array, contrib form alters become impossible to support.

    Tagging for that.

    Should we make this a non-blocker and do ๐Ÿ“Œ [PP-1] Update all remaining ConfigFormBase subclasses in Drupal core to use config validation Active first?

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • last update 9 months ago
    30,168 pass
  • @phenaproxima opened merge request.
  • last update 9 months ago
    30,168 pass
  • last update 9 months ago
    Custom Commands Failed
  • last update 9 months ago
    30,161 pass, 3 fail
  • last update 9 months ago
    30,163 pass, 2 fail
  • last update 9 months ago
    30,168 pass
  • Status changed to Needs work 9 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • last update 9 months ago
    30,363 pass
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • last update 9 months ago
    30,363 pass
  • last update 9 months ago
    30,363 pass
  • last update 9 months ago
    30,363 pass
  • last update 9 months ago
    30,363 pass
  • Assigned to Wim Leers
  • Status changed to Needs review 9 months ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • last update 9 months ago
    30,351 pass, 4 fail
  • last update 9 months ago
    30,363 pass
  • last update 9 months ago
    30,363 pass
  • last update 9 months ago
    30,363 pass
  • Status changed to Needs work 9 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium borisson_ Mechelen, ๐Ÿ‡ง๐Ÿ‡ช

    Looking at the implementations in the last few commits, this seems like such a simple DX, works very well.
    I had one remark on the pull request, but it might not be something we often do.

    I don't see the link to the comment, so https://git.drupalcode.org/issue/drupal-3382510/-/commit/8e4abd89e594877...

    Other than that small remark, I think this is good to go, waiting for rtbc on an answer on that comment. I'm happy if we feel like that goes too far.

  • Assigned to phenaproxima
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    I have a bit more than one remark ๐Ÿ˜…

    But: excellent work here, @phenaproxima! ๐Ÿ‘

    1. The issue summary does not say that this is removing mapConfigKeyToFormElementName(), which โœจ Add optional validation constraint support to ConfigFormBase Fixed added during the 10.2.x cycle (and which we're still in). So it's fine to remove, but we need to articulate why. We arrived at that solution after long and careful debate.
    2. The changes in core/modules/update/src/UpdateSettingsForm.php make this really convincing. FAR less code! And yay for the test coverage that was added in the previous issue, that means I am immediately confident this works correctly for sequences too ๐Ÿฅณ
    3. I really like how this gives you one more reason to update your config forms, even if you do not yet have validation constraints: your MR shows that we'll be able to remove most ::submitForm() overrides! ๐Ÿคฉ
    4. But โ€ฆ other concerns I raised at #2408549-178: There is no indication on configuration forms if there are overridden values โ†’ still stand:

      • โ†’ we could ensure this by validating that each #config_target value points to a config name that actually exists and is one of the getEditableConfigNames() and its property path actually exists in the schema
      • Related: it now is suspicious when a ::submitForm() override exists. Most of the time you should not need that. As is the presence of #default_value.

      • โ†’ this is now no longer true. But ideally we'd be able to adopt the same pattern for config entity forms, although then it wouldn't be something like '#config_target' => 'node.settings:key', but something like '#config_target' => 'node.type.*:key'. Could we do a brief PoC to verify that this would be possible? ๐Ÿ™
  • This issue AFAICT introduces a regression/BC break: see my comment on NegotiationSessionForm.php
  • This makes deprecation of the form-based validation (see ๐Ÿ“Œ [PP-2] Follow-up for #3364506: add deprecation once all simple config forms in core implement Postponed ) still possible to detect: the complete absence of #config_target is a certain indicator.
  • last update 9 months ago
    Custom Commands Failed
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Updated the issue summary to explain the rationale for removing copyFormValuesToConfig() from the API.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    ideally we'd be able to adopt the same pattern for config entity forms

    I could be wrong, but my knee-jerk reaction is that this will not fly.

    Config entity forms are backed by config entities, which are fundamentally different beasts than simple config. Simple config is a fairly dumb key-value map, but config entities can have all sorts of specialized logic in and around them, which isn't the case with simple config. We can't just dump a form element's value into a config entity's key, optionally through some transformation callback; we'd need to map those elements' values to methods of config entities -- and that would mean somehow encoding the proper invocations for those methods.

    Sure, we could use ConfigEntityInterface::set() and ::get()...but that can bypass all kinds of very important logic that may need to run for the entity to be coherent/valid. Doing it that way is a recipe for disaster (and frankly it would probably fail many tests right off the bat).

  • last update 9 months ago
    30,392 pass
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    I updated the change record to explain how to use this.

  • Assigned to Wim Leers
  • Status changed to Needs review 9 months ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    ๐Ÿ“

  • Assigned to phenaproxima
  • Status changed to Needs work 9 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    I think this is 99% done. The one thing that is "iffy" to use @phenaproxima's words (the bidirectional transformation methods) actually suggests we should keep copyFormValuesToConfig() available as an escape hatch for more complex use cases.

    Note: once ๐Ÿ“Œ Configuration schema & required values: add test coverage for `nullable: true` validation support Fixed lands, we could not only remove #default_value, but also #required ๐Ÿ˜„

  • last update 8 months ago
    30,397 pass
  • Assigned to Wim Leers
  • Status changed to Needs review 8 months ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    I think โ€ฆ that perhaps this is fine. It helps keeps things simple for the 99% case, and for the remaining 1%, you'd just have to implement copyFormValuesToConfig() instead of using #config_target. WDYT about that?

    That'd be good to incorporate in the change record.

    Makes sense to me. Tagging this issue for post-commit change record updates (since there are already change record updates needed, and I can't go edit the revision that has the required changes).

    Committers, please note that this should be set to "needs work" post-commit, so that I can make the change record explain when one should override copyFormValuesToConfig().

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • Issue was unassigned.
  • Status changed to RTBC 8 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Per @lauriii.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom longwave UK

    I am not convinced that the transformer function should be bidirectional; per single responsibility principle, a function should generally only have one purpose - here the transformer has two depending on the type of the argument. Because form API is not currently strictly typed (numeric inputs arrive as strings, for example) then I think it might end up being tricky detecting the direction of transformation - especially if the config value is also a string. If we don't want to declare two functions should each transformer be a class that implements an interface with two methods?

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    What if we added a new param to the transform function -- bool $is_saving? That way the function would have more to go on than just "how does the argument look".

    I agree that it sort of flies in the face of the single responsibility principle, but IMHO that's a sacrifice worth making in the name of keeping the DX as simple as possible.

  • ๐Ÿ‡ท๐Ÿ‡ดRomania claudiu.cristea Arad ๐Ÿ‡ท๐Ÿ‡ด

    Maybe we can replace that array/string with a very light class:

    class ConfigTarget {
      public function __construct(
        public readonly string $configTarget,
        public readonly array|string $transformOnLoadOrSave,
        public readonly array|string|null $transformOnSave = NULL,
      ) {
        // @todo:: prepare callbacks to do a proper check.
        \assert(is_callable($transformOnLoadOrSave));
        \assert(transformOnSave === NULL || is_callable($transformOnLoadOrSave));
      }
    }
    

    If $transformOnSave is missing, $transformOnLoadOrSave is bidirectional.

    Then we have:

    <?php
    $form['foo'] = [
    ...
    '#config_target' => new ConfigTarget(
    'media.settings:iframe_domain',
    '::nullIfEmptyString',
    [SomeOtherClass::class, 'transformOnSave'],
    ),
    ...
    ];

  • last update 8 months ago
    Custom Commands Failed
  • last update 8 months ago
    30,412 pass
  • Status changed to Needs review 8 months ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Now that I made the separation between save and load callbacks explicit, this needs review again.

  • Status changed to RTBC 8 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium borisson_ Mechelen, ๐Ÿ‡ง๐Ÿ‡ช

    I agree that there are usecases where the saving and loading are different, and it is a pattern that we also use in different places.
    The argument that this is now a single responsibility instead of doing both ways is a good argument as well.

    I looked at the changes made in the last commit and they make sense as well, discussed this with @lauriii at drupalcon.

    Does this still need subsystem maintainer review?

  • Status changed to Needs work 8 months ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom longwave UK

    Added some comments.

  • 21:09
    4:01
    Running
  • 57:20
    18:25
    Running
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ

    Discussed with @Wim Leers and @longwave. The callbacks - save_callback and load_callback are tricky to name. They are not loading or saving anything. As the new document says they are for transforming the value between the config and the form system.

    We discussed several ideas...

    1. Add a new ConfigTransformer class & interface that has toForm() and toConfig() as static methods.

    So the code to use this becomes

          '#config_target' => [
            'target' => 'book.settings:allowed_types',
            'transformer' => '\Drupal\Namespace\ConfigTransformer',
          ],
    

    The advantage of this is that the transformers are reusable and well documented. The disadvantage of this is that the transforms are moving away from the config form class and we'll have more and more classes.

    2. Add a new ConfigTarget value object

    The code to use this becomes

          '#config_target' => new ConfigTarget(
            'update.settings:notification.emails',
            '::loadEmailsFromConfig',
            '::storeEmailsInConfig',
          ),

    The advantage of this is that we're not building on the massive ArrayPI of forms so it is easy to discovery what is possible and documenting what the callbacks do is much simpler. Ie. the class would have a method like ConfigTarget::getFormToConfigTransform() which in the above instance would return '::storeEmailsInConfig'

    3. Should these transforms happen using the Form API?

    The form API already has #value_callback that transforms input to an expected format. That's what we're doing here. However there's no API for manipulating #default_value so we'd need to add that. Also using #value_callback would mean that if we use a Checkboxes element we'd become responsible for calling \Drupal\Core\Render\Element\Checkboxes::valueCallback() which does not feel right. So #value_callback is happening at the incorrect level.

    4. Come up with better names for save_callback and load_callback

    Maybe something like config_to_form_transform() and form_to_config_transform()

    While writing this comment I realised that option 3 is untenable and thought that there is an option 4 so I'll add that. This option was not discussed with the others.

    I think I prefer option 1 or 2 but could live with 4 is other agree.

  • ๐Ÿ‡ท๐Ÿ‡ดRomania claudiu.cristea Arad ๐Ÿ‡ท๐Ÿ‡ด

    I'm for 4., which is more or less what I've proposed in #36, because:

    • Not a blind array that we should know how the build the sequence
    • It's typed
    • It's documented
    • Just a tiny class
    • Easier in IDE
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Here's another idea: what if we introduce two new protected functions to ConfigFormBase (I'm not married to these specific names):

    protected function configValueToFormValue(string $config_name, string $property_path, mixed $value): mixed {
      // Default implementation -- no transformation.
      return $value;
    }
    
    protected function formValueToConfigValue(string $config_name, string $property_path, mixed $value): mixed {
      // Default implementation -- no transformation.
      return $value;
    }
    

    If you need to do transformations, you can override one or both of these methods. This way, all the complexity of #config_target disappears: it's no longer an ArrayPI; you don't need to write a bidirectional transformation function; and it's very clear how to do a transformation in either direction.

    Since transformations are a relatively "advanced" use case, I think we can rely on developers to grok this.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom longwave UK

    Transformer functions are hopefully the exception rather than the rule and for good DX given the rest of form API lives in a single class we want to try and keep the transformers in the same class as well. Can we use (or perhaps abuse) PHP attributes so transformer methods can live in the same class?

    public function buildForm(array $form, FormStateInterface $form_state) {
      $form['update_notify_emails'] = [
        '#type' => 'textarea',
        '#title' => $this->t('Email addresses to notify when updates are available'),
        '#rows' => 4,
        '#config_target' => 'update.settings:notification.emails',
      ];
    }
    
    #[FromConfig('update.settings:notification.emails')]
    public static function fromNotificationEmails(array $value): string {
      return implode("\n", $value);
    }
    
    #[ToConfig('update.settings:notification.emails')]
    public static function toNotificationEmails(string $value): array {
      return array_map('trim', explode("\n", trim($value)));
    }
    

    Multiple config keys can share the same transformer functions if they need to. If there is no method tagged with the attribute no transformation is done.

    Not sure how to scale this when we want to share transformers between forms though.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Ohhh! I think I like that? The major downside to that (besides new syntax nobody is familiar with) is I think that it becomes a very abstract, relatively disjointed DX, where you have to remember the various moving parts. I do think we could add logic to check that if a "from" exists, the "to" must also exist, and vice versa? throw new \LogicException() if either is missing? ๐Ÿค”

    Not sure how to scale this when we want to share transformers between forms though.

    In cases like these, reuse is IMHO not very important. Just copy/paste these snippets? Otherwise all of these need to become APIs too. This approach would mean it's not actually a public API, but โ€ฆ if the need arises, we could have a CommonConfigFormTransformers utility class that one could reuse?

  • ๐Ÿ‡ท๐Ÿ‡ดRomania claudiu.cristea Arad ๐Ÿ‡ท๐Ÿ‡ด

    ...we want to try and keep the transformers in the same class as well

    I don't think so. For example strtolower() is also a transformer. Why creating a method just to do that? Then [\Drupal\Component\Utility\Bytes::class, 'toNumber'] is also a transformer. Could we use it directly but still allow ::transform() (which is in the class)?

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    I do think we could add logic to check that if a "from" exists, the "to" must also exist, and vice versa?

    ๐Ÿ‘Ž I don't recommend this. Of the cases where transformations have to be done in this MR, only one of them needs both to- and from- transformations. The rest of them only need to be changed when the value is being saved.

    What if we allowed the attributes to reference other callables (like strtolower()), if they needed to?

    So for example, if you do this, you are saying that stringToArray() is the transformation callback for update.settings:notification.emails:

    #[ToConfig('update.settings', 'notification.emails')]
    public function stringToArray($value) {...}
    

    But you could also do this, on buildForm():

    #[ToConfig('system.site', 'name', callback: 'strtolower')]
    public function buildForm($form, $form_state) {...}
    

    In this case, you're saying "I want to use strtolower() as the transformer for the system.site:name config property." This would be a very advanced use case but it would address what @claudiu.cristea points out in #45.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ

    I'm not that keen on #46 and the attribute stuff because I think that have attributes + #config_target in form array feels odd and hard to get used to.

    I think #42 might be okay especially if we encourage the use of match. But I think I've come up with a reason why attributes and #42 won't work in the long run. Form alters. How is dblog_form_system_logging_settings_alter() or something similar able to use these. Therefore I think we need this information in the form array.

    @longwave and I discussed whether or not we could use schema for this. This is kind of how config translation works - see config_translation_config_schema_info_alter(). This method adds a new form_element_class to config schema. The problem putting the transformer information into schema is that this will tie schema and fomr stuff together. So I don't think this is correct.

    I think of all the options is something like #36 (sorry @claudiu.cristea for not crediting you with the idea in #40 is the best. It can be used by form alters, it can use both functions on the form and functions anywhere else and the very light class can clearly document with the transformers do.

  • 46:25
    59:37
    Running
  • Status changed to Needs review 8 months ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Okay, I took a shot at implementing this with a ConfigTarget value object.

    To keep things frictionless for simpler cases, I made it so that you can still have #config_target be a string, and it will automatically be converted to a ConfigTarget object for you. These objects can be serialized, so they can be stored directly in the config-key-to-form-element map without trouble.

    For advanced cases (that is, if you need a transformation in either or both directions), I think we can expect people to call ConfigTarget::create().

  • last update 8 months ago
    30,381 pass, 23 fail
  • 32:02
    3:29
    Running
  • last update 8 months ago
    Custom Commands Failed
  • last update 8 months ago
    Custom Commands Failed
  • last update 8 months ago
    30,418 pass, 1 fail
  • last update 8 months ago
    30,418 pass, 1 fail
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    That is looking very encouraging! Thanks for the fast turnaround time ๐Ÿ™

    Curious what @longwave & @alexpott think ๐Ÿ˜Š

  • last update 8 months ago
    30,418 pass, 1 fail
  • last update 8 months ago
    30,418 pass, 1 fail
  • 32:46
    4:50
    Running
  • last update 8 months ago
    Custom Commands Failed
  • last update 8 months ago
    30,421 pass, 1 fail
  • last update 8 months ago
    Custom Commands Failed
  • 50:44
    47:16
    Running
  • Assigned to phenaproxima
  • Status changed to Needs work 8 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    I did a very thorough review, and could barely find anything to complain about.

    This looks great. It's _almost_ a net-zero diff ๐Ÿ˜ฎ

    A few more nits fixed, and I'll confidently RTBC ๐Ÿ˜Š

  • last update 8 months ago
    Custom Commands Failed
  • last update 8 months ago
    Custom Commands Failed
  • Assigned to Wim Leers
  • Status changed to Needs review 8 months ago
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Back to you for final review!

  • last update 8 months ago
    30,436 pass
  • Assigned to phenaproxima
  • Status changed to RTBC 8 months ago
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Zero remarks left.

    Issue summary clarified (and slightly updated, it was slightly wrong/stale).

    @phenaproxima: you previously created a change record revision โ†’ with the then-up-to-date #config_target stuff, can you update it? ๐Ÿ™

  • Assigned to Wim Leers
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Change record updated!

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • Issue was unassigned.
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Crediting myself for writing the current approach, Wim for reviewing it, and @claudiu.cristea for suggesting the approach we ultimately used.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts

    Oh, and @longwave for review.

  • last update 8 months ago
    30,439 pass
  • last update 8 months ago
    30,439 pass
  • last update 8 months ago
    30,457 pass, 1 fail
  • last update 8 months ago
    30,466 pass
  • last update 8 months ago
    30,485 pass
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States phenaproxima Massachusetts
  • ๐Ÿ‡จ๐Ÿ‡ญSwitzerland bircher ๐Ÿ‡จ๐Ÿ‡ฟ

    re my comment on the MR: simpler is good so that was not meant to hold up anything.

    I much prefer this over the unshipped API so +1 for RTBC.
    The issue summary update looks good too now. Maybe the CR could mention the more complex example with the class and not just the string.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ

    Creditting @bircher as they were involved in the conversations at Drupalcon that led to the current API.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ

    Spent lots of time discussing this at Drupalcon. I think the new API is the easiest way forward for now. I do wonder if something else will come up as we go forward and I have ideas for two follow-ups but I think doing them will only change internal implementation details so I'm not worried about that.

    The two ideas are:

    1. Do not require the config in #config_target to be listed in getEditableConfigNames() - this would make doing form alters like dblog_form_system_logging_settings_alter() really easy as you would not need dblog_logging_settings_submit()
    2. Allow all callables in ConfigTarget - instead of saving ConfigTarget objects to the map we could save the location in the form array instead so we can get the config target from the form array

    Committed e1c95b2 and pushed to 11.x. Thanks!

    I'm going to ask the RMs if we can backport this to 10.2

  • Status changed to Downport 8 months ago
    • alexpott โ†’ committed e1c95b2f on 11.x
      Issue #3382510 by phenaproxima, Wim Leers, alexpott, longwave, claudiu....
    • alexpott โ†’ committed 77945942 on 10.2.x
      Issue #3382510 by phenaproxima, Wim Leers, alexpott, longwave, claudiu....
  • Status changed to Fixed 8 months ago
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ

    Discussed with @catch and we agreed to backport this.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Yay! ๐Ÿฅณ

    RE: Follow-ups

    1. I contemplated this too. But I think it'd be valuable to keep the extra guardrails around for the 99% use case where a ConfigFormBase subclass is not being altered. And I think I see a way how: i) use ::getFormId() to determine if there any form alters for this form, ii) if no form alters exist: inform the developer that this config target does not make sense, iii) if they do exist: allow arbitrary targets

      Note: this MR did not introduce any validation of the #config_target property! It only is not going to validate or save config targets outside of :: getEditableConfigNames ()

      (I think we discussed verifying the #config_target makes sense, but it was not considered required, because Form API in general has very few guardrails. )

    2. I like this!

    Unblocked ๐Ÿš€

  • ๐Ÿ‡ญ๐Ÿ‡บHungary Gรกbor Hojtsy Hungary

    I think this should be part of a config validation related release highlight segment :) Feel free to identify more bits to include in that.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ

    I've created ๐Ÿ“Œ Do not require the config in #config_target to be listed in getEditableConfigNames() Active - I disagree with

    extra guardrails around for the 99% use case

    - I'm not sure why they need to exist and what they are for. Let's debate on the issue.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    This introduced a regression: ๐Ÿ› Follow-up for #3382510: FormStateInterface::setErrorByName() needs not #name but a variation Closed: duplicate . ๐Ÿ›

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium Wim Leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    One more thing: this massively improved DX, at one important cost: it hardcoded the assumption that UI/form elements map 1:1 to config property paths (and hence violations for property paths).

    This makes better UIs (that present things in the user's mental model) impossible in some cases: ๐Ÿ“Œ ConfigFormBase + validation constraints: support composite form elements Needs review .

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom alexpott ๐Ÿ‡ช๐Ÿ‡บ๐ŸŒ

    Opened ๐Ÿ“Œ Allow all callables in ConfigTarget Fixed to address #62.2

  • Automatically closed - issue fixed for 2 weeks with no activity.

  • Production build 0.69.0 2024