Problem/Motivation

Postponed on #3364506: Add optional validation constraint support to ConfigFormBase.

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

Issue fork drupal-3382510

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Wim Leers created an issue. See original summary.

Wim Leers credited lauriii.

wim leers’s picture

Issue summary: View changes

Added a working PoC implementation to the IS 😊

wim leers’s picture

Title: [PP-1] OneToOneConfigFormTrait: make it super simple to use validation constraints on simple config forms » [PP-1] OneToOneConfigFormTrait: make it super simple to use validation constraints on simple config forms, adopt in Media(Library)SettingsForm + BookSettingsForm
Issue tags: +Needs issue summary update
wim leers’s picture

wim leers’s picture

Title: [PP-1] OneToOneConfigFormTrait: make it super simple to use validation constraints on simple config forms, adopt in Media(Library)SettingsForm + BookSettingsForm » OneToOneConfigFormTrait: make it super simple to use validation constraints on simple config forms, adopt in Media(Library)SettingsForm + BookSettingsForm
Assigned: Unassigned » wim leers
Status: Postponed » Active
wim leers’s picture

Assigned: wim leers » Unassigned
Issue summary: View changes
Status: Active » Needs review
Issue tags: -Needs issue summary update

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.

phenaproxima’s picture

Status: Needs review » Needs work

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

wim leers’s picture

Status: Needs work » Needs review
phenaproxima’s picture

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 Ye Olden Way, 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 older-style 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. Introducing something unfamiliar will be counterproductive.
wim leers’s picture

Assigned: Unassigned » phenaproxima
Issue tags: +Needs title update, +Needs issue summary update

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 Needs title update, Needs issue summary update for that.

Should we make this a non-blocker and do #3384790: Update all remaining ConfigFormBase subclasses in Drupal core to use #config_target first?

phenaproxima’s picture

Title: OneToOneConfigFormTrait: make it super simple to use validation constraints on simple config forms, adopt in Media(Library)SettingsForm + BookSettingsForm » Introduce a new #config_target Form API property to make it super simple to use validation constraints on simple config forms, adopt in Media(Library)SettingsForm + BookSettingsForm
Issue tags: -Needs title update

wim leers’s picture

Status: Needs review » Needs work
phenaproxima’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update
phenaproxima’s picture

Assigned: phenaproxima » wim leers
Status: Needs work » Needs review
borisson_’s picture

Status: Needs review » Needs work

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.

wim leers’s picture

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 #3364506: Add optional validation constraint support to ConfigFormBase 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! 🤩 (That also means that my statement There's no incentive for that to happen? 😇 at #2408549-178 is no longer true!)
  4. But … other concerns I raised at #2408549-178: Display status message on configuration forms when there are overridden values still stand:
    • how we will ensure that every single key-value pair in config will get a correct annotation in the form definition?
      → 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.
    • Finally, the pattern that #3364506 issue uses is architecturally similar to:
      → 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? 🙏
  5. This issue AFAICT introduces a regression/BC break: see my comment on NegotiationSessionForm.php
  6. This makes deprecation of the form-based validation (see #3384782: [PP-1] Follow-up for #3364506: add deprecation once all simple config forms in core implement) still possible to detect: the complete absence of #config_target is a certain indicator.
phenaproxima’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

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

phenaproxima’s picture

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

Entity forms already kinda do this mapping in a half-assed way; see \Drupal\Core\Entity\EntityForm::copyFormValuesToEntity().

So we could probably just rely on that method (even though it is rife with problems) and, in validateForm(), validate the config entity as typed data, mapping any violations back to the elements which correspond to the invalid property paths.

phenaproxima’s picture

Title: Introduce a new #config_target Form API property to make it super simple to use validation constraints on simple config forms, adopt in Media(Library)SettingsForm + BookSettingsForm » 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
Issue summary: View changes
phenaproxima’s picture

I updated the change record to explain how to use this (as a new revision of the existing CR): https://www.drupal.org/node/3373502/revisions/13264455/view

phenaproxima’s picture

Assigned: phenaproxima » wim leers
Status: Needs work » Needs review

🏓

wim leers’s picture

Assigned: wim leers » phenaproxima
Status: Needs review » Needs work

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 #3364109: Configuration schema & required values: add test coverage for `nullable: true` validation support lands, we could not only remove #default_value, but also #required 😄

phenaproxima’s picture

Assigned: phenaproxima » wim leers
Status: Needs work » Needs review
Issue tags: +Needs change record updates

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().

phenaproxima’s picture

Issue summary: View changes
wim leers’s picture

Assigned: wim leers » Unassigned
Status: Needs review » Reviewed & tested by the community
wim leers’s picture

Per @lauriii.

wim leers’s picture

longwave’s picture

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? Or for one off cases, could we allow two closures to be declared in the FAPI array?

phenaproxima’s picture

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.

We can't use closures, as far as I know, because form arrays are serialized and cached, and you can't serialize a closure (PHP will fatal).

claudiu.cristea’s picture

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:

$form['foo'] = [
  ...
  '#config_target' => new ConfigTarget(
    'media.settings:iframe_domain',
    '::nullIfEmptyString',
    [SomeOtherClass::class, 'transformOnSave'],
  ),
  ...
];
phenaproxima’s picture

Status: Reviewed & tested by the community » Needs review

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

borisson_’s picture

Status: Needs review » Reviewed & tested by the community

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?

longwave’s picture

Status: Reviewed & tested by the community » Needs work

Added some comments.

alexpott’s picture

Discussed with @Wim Leers, @borisson_ 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.

claudiu.cristea’s picture

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

  • It's 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
phenaproxima’s picture

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.

longwave’s picture

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.

wim leers’s picture

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?

claudiu.cristea’s picture

...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)?

phenaproxima’s picture

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 pointed out in #45 and make reuse possible.

But also...this is all quite a bit more complex than what I proposed in #42. What was wrong with that idea?

alexpott’s picture

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.

phenaproxima’s picture

Status: Needs work » Needs review

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().

Everything in ConfigTarget is immutable (except for a couple of explicitly internal properties), but if a form alter needs to change it, they can just replace it with a completely new ConfigTarget instance.

wim leers’s picture

That is looking very encouraging! Thanks for the fast turnaround time 🙏

Curious what @longwave & @alexpott think 😊

wim leers’s picture

Assigned: Unassigned » phenaproxima
Status: Needs review » Needs work

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 😊

phenaproxima’s picture

Assigned: phenaproxima » wim leers
Status: Needs work » Needs review

Back to you for final review!

wim leers’s picture

Assigned: wim leers » phenaproxima
Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Zero remarks left. 🤩

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

I previously advocated in #3364506: Add optional validation constraint support to ConfigFormBase that this approach could not work. Clearly I was wrong! This is a massive improvement DX-wise. It'll make adoption of config validation much simpler and makes #2408549: Display status message on configuration forms when there are overridden values trivial to bring to Drupal core too! (And as a bonus: it actually does most of #3384790: Update all remaining ConfigFormBase subclasses in Drupal core to use #config_target already, to prove it works 👏)

@phenaproxima: you previously created a change record revision with the then-up-to-date #config_target stuff, can you update it? 🙏

phenaproxima’s picture

Change record updated!

phenaproxima’s picture

Assigned: wim leers » Unassigned
phenaproxima’s picture

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

phenaproxima’s picture

Oh, and @longwave for review.

phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
bircher’s picture

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.

alexpott’s picture

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

alexpott’s picture

Version: 11.x-dev » 10.2.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

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

  • 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....
alexpott’s picture

Status: Patch (to be ported) » Fixed

Discussed with @catch and we agreed to backport this.

wim leers’s picture

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 🚀

gábor hojtsy’s picture

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

alexpott’s picture

I've created #3398891: Do not require the config in #config_target to be listed in getEditableConfigNames() - 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.

wim leers’s picture

wim leers’s picture

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: #3398982: ConfigFormBase + validation constraints: support non-1:1 form element-to-config property mapping again.

alexpott’s picture

Status: Fixed » Closed (fixed)

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

matthieuscarset’s picture

Is there any documentation about how `#config_target` should/can be used?