Problem/Motivation
There might be use cases in custom/contrib where we have to take a different decision depending on if we are comparing a field item list from within ContentEntityStorageBase::hasFieldValueChanged, ContentEntityStorageBase::populateAffectedRevisionTranslations or from within ContentEntityBase::hasTranslationChanges.
ContentEntityStorageBase::hasFieldValueChanged needs to compare only the values that are saved to the storage that would be target_id only in case of entity references.
ContentEntityStorageBase::populateAffectedRevisionTranslations and ContentEntityBase::hasTranslationChanges on the other side might need much more complicated decisions to be made e.g. in our system we have translation changes on a entity reference field if the referenced entities have been changed which we are mainly using for inline editing. In this case FieldItemList::equals is not sufficient because of two reasons:
1. We are not able to differentiate wherefrom the function is called.
2. We do not know the language for which it is called in case of not-translatable field but ContentEntityBase::hasTranslationChanges might still wanna compute differences only for entities of a specific language as when the parent is rendered its references are retrieved in the current language of parent.
Proposed resolution
To solve the problem we introduce a new method FieldItemListInterface::hasTranslationChanges which has two parameters - one for the list to compare against and one for the language to consider. This new method will by default just call ::equals, but to give more developer possibilities the new method will be called to compare the lists from within ContentEntityBase::hasTranslationChanges instead the equals.
Remaining tasks
Review, Change Record, Commit.
User interface changes
None.
API changes
New method FieldItemListInterface::hasTranslationChanges.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #31 | 2826021-31.patch | 15.15 KB | hchonov |
| #31 | interdiff-30-31.txt | 1.71 KB | hchonov |
| #30 | 2826021-30.patch | 14.76 KB | hchonov |
| #30 | interdiff-24-30.txt | 5.77 KB | hchonov |
| #24 | has-affecting-changes-2826021-24-interdiff.txt | 11.72 KB | berdir |
Comments
Comment #2
hchonovComment #3
hchonovComment #4
hchonovComment #5
hchonovComment #6
hchonovA new different approach which I think is better. Introduce a new method \Drupal\Core\Field\FieldItemListInterface::hasTranslationChanges which is used by ContentEntityBase::hasTranslationChanges instead the equals method and the new method has also a $langcode parameter in order for it to be able to estimate changes of computed properties based on a specific translation even if the field is not translatable which is needed to populate correctly the revision translation affected flag. The equals method on the other side remains used by the storage to check for direct value changes.
P.S. There will be definitely fails where the field item list is mocked in tests, so after the patch here fails I will take a look where we have to adjust the tests.
Comment #7
hchonovComment #8
hchonovIt needs a change record as well if we go with this approach.
Comment #9
mkalkbrennerThe patch in #6 would be a great improvement. In Search API we currently re-index all translations if an entity gets saved.
I would love to only re-index the translation that really changed.
The equals method for all fields is not sufficient when it comes to entity references.
Depending on the view mode the "rendered item" that is stored in the index needs to be re-indexed if a referenced entity has a change in the targeted translation.
All this stuff becomes relevant if we think about the various inline entity edit forms.
I'm aware that this might be hard to understand just from the perspective of the current drupal core. The patch just introduces the possibility to overwrite equals() in custom or contrib modules in way that allows a dedicated decision per language.
For our use-case the patch in #6 gave us a big performance improvement.
Comment #10
hchonovAdded test that hasTranslationChanges will behave the same as equals currently in core and updated the issue title and summary for the new approach.
Comment #11
tstoecklerVery interesting use-case. I think the method makes sense. Would be nice to have an implementation of it in core, but I guess that doesn't really make sense with the core field types.
I think this is another case for the "embedded" flag for entity references introduced in #2824097: Deep serialization for content entities to make an exact snapshot of an entity object's structure based on its current state. With that we could actually implement something like #9 in core if we had that (which would then be a first implementation in core). Although that - together with #9 - brings up an interesting point. In #2824097: Deep serialization for content entities to make an exact snapshot of an entity object's structure based on its current state we have thus far only considered the widget/form side of this, for which serialization is an issue. #9 is now talking about the formatter/view side of this (even though it's not actual Field API formatters (I think?), but still). Not sure yet if that's good (we can re-use the same setting) or bad (the setting is actually not well suited to be on the field-level).
Comment #12
hchonovWe could implement the method for the core EntityReferenceFieldItemList as it would help a lot in tracking changes when dealing with entity references. We have already implemented the algorithm in our custom field type and we would be happy to share it and probably see it core but I think it would be better if we first make this issue in and then in a follow-up discuss adding a special logic to EntityReferenceFieldItemList.
As currently there is no other real use case but only this one it would not make much sense of providing some artificial tests but the unit ones that we already have.
Comment #13
hchonovI was just addressed by mkalkbrenner about the patch and actually there is a small difference between the name and the returned result of the new method. Currently in the patch hasTranslationChanges returns the same as $equals, but actually it should negate the result :).
Comment #14
hchonovI've just realized that we were calling hasTranslationChanges with "x-default" as langcode when iterating over the fields of the default translation instead with the real default langcode.
I've made the change in the new patch version.
Comment #16
hchonovComment #18
mkalkbrennerre-rolled
Comment #21
james.williamsI *think* core does have a use-case for this: image fields. See #2941092-3: Add a $properties argument to FieldItemList::equals(). Image fields have height & width properties, which get recomputed on save if they are empty... in which case they can be ignored when checking whether there are translation changes. The approach in this ticket is a million miles better than the nasty patch I supplied to that other ticket as a temporary workaround :-)
Comment #22
catchThis is an older issue than #2960253: [meta] Allow Paragraphs widget/field and similar use cases to to be considered translatable but that issue has more recent activity, so I'm closing it as duplicate per discussion over there. I've transferred issue credit and hopefully see you over there.
Comment #23
berdirAs discussed, I'm reopening this as we agreed on splitting up my issue into 3 separate ones for each change and this is one of those 3.
Comment #24
berdirThis is a combined rebase as well as merging the alternative version from #2960253: [meta] Allow Paragraphs widget/field and similar use cases to to be considered translatable. Specifically, I kept the name we had there (hasAffectingChanges) as plach and I discussed that extensively. I also kept our version where we moved more of the logic into the method (also the ChangedFieldItemList and computed field check).
Instead of checking for $this->activeLangcode vs $this->defaultLangcode, I switched to $this->language()->getId() because that has that logic already included. Means 2 method calls more, though.
Also had to to update the unit test as some additional logic was added to equals() that requires property definitions.
Issue summary is not yet updated.
Comment #25
hchonovLet's save the language code in a variable outside of the iteration over the fields, so that we reduce the calls for retrieving the language code.
The general approach is to first check that the field isn't computed, which we override and skip in
ChangedFieldItemList::hasAffectingChanges(). This makes me think, that probably we don't need the check at all, but we have to implementComputedItemListTrait::hasAffectingChanges()and returnFALSEthere while inFieldItemList::hasAffectingChanges()we only delegate the call toFieldItemList::equals(). What do you think?Comment #26
tstoecklerNo opinion either way, just a quick note that - as @amateescu taught me - because
ComputedItemListTraitis agnostic of the field system (i.e. it deals withItemLists, notFieldItemLists) we should instead create aComputedFieldItemListBaseor so that handles the field-specific logic.Comment #27
hchonovHm, but we use the trait in
\Drupal\path\Plugin\Field\FieldType\PathFieldItemListand in\Drupal\entity_test\Plugin\Field\ComputedTestFieldItemList, which are FieldItemLists and not ItemLists?Comment #28
tstoecklerRight. My point is that we shouldn't add field-specific logic to the trait itself as it can also be used for (non-field) item lists.
Comment #29
hchonov:(
Comment #30
hchonovWhat about keeping track of the non-computed fields in a static property in
ContentEntityBase, keyed by the entity type ID and the bundle? This way even when dealing with a lot of entities e.g. nested entity references we'll optimize the saving by having less calls to determine if a field is computed or not and which fields we have to check. We actually already have the function which we could/should use for this -ContentEntityBase::getFieldsToSkipFromTranslationChangesCheck(), where we have to iterate through the fields and retrieve the computed ones as well and exclude them from the fields being checked.I've implemented this in the attached patch so that it is more clear what I am suggesting.
Comment #31
hchonovFixing #25.1 and the test fails in #30.
Comment #33
berdirI'm not quite sure how I feel about introducing static properties for that computed check, I doubt that the performance difference of that is measurable. Also, it's not a regression from this patch, we already had the check, the only difference is that it's slightly later, so we do the filter calls already and then decide to ignore the field.
Are we 100% sure that a computed field never wants to affect this? My reason for moving it into the field item class was to support that (maybe just theoretical) use case. A computed field could still be backed by storage, what about the path field, should having an alias on a translatable path field count as translation changes?
Comment #34
plachI agree that it would be consistent with the route taken here to allow also computed fields to specify their custom logic around affecting changes, if they really need to.
Comment #35
hchonovRe #33:
Consider saving 30 entities with 10 translations each and 15 fields, 10 of which represent the entity metadata fields. The entities are saved with no changes.
This means that if we check in
FieldItemList::hasAffectingChanges()if the field definition is computed, then we'll call 30x10x15 = 4500 timesFieldItemList::getFieldDefinition()and through it 4500 timesFieldDefinitionInterface::isComputed(). This sums up to 9000 function calls triggered byCEB::hasTranslationChanges()by simply saving the entities without any changes.During the saving
CEB::hasTranslationChanges()is being called for each translation at least 2 times:and additionally 2 times from constraint validators:
Which means that the function calls would be not 9000, but at least 2x9000=18 000.
With the solution I suggest we would have only 15 function calls to
FieldItemList::getFieldDefinition()and through it 15 function calls toFieldDefinitionInterface::isComputed()per entity type and bundle combination. In the worst case, where the 30 entities are from different entity types each, which kind of architecture I doubt, we would have only 30x(15+15)=900 function calls triggered byCEB::hasTranslationChanges(). Which sums max up to 900 calls no matter how oftenCEB::hasTranslationChanges()is executed during saving or during the whole runtime.The difference in the count of function calls between the approaches is 17100 calls.
Function calls in PHP are expensive. Running the following code shows that:
But I don't think, that this is the issue where we have to discuss whether computed fields should or should not have the ability to influence the output of
CEB::hasTranslationChanges(). This would be a completely different feature. Currently inCEB::hasTranslationChanges()we skip those fields from the check, therefore I don't see any reason to change this behavior in the current issue. We don't even have an use case for this, so why should we suddenly support it in the current patch when we don't need to?Comment #36
berdir> Function calls in PHP are expensive.
Yes, but it's all relative. Saving an entity does hundreds of method calls :)
Well, this issue is exactly about making the existing check more flexible. We already renamed the method to hasAffectingChanges() so that it is also not specifically about translations. And I already provided a possible use case above actually, the path field.
Right now in HEAD, I can edit a translation, change nothing except the path field, save it and hasTranslationChanges() return FALSE because the path field is computed. Is that really the expected behavior? Maybe, maybe not, I also don't want to change that behavior here in this issue, but why enforce that such a field can't have a say in what the result of this check is?
Comment #37
hchonovI don't think this is an argument to introduce thousand more when having the option not to.
The main idea of this issue is to enable non-translatable fields like ones holding inline references to be able to hook into the decision of
CEB::hasTranslationChanges().It never did and allowing it might be considered a BC break.
From my point of view this is exactly the right behavior, because this information is stored somewhere else and doesn't affect the content of the entity itself.
Because we never needed this feature and still we don't have a real use case for this. All I am asking for is a realistic use case. If there is none, I don't see the point into supporting this feature.
Comment #38
mpp commentedComment #39
plachIn the name of progress, could we drop any non-critical objections we have on each other's code and make sure this lands before alpha? Forcing people to rely on patches for Paragraphs for the whole 8.6 cycle seems a way worse strategy than accumulating some more technical debt, assuming we are doing so.
Comment #40
plachComment #41
berdirI'll try to get back to this asap.
One thing I noticed is another problem with the usage of this in hasTranslationChanges() or more specifically, the constraint.
We've been testing this in our distribution as part of enabling and using content moderation. We've been running into some problems with more complex field types, problems identified so far:
GeolocationItem: It has a bunch of extra properties that it recalculates in preSave(), but that's only called after validation. I had some success in fixing that by doing that logic in setValue(), or maybe an onChange so that the computed + stored properties are always in sync if you change lat/long. However, it also has a map property that isn't set at all and then the properties don't match. Worked around that by initializing that in setValue() to an empty array, but that's kind of an API change?
WebformEntityReferenceItem: Same deal with a map property.
What seemed to help with the data properties was to put an array_filter() into the callback in FieldItemList::equals(), but not sure if that makes sense? Does seem fine to ignore a property being NULL/''/[] with one that's not set at all, which == fails on?
Comment #43
hchonovRe #39:
In that case it makes more sense to stick to the current behaviour, where we're determining in ContentEntityBase which fields to check for differences and leave the discussion about changing this for later.
Re #41:
But this isn't a fault of the current patch, because the equals method would've returned the same, right?
Comment #44
berdir> But this isn't a fault of the current patch, because the equals method would've returned the same, right?
No, it is not a fault of the patch. But those examples show that the problem described in the issue title are not yet fully resolved, there is a difference as to when the method is called (before/after preSave()) and also a problem with map properties.
The main difference is the way it is used. For the save part, it's an optimization, if it returns non-equal, we save the values, nothing is broken, it's just slightly slower. However, for this method/validation, it is hard true/false check, either it is OK or it's not, and then you can't save.
I'm not quite sure how to proceed, if we can improve something at all, just wondering if we should keep this issue open and commit the patch we have in yet another related/sub issue?
Comment #45
hchonovThe issue problem stated two problems that the current patch is solving:
Both problems are solved by the patch. If you think we have to change the issue title to better fit the goal of the issue, then we might.
We have a similar case where we're changing values in a field
preSavemethod, but we've added a special behavior to the new field method for detecting this. This is what the new method is there for, not simply to forward the call to theequalsmethod :).IMHO the problems you've described should be solved in the
GeolocationItemand the current patch is ready to go.Comment #46
james.williamsI totally agree with this, in the sense that typed data item classes should be able to do what they like with their own data structures, so they then need a way to indicate how a data structure is to be considered 'changed' or not.
Note that core's own ImageItem also needs something implemented for this, as its 'height' and 'width' properties have the same kinds of issues as the contrib GeolocationItem, i.e. they are filled in by its preSave() method, when needed. I suspect follow-up issues are best for item classes though anyway, no need to delay improving the core API / field processing.
I trust my change to the issue title helpfully reflects where this discussion/work has now progressed. I.e. there are multiple places that check for changes, translation is just one of them.
Comment #47
hchonovThe
ImageItemis a good example, but actually itspreSavewould change thewidthandheightproperties only if the referenced image entity has been changed. So in this case theequalsmethod is able to determine that there are changes before thepreSavemethod has run. We don't need the information which properties have changed. We only need to know if there is a change on the item and this works currently. If a change on propertyAtriggers a change on propertyBin the field itempreSavemethod, then we still have the needed information for theequalsmethod, no matter if it runs before or after thepreSave.. With theGeolocationItemits probably the same story.Comment #48
james.williamsUnfortunately it's not quite that simple. The widget used for images can end up submitting with no change in the referenced image, but clearing the height & width. So currently that's then considered a change, even though the
preSavewould only go and set the height & width properties what they already are in storage. ie. something like the following are compared:vs
Those are not considered equal, even though the height & width in the first array will get set to '456' and '789' by
preSave. I hope that makes sense?So yes the new patch would enable resolving this, because the
ImageItemclass could implement the new method to check for equality by ignoring the empty dimension properties. So I'm saying that I'm in favour of your patch, and just that we have a potential test case in core :-)Comment #49
berdir@plach and I discussed how to move forward with this exactly.
ImageItem is in fact a good example of the problem that I described. That said, this isn't a problem we can solve generically I think, best we can do is change existing field type plugins to e.g. recalculate those values on setValue() and not preSave(), so they're always in sync.
However, that is a pre-existing problem with the equals() implementation, how it is used and when it is called. While the issue title is quite generic, the goal/implementation is actually specifically to allow field types to implement and additional, new method to react differently to storage-level equals() checks compared to checks that are about whether it has (translation) affecting changes.
Considering that, and that this is something that can not get into a stable minor release and is basically ready, we think it makes more sense to create a new issue for making sure that all field types need to be adjusted for equals to work as expected here. That is also something that should be possible in a patch release I think.
In that light, I'm also not quite sure about the issue title change.
While @hchonov and I still disagree about some details of the patch and whether they are necessary here, I think this is good enough as a first step and solve both his problem as well as mine. If there is ever a problem with that static cache, we can change it internally for example.
Comment #50
plachThanks @Berdir, RTBC'ing per #49.
Also removing the needs CR tag, since we have https://www.drupal.org/node/2975280 now.
Comment #51
james.williamsHooray, thanks everyone!! :-D
Comment #52
hchonov@Berdir++, at least we agree on the basic approach, which is something :).
Comment #53
wim leers🎉
Comment #54
catchThis could do with an explanation of why it has to be a static property, from the discussion here it appears to be only for performance?
Comment #57
catchI've opened #2988300: Should ContentEntityBase::fieldsToSkipFromTranslationChangesCheck be static? as a follow-up.
Committed/pushed to 8.7.x and cherry-picked to 8.6.x. Thanks!
Comment #58
berdirAwesome, thanks!
I also created #2988309: Ensure that all field types return TRUE on equals() for the same values to deal with the image field problem mentioned in #48 as well as the contrib field types I mentioned in #41.