Spin off from #2015697: Convert field type to typed data plugin for file and image modules.
[disclaimer: I personally don't think that's a big issue, but opened this at the request of @claudiu.cristea :-)]
Context
FieldDefinitionInterface, introduced in #1950632: Create a FieldDefinitionInterface and use it for formatters and widgets, merges the notion of field-level & instance-level settings into one single set of 'settings', because:
a) only "configurable fields" have a notion of 'instances', base fields have a single 'field" definition structure
b) 95% client code doesn't care where the setting comes from, they just need the setting value for the current field definition structure they work with.
c) this works transparently for code that only has a $field structure to work with and no $instance (i.e. code that works at a cross-bundles level, e.g. Views, some parts of Entity_ref).
Then there is the (rare) case where a field type uses a setting with the same name at both levels - Image fields currently build on this for their 'default_image' setting.
- It was decided in #1950632: Create a FieldDefinitionInterface and use it for formatters and widgets that one level would take precedence over the other.
- #2060003: Wrong precedence in Field::getFieldSetting[s]() when setting appears in both field and instance fixed FieldInstance::getFieldSetting[s]() so that the order is "instance wins over field" instead of the opposite - that's what image fields expect.
Problem
The issue is that whichever precedence we set for field-level vs instance-level settings within FieldInstance::getFieldSettings(), there is always code that needs the opposite.
"Instance wins over field" is the correct choice for most consuming code. But then there is ConfigFieldItem::settingsForm(), that is in charge of building the settings form for the *field* level settting. By definition, this code needs the field-level values of the settings, not covered by the instance level, to place in the form's #default_value.
The recommended way to access settings within a FieldItem class is $this->getFieldSettings(), which is a shortcut for $this->getFieldDefinition()->getFieldSettings(). But since $this->getFieldDefinition() is an $instance, $this->getFieldSettings() will give you "instance wins over field" values. See #2015697-31: Convert field type to typed data plugin for file and image modules.
Currently ImageItem::settingsForm() works around this with:
$field = $this->getFieldDefinition()->getField();
$settings = $field->getFieldSettings();
This works because in practice $this->getFieldDefinition() is a FieldInstance, so getField() works. Our current runtime flow currently never calls settingsForm() if the field is a base field, base fields are not configurable.
But strictly speaking, $this->getFieldDefinition() returns a FieldDefinitionInterface, and getField() is not part of that interface.
Even if base fields stay non-configurable and we never display settings forms for them, we might come to a point where Field UI displays a non-editable summary of their settings (like for widgets & formatters). Then the method in charge of displaying this summary output would have the same issues.
Comment | File | Size | Author |
---|---|---|---|
#24 | field-type-settings-forms-24.patch | 20.57 KB | effulgentsia |
#24 | interdiff.txt | 7.33 KB | effulgentsia |
#23 | field-type-settings-forms-23.patch | 26.31 KB | effulgentsia |
#21 | field-type-settings-forms-21.patch | 26.43 KB | effulgentsia |
#18 | field-type-settings-forms-18.patch | 26.37 KB | effulgentsia |
Comments
Comment #1
yched CreditAttribution: yched commentedSo my take on this, and the reason I consider this to be a non-issue, is:
- If a field type chooses to build functionality on "a setting appears at both field and instance levels", then it's its job to deal with the consequences.
- The consequences are: 'when used on a configurable field, you need some extra dance if you need to access the "field settings not covered by the instance level".
- That extra dance is:
And that is perfectly fine IMO. Some code has special logic in case it deals with configurable fields.
Comment #2
effulgentsia CreditAttribution: effulgentsia commentedI agree with #1, but you can also call
$this->getInstance()
instead of checking whethergetFieldDefinition()
happens to be an instance. Isn't that why ConfigFieldItemInterface has getInstance() as part of its interface? It's exactly the functionality of providing settings forms that makes a field type a "configurable" field type, and for that purpose, and that purpose only, it makes sense to act on the "instance" and "field" config entities directly, rather than via getFieldDefinition().Wouldn't Field UI need to choose whether to display the summary as a net summary, or as a partitioned summary of field and instance? If it chooses the former, getFieldDefinition() would be what it needs to act on, for both configurable and nonconfigurable. If it chooses the latter, then it would be meaningless to do that for a nonconfigurable field, so it should only do so for configurable fields, and therefore, safely act on getInstance() and getInstance()->getField().
Comment #3
yched CreditAttribution: yched commentedThing is, I'd really want to remove getInstance() :-)
We added it in the rush of the home stretch for the "field types as plugins" patch, when figuring out how to merge with the FieldDefinitionInterface that went in in parallel. But as we've seen in the various "convert D7 field types to plugins" issues since then, its existence makes it too tempting to use in other methods in FieldItem classes, de facto making then unusable for base fields. We don't want to end up with almost-duplicate field types, one that works for base fields, one that works for configurable fields - more on this in #2052135: Determine how to deal with field types used in base fields in core entities :-)
The same "field type" classes should be usable for base fields and configurable fields alike, and do their work using getFieldDefinition() / FieldDefinitionInterface only, without assuming that it is a "configurable FieldInstance". Current HEAD will then happen to never call settingsForm() / instanceSettingsForm() if the field type is used by a base field, but the rest of methods work the same.
Aside from this edge case about "settingsForm() & setting with same name at both field and instance level", this works really well. And special cases can use code like #1 when they need to differentiate.
Comment #3.0
yched CreditAttribution: yched commentedtypo
Comment #4
effulgentsia CreditAttribution: effulgentsia commentedOk. Do you think it makes sense to add a parameter to getFieldSetting(s)(), like $setting_source, that could be one of 3 constants: SETTING_SOURCE_DEFINITION (default), SETTING_SOURCE_INSTANCE, SETTING_SOURCE_FIELD? With those constants defined in ConfigFieldItemInterface and the logic in #1 implemented within ConfigFieldItemBase::getFieldSetting(s)()?
Comment #5
yched CreditAttribution: yched commentedMulling over #4 :-)
Meanwhile, opened #2074547: Remove ConfigFieldItemInterface::getInstance().
Comment #6
yched CreditAttribution: yched commentedre #4: yeah, would make sense I guess. We had considered adding such flags in getFieldSetting(s)() in FieldDefinitionInterface, but that felt wrong / convoluted. Adding that to the helper methods within ConfigField / ConfigFieldItem sounds like it makes more sense and is more manageable.
Not sure we'd need three constants though ? The default behavior + one "field only" option should be enough ? (then, could be a boolean flag or two constants)
An "instance only" mode seems like it would need to support code to spill within Field/FieldInstance::getFieldSettings(), which I'd rather avoid, and we have no use case.
Comment #7
effulgentsia CreditAttribution: effulgentsia commentedI was thinking $field->settings and $instance->settings would work so long as those are public properties of the config entities. And if we ever make them non-public, we'll need to add method accessors (separate from getFieldSettings()) for it anyway. There's a conceptual difference between "the settings stored in my config entity's yaml file" and "the total effective settings for the field item I'm defining" that we shouldn't remove.
What this is saying is that a field type in generating an instance settings form could always treat a configured setting for the field as a default for the instance. For example, if an image field's 'default_image' is configured to something at the field level, then on the instance settings form for a new instance, we want that as the default. Which then also means, on saving the instance, that would get saved to the instance's yaml file, and a future change of the field-level 'default_image' wouldn't propagate to the existing instances. I think that's pretty reasonable, but I don't know if we have any Field API history for that being undesirable. If we're ok with this behavior though in all cases where field and instance have the same setting key, then yeah, a boolean instead of 3 constants makes sense.
Comment #8
yched CreditAttribution: yched commentedDrats, that's right. instanceSettingsForm() has the mirror issue of course.
The behavior you describe in #7 paragraph 2 is not something we want IMO. It would be a semi-wanted byproduct of the current code, I don't think we want to support and maintain such a feature around "settings". It's also not the behavior image field expects for its 'default_image' anyway.
So then yes, this means a way to get "instance settings not merged with field settings", which is not something FieldInstance::getSettings() supports right now. If we want to add support for such flags in FieldItem::getFieldSettings() only, then it means fetching $instance->settings directly - but then we still need to merge 'default instance settings', because some might be missing in $instance->settings...
Comment #9
effulgentsia CreditAttribution: effulgentsia commentedHow's this?
Comment #11
effulgentsia CreditAttribution: effulgentsia commentedComment #12
andypostI think it's no-go because
getFieldSettings($source)
is hard to understand for developers.Expected argument is
$settings_key
not the settings storage...no reason in intermediate variable just
return $this->settings + ...;
Suppose DX-- here
Comment #13
effulgentsia CreditAttribution: effulgentsia commentedWould different methods, like getFieldSettingsFromFieldOnly() and getFieldSettingsFromInstanceOnly() be better DX, and if so, any suggestions for better names than that?
Comment #14
effulgentsia CreditAttribution: effulgentsia commentedAlternatively, we could make every single field type's instanceSettingsForm() method start with:
$settings = $this->getFieldDefinition()->getSettings()
, and every settingsForm() method start with$settings = $this->getFieldDefinition()->getField()->getSettings()
, and then not need a wrapper method in the base class? But I think we'd then need a comment above that line explaining what we're doing, and we'd be duplicating that for each field type.Comment #15
claudiu.cristeaWhat about disallowing the sharing of setting name between field and instance? What are consequences? What would be wrong with that? This will keep think clean and will drop any confusion.
Comment #16
effulgentsia CreditAttribution: effulgentsia commentedWell, currently, ImageFormatterBase::prepareView() has this code:
That kind of sucks. We should be able to replace all of this with
$fid = $this->getFieldSetting('default_image');
. Same for any contrib field type that wants to allow a per-instance setting to override a per-field setting.Here's a patch that I think addresses the DX concerns of #12.2. In other words, for settingsForm() and instanceSettingsForm(), we pass to those functions the settings that that form is responsible for. Everywhere else, getFieldSetting(s)() returns the correctly merged setting(s). Thoughts?
Comment #17
yched CreditAttribution: yched commentedYes, #2075095: Widget / Formatter methods signatures needlessly complex does that while touching ImageFormatterBase::prepareView(). That other issue might be tricky to get in fast though :-).
I've been considering whether we should drop support for "same setting name" too, but I had to agreee that Image use case, while not very frequent, is totally valid, and it would be sad to make it more complicated by forcing the setting to be split in two different names. Two separate settings would also look weirder when the field type is used for a base field.
I need to look more closely at the patch, but I don't really see the DX as particularly awful...
Comment #18
effulgentsia CreditAttribution: effulgentsia commentedThis addresses #12.1. I think that covers all feedback so far, so looking forward to more.
Comment #19
effulgentsia CreditAttribution: effulgentsia commented#18: field-type-settings-forms-18.patch queued for re-testing.
Comment #21
effulgentsia CreditAttribution: effulgentsia commentedStraight reroll.
Comment #22
yched CreditAttribution: yched commentedMy issue with latest approach is that injecting some $settings into the methods makes the status of the code a bit shaky:
the object still holds a FieldDefinition, but only parts of it should be trusted in those methods, settings should be read from the incoming param.
But more globally - sorry for this :-/, I think I'm turning sides and would advocate we just don't go out of our way to support "same name settings".
@effulgentsia #16 :
Myself #17:
... and it turns out it doesn't work of course :-).
We make sure that FieldInstance objects get saved with at least default instance-level settings. So $instance['settings']['default_image'] will always be present (possibly just an empty string). And FieldInstance::getSetting() always makes $instance-level settings, even if "empty" (for "some" definition of empty: NULL, '', 0, FALSE, 'im_empty'...), cover up whatever is at the field level.
There's no logic we can place in there to make "give me the instance setting, if "empty" look in the field" work magically - at least not without reconsidering the auto-population of default values, and I'm frankly not keen on going there only to support what remains an edge case...
So yes, the promise of supporting "same setting name" in instance & field is if consuming code could just "not care" and say "give me the setting" and we read it from wherever it is present. But I don't think we can do that.
Then instead, consuming code has to do extra work for "is there a value at $instance ? if not, I'll grab the $field and check in there" - and this, in code that is supposed to not know about $field and $instance but work only with FieldDefinitionInterface.
So in the end, I agree with @claudiu.cristea in #15. We should just drop that IMO.
The image field type can have a 'default_image' field setting and an 'instance_default_image' instance setting. Consuming code checks both places in order, not the end of the world. ImageItem can even provide a helper getDefaultImage() method that does that and let consuming code be simple.
Comment #23
effulgentsia CreditAttribution: effulgentsia commentedI disagree with #22. Will write up why in a bit. For now, here's a straight reroll. I'll clean up this patch a bit more, but posting the reroll first, so that I can attach interdiffs with subsequent patches.
Comment #24
effulgentsia CreditAttribution: effulgentsia commentedThis patch removes adding methods to Field[Instance][Interface]. That's out of scope for this issue and detracts from the main point of the patch.
Comment #25
effulgentsia CreditAttribution: effulgentsia commentedIt's not about trust, it's about meaning. getFieldSettings() is about getting the settings as they should apply to the field data being described by the field definition. It's analogous to getComputedStyle() for working with a DOM element's CSS value in JavaScript. For example, maybe we should add a drupal_alter() step at the end of FieldInstance::getFieldSettings() to allow modules to runtime alter the settings without affecting what's stored in the CMI files. Perhaps we don't need to, since CMI also comes with a context override system, but conceptually, we could add that drupal_alter() and the meaning should still make sense: everything that needs to act on a field data object would just start using those altered settings. However, providing the field or field instance settings forms has nothing to do with that meaning of getFieldSettings(). Instead, the entire purpose of the form is to provide a UI to what is stored in the CMI files. Injecting those settings into the form functions makes sense. Continuing the getComputedStyle() analogy, it would be like a browser plugin that allowed you to directly edit the CSS files of a web page: it would need to present to you the direct values that are in those files, not the net result of getComputedStyle().
That doesn't seem like that big a deal to solve. Surely, we can filter out NULLs either from what we save to the instance's CMI file, or from what is merged in getFieldSettings(). But even if we choose to not do that, and instead drop support for setting inheritance, I still think we should proceed with this patch for the reasons stated above.
That's a great workaround if we choose to not support automatic setting inheritance.
Comment #26
yched CreditAttribution: yched commentedNot sure I'm convinced by the "inject settings" rationale, it's still a bit of a WTF IMO - and the idea of a drupal_alter() on settings makes me cringe :-) But I'll mull over this a bit.
Main problem is submitted form values from instanceSettingsForm(). That would mean trusting implementations to generate forms that produce submitted form values that are consistently NULL (or some when the setting is supposed to be "not set, fallback to the field-level setting in case it's set". The current form for the instance 'default_value' returns '0' for "not set", for example. But '0' might be a perfectly valid value for a different setting. 'settings' is the land of field types, we currently don't assign any fixed semantic or behavior to the values. We store them as they're given to us and hand them back when asked for them, that's it.
Honestly, this feels like a can of worms to me, for what is an edge case with an easy workaround :-)
Comment #27
andypostWorking on #2013679-186: Helper issue for #1953408 (Remove ArrayAccess BC layer) I found that existence of
getFieldSettings()
is really confusingempty()
on setting value, suppose this is a killer of dxgetFieldSetting()
->settings[]
to access this by the right way (image and a lot of tests)Suppose suggested 3rd parameter will bring dx down because most of devs will prefer to access settings directly,
primary for
empty()
andisset()
So I suggest to get rid of it asap and use plain arrays
Comment #28
yched CreditAttribution: yched commented@andypost:
This is not what this issue is about. getFieldSetting() is not going anywhere, and we won't switch back to plain arrays - precisely because we need a merge functionality so that configurable fields' $field & $instance structures can act as FieldDefinitionInterface objects, and features can build on "base fields" & "configurable fields" indistinctly.
Note that you can still get the settings as an array, if that makes more sense for the piece of code you're writing, with getFieldSetting*s*().
Comment #29
andypostOk, so filed clean-up patch for image module #2087429: Remove deprecated call field_info_instance() in ImageFormatterBase.php
Comment #29.0
andyposttypos
Comment #30
tim.plunkettThis was bumped to major in 27, but with a proposal that is out of scope. So demoting.