Overview
At present `sourceTypeSettings` and `sourceType` in stored in the data model in the inputs column. This is because things like `hook_storage_prop_shape_alter` can modify the data shape used for a component based on the presence/absence of typed-data types. These can be quite large, see this example from in XB's tests
Additionally a component (SDC or Code) definition can change which can have an impact on instance settings. For example if a string prop in an SDC component is changed to an integer or a new value is added to an enum this can change either the source type or settings..
Proposed resolution
Change the definition of the `prop_field_definitions` settings for both SDC and Code components. Instead of making it a point-in-time definition of the props definitions - make it a version history.
These are the scenarios that would constitute a new version:
- Adding a new required prop - this is relevant if we go down the non JSON storage approach
- Removing a value from an enum
- Changing the mapped field type
- Removing a prop - although the net outcome here is redundant data in the database for old components - so we might not care here
If we take those scenarios as inputs, we should be able to create a determinstic hashing approach that gives us a unique version ID for each component and set of those constraints. → deterministicness not yet guaranteed, descoped to #3528159: Ensure deterministic version hashes for ComponentSource-specific settings, thanks to config schema-powered normalization.
We then should be able to store this hash as a version ID along with the component ID in the component tree.
Before
tree data
[
[
'uuid' => 'two-column-uuid',
'component_id' => 'sdc.experience_builder.two_column',
'inputs' => [
'width' => [
'sourceType' => 'static:field_item:list_integer',
'value' => 50,
'expression' => 'ℹ︎list_integer␟value',
'sourceTypeSettings' => [
'storage' => [
'allowed_values' => [
[
'value' => 25,
'label' => '25',
],
[
'value' => 33,
'label' => '33',
],
[
'value' => 50,
'label' => '50',
],
[
'value' => 66,
'label' => '66',
],
[
'value' => 75,
'label' => '75',
],
],
],
],
]
],
],
];
Component config
prop_field_definitions:
heading:
field_type: string
field_storage_settings: { }
field_instance_settings: { }
field_widget: string_textfield
default_value:
-
value: 'There goes my hero'
expression: ℹ︎string␟value
cta1href:
field_type: link
field_storage_settings: { }
field_instance_settings:
title: 0
field_widget: link_default
default_value:
-
uri: 'https://example.com'
options: { }
expression: ℹ︎link␟uriAfter
tree data
[
[
'uuid' => 'two-column-uuid',
'component_id' => 'sdc.experience_builder.two_column',
'version' => 'sIXdVsZK0yE', // 👈️ Prop definition version ID
'inputs' => [
'width' => [
'value' => 50,
],
],
],
];
Component config
prop_field_definitions:
current: sIXdVsZK0yE # 👈️ Store the hash of the current version
sIXdVsZK0yE: # 👈️ Keep each historic version, keyed by a hash
heading:
field_type: string
field_storage_settings: { }
field_instance_settings: { }
field_widget: string_textfield
default_value:
-
value: 'There goes my hero'
expression: ℹ︎string␟value
cta1href:
field_type: link
field_storage_settings: { }
field_instance_settings:
title: 0
field_widget: link_default
default_value:
-
uri: 'https://example.com'
options: { }
expression: ℹ︎link␟uri
vfTzLuF89Xp: # 👈️ This is an older version
heading:
field_type: string
field_storage_settings: { }
field_instance_settings: { }
field_widget: string_textfield
default_value:
-
value: 'There goes my hero'
expression: ℹ︎string␟value
cta1href:
field_type: uri # 👈️ Different field type
field_storage_settings: { }
field_instance_settings: { } # 👈️ Different field settings
field_widget: uri # 👈️ Different widget
default_value:
-
value: 'https://example.com'
expression: ℹ︎uri␟value # 👈️ Different data typeUser interface changes
Issue fork experience_builder-3523841
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
Comment #2
larowlan.
Comment #3
wim leersThis is very similar to the outline I wrote on #3520484: [META] Production-ready ComponentSource plugins.
The key difference: I tried to do it generically, not restricted to SDCs & code components. So, for example, Block plugins may change (evolve) their settings and provide an update path, and so XB should be able to apply such an update path. Layout Builder hasn't solved this yet either — see the #3521221: Block plugins need to be able to update their settings in multiple different storage implementations core issue
Comment #4
wim leersSpecifically doing this for only the
sdcandjscomponent sources does not make that much sense to me yet. What problem does that solve? 🤔To avoid the need to look up in some central place what field type + storage settings + instance settings + widget should be used for a given SDC prop is … exactly why
StaticPropSources contain all that information, and why that is stored in theinputsfield property of the XB field type.And … I think that is what you're aiming to solve here: storing less data in
inputs. Even though you omitted it from the issue summary: you only showed the before vs after for thetreefield property, not for theinputsfield property 😄 Updated issue summary based on this interpretation. But I'd like you to confirm it. 🙏Comment #5
wim leersVery closely related, and one of the oldest XB issues: #3463996: [META] [PP-1] When the field type, storage/instance settings, widget, expression or requiredness for an SDC/code component prop changes, the Content Creator must be able to upgrade.
FYI over at #3515629-16: FieldWidget's XB transforms must be bubbled by the Field Widget rendering to inform the client.2, I sketched how I now think this could actually happen automatically.
Comment #6
wim leersFrom @larowlan's issue summary:
I don't think I agree with stating these are things we have to support:
EDIT: see #3509115-6: [META] Plan to allow editing props and slots for in-use code components for a more detailed description of this.
hook_storage_prop_shape_alter()allows and what #5 is about. And it's what allows the storage efficiency improvement described in #4.\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\GeneratedFieldExplicitInputUxComponentSourceBase::validateComponentInput().So: what is your purpose with this issue, @larowlan? Storage efficiency, input schema evolvability, both, or something else still?
Comment #7
larowlanThe goal is both. At present we store all of this in the prop source (and in the inputs column) but the repetition of e.g. storing all of the instance settings each time we use it is very verbose. Consider this
That's a lot of data to store for every instance of the element prop in the heading component. All that is unique to this instance is the value , expression and the source type.
When we have a dynamic prop source we're only storing this
which is much more succinct.
The intention here is to try to hoist some of the repeated elements (e.g source type settings) to somewhere we can rely on them being fixed. The idea is the versioning gives us a mutable place to store them. And in doing so it also opens up some more flexibility with regards to recovery for stored data in previous iterations.
Comment #8
longwaveI agree that slimming down the
inputsto remove a lot of duplicated data is worthwhile, although this feels like it adds a huge amount of complexity - and would this be better off as some kind of new core API?We also still have to handle error cases: because config is mutable we might still end up with references to versions that no longer exist in the config object.
Comment #9
catchCould this be different config entities for different 'versions' of the component, instead of trying to store all version information in a single config entity?
That would potentially make it easier to determine whether any content is using the old version, allow old versions no longer in use to be deleted etc.
Comment #10
wim leers@larowlan in #7:
Hah! I should've known 😅
@longwave in #8:
+1 to both those statements.
🤔 I'm not sure I follow. Are you thinking of #3469082: Add way to "intern" large field item values to reduce database size by 10x to 100x for sites with many entity revisions and/or languages? If so: I don't see how that applies here, because this is not about "interning", but about compacting? If not that: can you elaborate? 🙏
@catch in #9: I don't think there will be a big number of "versions" of a particular component. So storing all versions in a single
Componentconfig entity seems reasonable. Especially because we aim to allow instances using the older version to be able to transition/update to the newer versions (until none of the old version are left, and the old version can be deleted).That being said … making the version part of the
Componentconfig entity ID would make what I summarily described in #3520449: [META] Production-ready data storage trivial:So that is definitely worth considering, too.
We'd just forcefully disable older versions of a
Component— only one version is allowed to havestatus: true, and hence only that one will be served to the XB UI by/xb/api/v0/config/component, and that way everything will continue to work exactly as it does today 🤓 (The client side is completely oblivious to these details; all it cares about is that it's given some opaque string to identify a component to instantiate.)I think @larowlan has a good (implied) point: if it's okay for us to rely on "external data" for
DynamicPropSources (aka theFieldConfigentity, the host entity an instance lives in, etc.), then why is it not forStaticPropSources?So: let's get a working PoC and then re-assess 👍
@larowlan Given that this will touch the
Componentconfig entity implementation, its config schema, and(Static|Dynamic)PropSource, I think it probably makes sense for me to take this one on. Then you can tackle #3468272: Store the ComponentTreeStructure field property one row per component instance at the same time? 🤓Comment #11
longwaveIf we have some concept of versioned config entities then maybe that can help with #3521221: Block plugins need to be able to update their settings in multiple different storage implementations as well? We can store references/dependencies to specific versioned config, plus have some mechanism that can (somehow) migrate to the newer versions as required.
Comment #12
larowlanWhole heartedly agree, was hoping you might be interested in taking this on ❤️
Comment #13
wim leers@longwave: yep, see my response to @catch above :)
Getting this going 👍
Comment #14
wim leersComment #15
larowlanDiscussed this with @effulgentsia and there is another third option where we split the 'field union' bits of the component config entity (aka the bits that store the field types and settings) into a separate config entity that is similar to what field union config entities look like.
And then the component config entity can reference this, as can the component tree field type.
So the data model would go from
to
We can use typed data setValue to detect when 'component_id' is set and default 'inputs' to the current version of the field union-y thing.
That gives us scope to have something 'dynamic field-union' like later
Comment #16
catch#15 sounds very encouraging.
Comment #17
wim leersI like it! Wish I thought of it! 😄
Consider it done. I'll be spending my AM getting this going! 🤓🕺
Comment #19
wim leersGot the foundations ready:
type: versioned_config_entity, a subtype fortype: config_entity\Drupal\experience_builder\Entity\VersionedConfigEntityBasesettings.local_source_idto the top level atsource_local_id(sibling to HEAD'ssource) — which makessettings.*(aka everything besides the intra-source plugin ID) toversioned_properties.settings.*— which makes them versionable 👍fallback_metadata(currently only slot definitions) toversioned_properties.fallback_metadatasource— we can use the version itself to determine when to use theFallbackcomponent source pluginThe 4 crucial tests largely (but not completely) pass (
(Block|SingleDirectory|Js)ComponentTest+ComponentValidationTest). Initially ~58% was passing , then ~84% passing and at end of day 98.57 % passing.Key next step (tomorrow): idempotently computing the version hash.
Comment #20
wim leerssource_local_idimmutable.I begrudgingly added recursive key sorting (copy/pasted from
AutoSaveManager::recursiveKsort()), which allowed me to get tests to pass more for SDCs, but still not completely. We shouldn't do that sorting here though, because the order could be meaningful.Worse, that wasn't enough. Root cause AFAICT: the configuration system upon saving sometimes converts
trueto1andfalseto''. This then unsurprisingly results in the hash not being the one that was expected.I have one bit of hope: applying the same trick as
\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\BlockComponent::fixBooleansUsingConfigSchema()— i.e. use config schema to perform the appropriate normalization. We'll see if that works out. If it doesn't, I think deferring validating the generated versions to a follow-up issue is reasonable. All of the validation at the very start is what truly matters for data integriy — this wasgoing the extra mile. 🤞
Comment #21
wim leers🤯
Core's got this:
… but that should obviously be
type: boolean.Fixing that alone makes a big difference: before (83.29% passed) vs after (86.89% passed).
Comment #22
catchI'm trying to understand what the implications of this are if someone changes things then changes them back again.
Will it be:
version 1
version 2
version 3 (identical to version 1)
Or... will it be:
version A
version B
version A
?
For an upgrade path, version 1 would need to be updated to version 2, then to version 3, but Version A would be able to remain version A if it hasn't already been updated to version B.
Apologies if this is hard to parse, that's the third rewrite..
Comment #23
wim leersNow using config schema as described in #20.2. That brings us down to 86.18% passing.
To avoid a single piece of (very detailed) validation to cause an already big MR to grow a lot in size (currently:
29 files, +862, −422), I commented out a single config schema line which gets us back to ~98% passing 👍We could live without this validation although it IMHO would be preferable not to.
I have to switch to other work now. To be continued, probably later this week. Let's hear from @larowlan if this is what he had in mind. 😊
Next steps here (beyond getting all tests to pass) are to update the
treeandinputsdata structures — although preferably that'd happen after @larowlan's #3468272: Store the ComponentTreeStructure field property one row per component instance is merged, to avoid huge conflicts and significant rework. Marking[PP-1]for that.Comment #24
wim leers@catch in #22: for the actual upgrade path logic, see:
#3524751: [later phase] Component Source plugins: generalized support for schema changes of explicit inputs, with within that:
This issue should IMHO be focused solely on:
Componentconfig entity know about/remember different versionstype: experience_builder.component_source_settings.(block|js|sdc), which itself actually is the result of a computation in the respectiveComponentSourcepluginsObservations for those 3:
ComponentMetadataRequirementsCheckerand friends detect that the explicit input schema has changed, judge whether that's feasible or not, and then for example disable the affectedComponentconfig entity. This is why my 3rd "solely about" point is so narrow.Comment #25
catch@WimLeers, well this has implications for the upgrade path.
There's a huge difference between:
version 1
version 2
version 3 (identical to version 1)
and:
hash 1
hash 2
hash 1 (identical to version 1).
in that there'd be no need to update hash 1 to hash 1, which would be good. I think that's what this issue results in, but it would be good to confirm.
Comment #26
wim leersThat's indeed what I'm aiming for, but per #20.2 and #23, I'm running into challenges that AFAICT are caused by the config system modifying the contained values 😅
Deterministic hashing also requires a deterministic input, and that's where the config system is currently getting in the way. That's the bit I'd want to push to a follow-up issue, because that likely requires complex tweaks and shenanigans. This MR in and of itself already moves us to a far better place.
Plus, we can completely transparently recompute the hashes at a future time, given they DO only rely on the stored settings. 👍 The problem is that those settings themselves are neither ordered nor casted deterministically today. AFAICT
\Drupal\Core\Config\StorableConfigBase::castValue()should help us get there, but that is AFAICT not available to me for use on arbitrary config subtrees:\Drupal\Core\Config\Config::save()calls it and nothing else does. I'd have to do some shenanigans for XB to be able to use that. That could easily end up being a day-long and multi-100-LoC endeavour, which is why I'm advocating for focusing on getting the mechanics right in this MR.Comment #28
wim leers@larowlan in chat:
Hah, I think it’s actually very simple: I wrote that on May 16 (Friday). Then I didn’t get to spend my AM that day on it like I said/hoped. 😑
I actually started on Monday, May 19. And I literally started from the POV: “how can we do this generically?” — because we likely want more versioned config at a later point. For example, product requirement
37. Revisionable templates— but I know that @lauriii has expressed the intent to do it for more config than that.Then I looked at the issue summary, and what I'd done was perfectly on track to achieve that.
tl;dr:I simply forgot about #15 🙈🙈🙈Comment #29
wim leers@larowlan also asked this in chat:
I kinda answered this above (basically: "oops!"), but digging into it a bit to help ensure the direction you end up taking takes some additional considerations into account 🤞
Having worked on both this and #3468272: Store the ComponentTreeStructure field property one row per component instance, I now have some questions about #15 (which looks super familiar having spent the entire day reviewing #3468272 🤠):
inputskey twice. That's accidental, right? I think It'd essentially be:ShapeMatchedFieldUnion) be?Is it the component ID that it was generated for? That'd make it behave much like the
FilterFormat–Editortwin/sibling pairing. In that case, I'd imagine you'd want to usetype: versioned_config_entityandVersionedConfigEntityBase.Or would the ID be something that's based on the contents of the field union? The appealing thing about that is that it'd allow multiple
Components to use the sameShapeMatchedFieldUnion. But: how often would that even happen?shape_matched_field_union_idkey-value pair that references another config entity … must actually itself be versioned, because we need to be able to know for a givenComponentconfig entity what all of its previously used field types/instance+storage settings were (aka all of the ways their explicit inputs have ever been stored).Without that, we won't be able to do #3524751: [later phase] Component Source plugins: generalized support for schema changes of explicit inputs (adding as related) nor #3463996: [META] [PP-1] When the field type, storage/instance settings, widget, expression or requiredness for an SDC/code component prop changes, the Content Creator must be able to upgrade (already linked).
Comment #30
larowlanThanks, yeah I actually think with the versioned config entity we still have the pieces we need to generate a dynamic field union
I'll have a think about how we can actually use that, I suspect it will be via typed data reference computed properties on the inputs column.
i.e. I'm thinking
$entity->get('component_tree')->get(2)->inputs->some_prop_that_is_modeled_by_a_link_field->urimight be possible.I think that's the kind of appeal we're looking to borrow from field_union (rather than the 1 table per field etc) - can you confirm @catch
Comment #31
catchI'm behind on the past handful of comments and only replying to #29 and #30 right now, hopefully doesn't make things more confusing.
The two things I think we benefit from something field union-ish:
1. A coherent representation of what field types are in use for the values that now get stored per row with
component_id | inputsin #3468272: Store the ComponentTreeStructure field property one row per component instance e.g. component A includes field types foo and bar. This ought to help with dependency calculation and various other things.I think that's what this issue is explicitly about.
2. The potential to use field unions as part of an overall data model in general
e.g. field unions being available as structured fields which can be laid out by XB, without having to be in exposed slots (the album track listing and other examples). It feels very likely that if this isn't available, people will hack together similar functionality via exposed slots, or continue to use paragraphs for that. It may not be necessary for XB iin tself, because for XB a field is a field, but it might be necessary for XB to displace paragraphs and/or circumvent people coming up with horrible workarounds for this use case.
I had hoped in #3440578-80: [PP-2] JSON-based data storage proposal for component-based page building that XB could 'build upwards' from a dynamic field union so that the XB field would be something like a compound field that included the dynamic field union + the extra slot and parent data. This is also a bit like what some of the approaches discussed in #3477428: [PP-1] Refactor (or decide not to) the XB field type to be multi-valued, to de-jsonify the tree, and to reference the field_union type of the prop values were like.
Now that #3468272: Store the ComponentTreeStructure field property one row per component instance is in, it's much closer to this, but not quite the same, but it feels like this issue could bring it a step closer again, and #15 seems like doing that.
e.g. I could imagine a dynamic_field_union module having a field where the two columns are 'field_union' (config entity reference) and 'values' (JSON).
And I think that #15 is saying that if XB splits the config entity, then there would be no difference between those config entities, so that they could potentially even be shared between the XB field and a field union module, or one could extend from the other, or something like that. And then XB would then have a second config entity for any extra bits that aren't explicitly about the field types.
Couldn't this information be determined by querying the field table itself - e.g.
SELECT DISTINCT(field_union) FROM xb_field WHERE component = %component(but a nice entity aggregate query).But also:
It seems useful to have this available.
Comment #32
effulgentsia commentedNow that #3468272: Store the ComponentTreeStructure field property one row per component instance is in, it would be helpful if the before and after examples in this issue summary were updated to be based on the new schema that landed in that issue.
AFAICT, the "after" in this issue will result in the
inputscolumn being reduced to containing only thevaluekey for each input, is that correct? For example, for an instance of aheadingcomponent with 3 props, theinputscolumn would look like:If I'm understanding that correctly, then would it be reasonable for the scope of this issue to also include removing the
valuekey since it would become superfluous? In other words, could we simplify the above to:Comment #33
effulgentsia commentedComment #32 is based on the assumption that we're versioning the union of the inputs, not each input individually, and therefore the "version" (or "field union ish" config entity reference) would be pulled out into its own column, but please correct me if that's an incorrect assumption.
Comment #34
effulgentsia commented#32 shows an example that only includes scalar values for each input, but it would work for objects/arrays as well. For example, if you have an input that's an array of links, you could still remove the superfluous
valuekey. I.e., instead of:you could have:
Comment #35
larowlanI think we might be talking about two problems here and how we solve them requires coming at this problem from the two different angles.
On one hand the 'a field is a field' is indeed the domain of custom field or a (completed) field union module (or tools like drush generate that will create you a custom plugin). This is the best solution for structured data - I think the album track listing falls under that use-case. I think modelling that as components is wrong as you lose all of the strengths of Drupal. If you need to do presentation of that field with XB, dynamic prop sources can cover it.
The other problem where there's repeating multi-column data is purely presentational - e.g. something like a carousel with an image field, a caption and a blurb, there's little re-use of that so structured data isn't as applicable. I think #3467890: [later phase] Support `{type: object, …}` prop shapes with single level that require *multiple* field types: use `field_union`? — OUT OF SCOPE: nested components/component reuse is how (as I understand it) XB plans to tackle that. That in itself does involve either borrowing from or leveraging field union. We already support array structures in SDC definitions when the array items match an existing field type (i.e multi-value text field, link field) but not for arbitrary object shapes - and that's where field union (or borrowing from) is useful - effectively deriving fields from thin air. I think that works with the pieces of field union that already exist (data structure and widgets). The bits of field union that are still in progress (UI to manage all the things) aren't needed here because we don't need people to build them from the UI, we just need the config entity and the widget to exist.
I think this discussion would be easy if we had have had enough time in the last 7 years or so to finish field union. But we haven't - the most feature complete work is in the branch of #3011353: Add UI for adding unions (field_union_ui.module) - it contains the pieces we need (widget, config entity) but no formatter - which in an XB world we don't need. I think there is merit in a spike to lift those pieces out of Field Union into XB with #15 as the data model. I think that is probably 2-3 days work to validate it. If that happened and this ended up living in XB rather than Field Union I would be OK with that. Field Union has never even had an alpha release and hence if it lives on in a different form that is totally fine. If XB ends up in core in the future, even better. Field Union then just becomes 'field union UI' module and includes the forms/routes etc for creating the config entities and a formatter for anyone not using XB for entity rendering.
Updated the issue summary per #32 to #34.
Comment #36
larowlanIf we do pivot to the approach in #35 and #15 then the work in the MR here isn't wasted because as @wim leers points out (I thought here, but perhaps elsewhere) there is a product requirement to version content templates and patterns.
Answering the questions in #29
1) yes that was an accident, there shouldn't be two inputs, the first one should have read 'field_type' or 'schema' or whatever we call the new secondary config entity.
2) I would think 'DerivedFieldType' because I think there's a use case to use it with arrays of objects per #3467890: [later phase] Support `{type: object, …}` prop shapes with single level that require *multiple* field types: use `field_union`? — OUT OF SCOPE: nested components/component reuse (what a neat nid, just needs a 5 in there). If we know we're going to need this functionality to make that happen, it makes sense to evaluate if it is feasible here and smooth the path to that in the future.
3) I think it could just be a new config entity, because it is effectively a new field type
So do we think there is merit in doing a spike on #15 / #35 ?
Comment #37
wim leersYes, please! 😊🙏
Comment #38
catchThe more concise structures in #32/#34 look great, it's not a massive reduction in storage requirements but it's definitely worth having, and also more readable for me. Also 100% agreed with #35.
Comment #39
larowlanThinking about this some more, the minimal dataset in #32 and #34 omits the sourceType and expression. I have an idea about how we might do that for static prop sources here. We'll need to retain that for non static props (Dynamic, adapted) but I think that's ok as they are less common.
Comment #40
wim leersDid an early peek at @larowlan's in-progress branch — observations:
experience_builder.composite_field.*:fields.<key>.field_typewon't itself allow referencing a composite field (type), right? Related: #3467890: [later phase] Support `{type: object, …}` prop shapes with single level that require *multiple* field types: use `field_union`? — OUT OF SCOPE: nested components/component reuse.type: experience_builder.composite_field:.*.third_party.experience_builder. Why not keep that information ontype: experience_builder.generated_field_explicit_input_ux? 🤔 The newCompositeFieldconfig entity type must by definition not have a reference back to theComponentconfig entity that will use it, which means I don't see any way for being able to validate (aka ensure data integrity). Whereas it'd be trivial to validate if these key-value pairs remained ontype: experience_builder.generated_field_explicit_input_ux!CompositeFieldType::isInUse()will only work for content entity types, so it won't work for e.g. an XBContentTemplateconfig entity — won't this be problematic if this component (and its associatedFieldCompositeconfig entity) happen to only be instantiated in config entities? I see that::preSave()explicitly relies on::inUse(), so that seems like a date integrity problem? Or … I bet you're going to address this using apre_savehook implementation that makes it also respect XB's (atypical!) use of fields in config entities? 🤓Comment #41
larowlan1) yes I hope we can add typed-data integration to easily set/get inputs by prop name on the inputs column in ComponentTreeItem so folks dont need to learn the ins and outs of it being JSON encoded, see comment #30 for the type of DX I'm thinking about
2) my hope is that it will, I've got one eye on that as I'm working on this. The code I touched in
\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\GeneratedFieldExplicitInputUxComponentSourceBase::getPropsForComponentPluginI hope to be able to refactor further into a service (or perhaps method on the existing matcher service) that takes an array of props rather than a component plugin. Because then we could also use it for object shapes and nest them.3) if we store this in the source plugin settings, we need to be able to version it still. Either way I'm still not sure about that and the interdependency between the two entities, I may end up moving it back and adopting the versioning logic you have in your MR. I should know today/tomorrow if the current approach will work or not and will pivot as needed.
4) At present this is also looking ahead to object shapes and more generic (structured data) uses of the composite field type where we have a fixed table schema and those changes can't be made if a dedicated field table has been created. I don't think it matters when we're talking about JSON or config mappings. But yes, its very rudimentary at this point and could be expanded to consider the dependencies data if we need it to. The structured data approach would require a separate module for adding these via the UI and would likely live at https://drupal.org/project/composite_field - a namespace we grabbed a while back after discussions amongst Drupal CMS team in relation to a possible requirement for 'field_union' in the long term in Drupal CMS but with a better name. If XB has the config entity and field type, then that module would just be the UI (including a widget). I've left a few todos in the code to date referencing that end goal.
Comment #42
larowlanToday's progress:
inputsto #32 and #34\Drupal\Tests\experience_builder\Kernel\ApiLayoutControllerPostTest::testWithDraftCodeComponentis failing because previously we were relying on prop field definitions being stored in autosave, will see what the options are for that tomorrowI think its probably a good point to stop and decide if we should keep going with this approach
Remaining tasks:
Comment #44
wim leersReviewed this
34 files, +2899, -166diff at a high level.Componentconfig entity is unsurprisinglyComponentValidationTest. But unfortunately that currently fails hard 😅ComponentTreeItemListTestnicely illustrates the simplification for the stored data 👍::applyComponentTreeItemDefaults(), and I'm especially concerned about how it's used byClientServerConversionTrait::clientModelToInput(). Left a lengthy comment there.ComponentInputsEvolutionTestis fantastic! But … see the aforementioned comment for why that actually doesn't cover the full gamut of possible input evolutions.AFAICT there are more remaining tasks:
field_union🙏 I bet that >2K of the new lines are coming fromfield_union?I still am optimistic! So, I'd say: start with my main concerns (explained in detail on the MR), because if you can overcome/explain away those, then I think we're on track here 🤓
Comment #45
larowlanWill reply in detail shortly
Been thinking about this overnight
I think the third party settings and the component source specific aspects (eg the need to apply defaults in client conversion) make this approach non tenable
The third party settings show that while the schemas are close, they don't align
The composite field work we can copy into a branch in the issue for adding generic object shape support, so it isn't wasted work
The static prop source defaults work can be combined with Wim's versioned config entity and we can add a version column to the field item that will apply to all components not just specific sources. That will give us the consolidation of the inputs column we're chasing here
I'll change course to that today
Comment #46
wim leersKinda relieved to read my concerns about third-party settings are (seemingly) confirmed by you.
I read that as "versions for all
Componentconfig entities, not just some, and stored as a newcomponent_versionfield property for content entities and a same-name key-value pair for config-defined component instances" — which is exactly where I thought this was going last week 😄👍Can't wait to see what you cook up for me by your EOD/my start! 🤓
Comment #48
larowlanOne final issue with the composite_field_type approach is that storing prop field definitions in a separate entity that is only written when the component is saved means we lost the ability to have draft JS code components drawn from autosave.
Comment #49
wim leersTo enable @larowlan to focus on the actual tricky bits, and to avoid him having to spend time fixing the remaining test failures I left in #23 (8 days ago), I got that out of the way. I also addressed the
@todofor adding validation.All PHPUnit tests are passing, except for one. That one actually reveals a severe flaw in what I did 😇😬
The last failure:
… this is because the way
Componentconfig entities are currently validated per the current config schema, it is required for all past versions of aComponentto have valid source-specific settings.However … in the case of code components, it's possible that new props/slots are added! 😅 (More to come on that front in #3509115: [META] Plan to allow editing props and slots for in-use code components.) Which then fails due to
And that is the big flaw: that actually only the active version MUST be valid, older versions MAY be valid. It's impossible to perfectly validate them, given the old implementation is no longer around. Will try to push a solution for that, too.
Comment #50
wim leersDone:
(I'm aware of the clean-up potential. I'll let @larowlan refactor from this — AFAICT — working state to his liking 😊)
EDIT: looks like I made a silly mistake in there somewhere, because the test failures seem to look like this:
🤣
Comment #51
larowlanThanks!
Comment #52
wim leers@larowlan I don't understand why this commit brought back the
ComponentIdPartsconstraint, the::setSource()method etc. — could you explain? 🙏Comment #53
wim leersReduced
PHPUnitCI job from 5 failures, 8 errors to 2 failures, 1 error.But … that's misleadingly low:
FallbackInputTestpasses, but triggers some worrying PHP warnings, that I believe is related to #52. Which also leads to an informed guess about the answer to the Q I asked in #52: because of how A)VersionedConfigEntityBase::createVersion()always usesactiveas the identifier for the active version, B)type: experience_builder.component.versioned.active. The combination of those two means that only the active version has its source-specific settings validated (for IMHO good reasons, to solve #49), but that then of course fails when the component source is changed tofallback! 😅That means the comment I added is wrong:
… because as of that commit, the
activeversion might point to theFallbacksource 🙈 IOW, that commit made this dead config schema:So my solution for #49 was incorrect or at best incomplete. I'm hopeful I got it right this time 🤞
Comment #54
wim leersD'oh, I missed @larowlan's comment that would've explained #52 🙈
Doing a bit more work on this before passing the baton to @larowlan.
Comment #55
wim leersLinking related core issues. PHPUnit tests are green.
Back to @larowlan for:
… so we can hopefully land this MR soonish 🤞
Comment #56
larowlanComment #57
larowlanI think this is ready, there's a couple of open threads on the MR for creating followups, one for core, one for additional validation.
Comment #58
larowlanComment #59
wim leersComment #60
wim leers'fallback' is a version, not a plugin is genius! 👏
The one significant change in the validation logic I really didn't understand led me down a pretty deep rabbit hole, but I managed to crawl out 🐰
At the very end, I also scrutinized
ComponentInputsEvolutionTest, because that's what proves:Componentconfig entities can existClarifying that test coverage happened in the last batch of commits. (And the expanded test coverage revealed a few small bugs 👍)
Follow-ups:
79 files, +2578, -716— that follow-up won't change the data storage, but will ensure integrity. (This is also what I tagged this issue for in #23.)ComponentInputsEvolutionTestwith "the other side of the coin", already happening in #3501708: Prove that it *will* be possible to apply block settings update paths (assuming #3521221 in core) to stored XB component trees in config/content.versiontocomponent_version, which would make it matchcomponent_idnicely. But perhaps the better way forward is to renamecomponent_idto justcomponent. Extracted a bikeshed issue: #3528167: Rename `version` field property to `component_version`.Comment #61
wim leersI thought the issue summary actually was fairly up-to-date, so I removed that tag. But the issue title does not nearly adequately capture the scope. Trying to improve that prior to merging 🤓
Comment #63
wim leersComment #65
larowlanOpened https://git.drupalcode.org/project/experience_builder/-/merge_requests/1095 for some more simplification and added robustness in ComponentTreeStructureValidator
Comment #66
larowlanI agree this can be lower priority but I think we need to at least audit all places in the frontend that are looking at the component list (drawn from the component API in Drupal) to get field data (things like json schema etc) and confirm they are compatible with versions.
I have a sneaking suspicion that we might need to special case editing legacy versions - opened #3528284: Add e2e tests that prove we can edit an old version of a component to address that, so one more followup
Comment #68
wim leersThanks for that follow-up MR — very nice to see
ComponentTreeStructureConstraintValidatorbecome simpler 👍Comment #69
wim leersI opened #3528362: Deterministic Component version hash should depend not only on source-specific settings, but also slots + explicit input schema yesterday, that was an oversight here (and kinda in #3528159: Ensure deterministic version hashes for ComponentSource-specific settings, thanks to config schema-powered normalization).
@larowlan identified another oversight: #3528528: `ComponentInputs::getPropSources()` needs to take `GeneratedFieldExplicitInputUxComponentSourceBase::rawInputValueToPropSourceArray()` into account.
I think I spotted one more gotcha: the
\Drupal\experience_builder\Audit\ComponentTreeDependencyRepositorylogic + DB table looks at the dependencies of theComponentconfig entity and stores those calculated dependencies in thexb_component_tree_dependenciestable.As of this issue, it should be calculating and storing the dependencies of the referenced version of the
Componentconfig entity used in the component instance — which could be an older version! Fortunately, @larowlan got that right here:\Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem::calculateFieldItemValueDependencies()does:(unchanged here!)
which combined with
and
means that the correct version is indeed used 👍
The oversight: we didn't expand test coverage to explicitly test this.
Comment #70
wim leersBeing fixed at #3528499-20: Revisit storage of dependencies in separate table now we have separate deltas per component + remove plugin dependencies + make component version usages auditable.
Comment #71
wim leersMissing test coverage landed: #3528499-22: Revisit storage of dependencies in separate table now we have separate deltas per component + remove plugin dependencies + make component version usages auditable.