Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
After calling $entity->setNewRevision(TRUE)
we have no way of knowing what revision id the entity used to be.
Considered a blocker for #2640496: Revision ID in {node} and {node_revision} can get out of sync which is critical.
Proposed resolution
Set the revision id to an originalRevisionId
property in __construct
Update the originalRevisionId
on post save
Return the property with getOriginalRevisionId()
method
Remaining tasks
Review
User interface changes
none
API changes
Two methods added to ContentEntityBase and ContentEntityInterface
Data model changes
None
(Blocking #2809123: Reverting a revision doesn't keep moderation state)
Comment | File | Size | Author |
---|---|---|---|
#140 | 2809227-140.patch | 13.24 KB | timmillwood |
#140 | interdiff.txt | 1.72 KB | timmillwood |
Comments
Comment #2
timmillwoodI think this resolves the issue, need to write a test to prove it.
Comment #4
timmillwoodHere's a test for this.
Comment #6
timmillwoodFixed the broken test in #4.
Comment #7
catchIf you load an arbitrary revision, then save it as the default revision (for example a revert), then it's correct to load the current default revision in $entity->original.
$entity->original exists so it's possible to compare the before/after state - this needs to be between what was published vs. what is about to be published. For example something that reacts to a change in $entity->status.
Comment #8
timmillwoodI'm not sure I agree.
If you load an arbitrary revision, then save it as the default revision (for example a revert), the revision you loaded should be in $entity->original. This is a must when things are referencing specific revisions such as ContentModerationState in Content Moderation module.
In Content Moderation module if we have revisions 1,2,3, where 3 is currently the default revision, when reverting to 2 we need to know in the hook_entity_update to use the same moderation state for the newly created revision as we did for revision 2.
Comment #9
lomasr CreditAttribution: lomasr at gai Technologies Pvt Ltd for gai Technologies Pvt Ltd commentedComment #10
timmillwoodAfter speaking to @catch I think this boils down to the backwards compatibility of this bug.
Yes, it may be a bug, but some people might depend on the way it currently works. So do we fix it, or keep it how it is then add
$entity->previous
or something to load the actual original revision.Comment #11
timmillwoodHere's a patch to use $entity->previous instead of $entity->original.
Comment #14
timmillwoodLets give this another shot.
Comment #16
timmillwoodOnly loading $entity->previous if is set.
Comment #18
timmillwoodBroke my own test, that's not very productive.
Fixed!
Comment #19
timmillwoodComment #20
timmillwoodSo the patches in #6 and #18 do pretty much the same thing. The first uses
$entity->original
and the second uses a new property$entity->previous
Please review the code, and the options between the two options.
Comment #21
BerdirCan we maybe add previous as a method so we have a way of documenting it? Doing that for origin would be good as well but probably not in this issue.
We tried to remove it (origin) and have a separate $original argument passed to hooks but that issue never got in, which is one reason (more like an bad excuse) why it is not documented at all right now.
Comment #22
timmillwoodok, I will get to work introducing getOriginalRevisionId() and loadUnchangedRevision() methods to ContentEntityBase.
Comment #23
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedShouldn't they be on
\Drupal\Core\Entity\RevisionableInterface
?Comment #24
timmillwoodImplemented getOriginalRevisionId() and loadUnchangedRevision(), added them to RevisionableInterface.
Comment #25
timmillwoodUpdating the summary and title to make more sense based on the direction of the latest patch.
Comment #26
timmillwoodoriginal_revision
tooriginalRevisionId
originalRevisionId
property protectedgetOriginalRevisionId()
to return the current revision id if no original one is setloadUnchangedRevision()
from returning anything if no revision id is set.Comment #27
hchonovI would rather set this property in the constructor instead when the method setNewRevision is called.
This could be turned into a single line :
$revision_id = $this->originalRevisionId ? : $this->getRevisionId();
This currently does not have a default return value (null) if the original revision id is not present.
You also have to ensure that $originalRevisionId is updated with the new revision id in the postSave method.
Comment #28
hchonovAs we are caching only the default revision, the cache should be reseted only if you want to load the unchanged currently default revision.
Comment #29
hchonovOh and in createDuplicate you have to unset the $originalRevisionId as well :)
Comment #30
hchonovYou should not alter the RevisionableInterface as this is an API change, instead you could provide a new interface and make ContentEntityBase implement this new interface and after this issue is committed you could open a new one to merge this new interface into the RevisionableInterface.
Comment #31
hchonovOh sorry, I've forgot to mention that I have already this logic implemented in #2620980: ContentEntityStorageBase::loadRevision() should use the static and the persistent entity cache like ContentEntityStorageBase::load().
You could take the most of it from there :).
Comment #32
timmillwoodpostponing in favor of #2620980: ContentEntityStorageBase::loadRevision() should use the static and the persistent entity cache like ContentEntityStorageBase::load() which does most of what's needed.
Comment #33
timmillwoodReviving this to implement
getOriginalRevisionId()
.Comment #34
timmillwoodHere's an updated patch taking influence from #2620980: ContentEntityStorageBase::loadRevision() should use the static and the persistent entity cache like ContentEntityStorageBase::load().
Comment #36
timmillwoodoops
Comment #38
timmillwoodFixing failing tests.
Comment #40
timmillwoodRe-rolling for 8.2.x
Comment #41
BerdirIf we add a new interface, would it make sense to test for that here?
wondering if that's too much internal implementation details? Might be enough to just state that it will return the original revision ID the entity was loaded with even if it was changed (in any way).
missing return type.
should => must, I think?
Which is another nice example that shows how insanely hard BC is. So we have a new optional interface, everything is backwards compatible, yay! Except... if an existing storage won't call this method, it wont' work anymore?
We need a change record for this anyway I think, that should then specfically highlight that the storage must call this and when in the process exactly.
Comment #42
timmillwoodMade changes based on comments in #41.
Comment #43
timmillwoodAdded change record https://www.drupal.org/node/2825707
Comment #44
timmillwoodUpdated the test to be a help explain how the original revision id should work.
Comment #45
Berdirmissing docblock.
... which *does not* have a revision a revision ID yet, I think?
assertIdentical(NULL, $loaded->getRevisionId()) might work too, not sure what's better?
this should reference the actual function name, with (), there is no hook_update() ;)
same re hook_update
I think this comment could be clarified a bit. The comment just repeates the code but doesn't tell me why. The why is that the save above, unlike the first, updates the existing revision.
Also, I think I just found that this issue is a blocker for #2640496: Revision ID in {node} and {node_revision} can get out of sync, which is critical. The check/logic there doesn't work when updating a non-default revision as we're comparing with the default revision. Will explain more there, but this likely means that this issue is critical too.
Comment #46
Berdir@catch confirmed in IRC that this makes this issue critical. We should be close.
Comment #47
timmillwoodAddressed items in #45.
Comment #48
Berdirnot sure about rules for first line being a single line in docblocks for tests.. I usually try to word it so it is <80 characters but not sure about what the standards say exactly.
But I'd say better change it than having it set back to NW because of that.
the second reference is just the module. And also still missing (), which I think they should have so it is linked and clickable.
Comment #49
timmillwoodAddressing items in #48
Comment #50
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedid -> ID.
identifier ;)
We should wrap this in a
isRevisionable()
check.Why can't we do this in
ContentEntityBase::postSave()
? It would also make the new method unnecessary I think..Comment #51
timmillwoodAddressed items in #50 which included a big change based on #50.4.
Moving to
ContentEntityBase::postSave()
didn't really work because it was fired too early, but moving tosetNewRevision()
works well because it is called at the end of doPostSave().This means we can remove
resetOriginalRevisionId()
. Then the new interface for one method seems a bit odd, so movedgetOriginalRevisionId()
back into ContentEntityInterface.Comment #52
BerdirI'm not sure about this.
->setNewRevision(TRUE)
->setNewRevision(FALSE)
And then the revision ID is gone :)
maybe only do that assignment if we actually have a revision to assign?
Also, it at least needs a comment IMHO
Comment #53
hchonovUnfortunately I did not had the time to take a look at the changes made since the patch was cut off from #2620980: ContentEntityStorageBase::loadRevision() should use the static and the persistent entity cache like ContentEntityStorageBase::load(), but I hope that they are compatible. I will try to review it until tomorrow.
Comment #55
timmillwoodFixing #51 and #52.
#53: I think it'll be ok.
Comment #56
BerdirI'm not entirely sure what I prefer, the code now in setNewRevision() or the explicit method from before. This means we don't have to worry about other storages not calling the new methods, that's certainly a plus.
I think is ready, has had lots of reviews and nitpicks. Lets unblock the issues that are blocked on this :)
Comment #57
timmillwood@Berdir - Yesterday @amateescu and I discussed in length about where to reset the original revision ID. Specifically to negate the use of resetOriginalRevisionId(). First I tried postSave() as @amateescu suggested in #50, however this fired too early, so when hook_update was called the original revision ID had already been reset. Putting it in setNewRevision works well because the original revision id is set right before it's nulled. Also setNewRevision() is called (with FALSE) in \Drupal\Core\Entity\ContentEntityStorageBase::doPostSave which means the original revision ID is reset as one of the last things in the process, this means the test is unchanged between #49 and #50.
The only concern here is if setNewRevision(FALSE) is ever removed from \Drupal\Core\Entity\ContentEntityStorageBase::doPostSave, or if other storage doesn't call it. However generally this will still produce the desired results because the original revision ID would still get set when setting a new revision.
Comment #58
hchonovI somehow do not like relying on ContentEntityStorage::doPostSave calling setNewRevision(FALSE) to update the original revision id. This is something I would do explicitly during the post save.
Comment #59
timmillwoodI was wondering do we need to set the original revision ID in postSave() at all, because thst would set it to the new revision id, and therefore it would not be the original anymore.
Comment #60
hchonovContentEntityBase::createDuplicate has been forgotten - there the original revision id has to be unset and we need a test for this.
And yes I would say we have to update the original revision id explicitly in postSave as from now on we are working with an updated entity object.
Comment #61
timmillwoodAdding createDuplicate() update and test.
Comment #62
hchonovI still think we should update the original revision id explicitly in postSave instead as a part of setNewRevision, actually when we set the original revision id in the constructor and then in the postSave there is no need of keeping this logic in setNewRevision at all.
Comment #63
timmillwoodAfter talking to @amateescu I've moved the "reset" of the original revision id property to the ContentEntityBase::postSave method. This is now reset too early for hook_update which was being used as part of the testing, so this section of the test was removed, but all other elements in the test are returning their original result.
Comment #65
timmillwoodNeed to remember to switch to 8.2.x before rolling this patch.
Comment #66
Berdir->original is available in hook_entity_update(), I'm not convinced that not making this available is a good idea.
I can absolutely see use cases for this, for example when you need the original *and* the new revision ID, then this would be the only place to get that.
If we do, we should at least convert it to use presave instaed of update and not just delete that test coverage?
Comment #68
timmillwoodMoved the test to use entity_test_entity_presave().
Comment #69
timmillwoodAdded resetOriginalRevisionId() back, and making use of it in all the places including doPostSave().
Comment #70
hchonovSorry for doing this again but we have to break the reference of the variable when cloning the entity. See #2772979: Enforcing a cloned entity translation to be new propagates to the original entity and #2828073: Cloning an entity with initialized translations leaves $entityKeys and $translatableEntityKeys pointing to the old entity object as an example. A test is needed for this as well.
Comment #71
hchonovAnd we also have to make a reference in ContentEntityBase::initializeTranslation.
Comment #72
xjmComment #73
xjm@effulgentsia, @catch, @cilefen, @alexpott, and I discussed this issue today and agreed that it was critical as a blocker for #2640496: Revision ID in {node} and {node_revision} can get out of sync, which is already triaged as critical. (If it were not a hard blocker, this issue would just be major, but we decided to follow @Berdir's recommendation in #2640496-31: Revision ID in {node} and {node_revision} can get out of sync.
That said, it would be good to put the information for how this is a hard blocker in the summary of this issue.
Thanks all!
Comment #74
timmillwoodChanged from #70 and #71.
Comment #75
timmillwoodComment #78
BerdirI'm not sure about this. the revision ID is not translatable, why should we unset it here?
Imagine this:
$node = Node::load(1);
$translation = $node->getTranslation('de');
$transation->save();
That *needs* to have access to the original revision ID as well, just as if you'd call $node->save();
Comment #79
hchonovThe translation objects have to share all the $originalRevisionId property. In __clone we have to break the reference and make a copy of the value. In initializeTranslation we have to create the reference back again. That are different use cases and both need rest coverage.
Comment #80
timmillwoodI think I agree with @Berdir.
Updated the test to enforce this. When creating a new translation, the original revision ID should be the same as it was before.
Comment #81
BerdirCan we also verify that if you call $french->save() , the getOriginalId() is then the correct new ID for both $loaded and $french? That might not actually work yet and will need an explicit reference assignment like we do there for other properties.
Comment #83
timmillwoodComment #84
hchonovHmm now the clone will not have the $originalRevisionId set?
I think that #79 is what we need.
Comment #85
timmillwoodNot sure what you mean @hchonov.
Comment #86
hchonov$entity->originalRevisionId == (clone $entity)->originalRevisionId will now evaluate to FALSE, what you have in your test as well. But this is not correct.
Comment #87
timmillwoodUpdating what __clone does. Hope this is what @hchonov was thinking.
Comment #88
hchonovThank you, this is what I was referring to.
Here I would get the property directly by $this->originalRevisionId instead of using the getter function for it.
After setting a new revision it would be nice to check not only that the revision id of the french translation has been reset but also of the default one. I would also backup first the revision id and after setNewRevision check that getOriginalRevisionId is returning the correct value.
After saving the entity with a new revision it would be nice to check also that the original revision id of the default translation has been updated.
Comment #89
timmillwoodUpdating based on #88.
Comment #90
hchonovI think this is ready now.
Comment #91
catchWhy not setOriginalRevisionId() or because it's not a setter as such populateOriginalRevisionId()? Reset suggests it's been set already and is being reset.
Otherwise just nits:
which does
ID
ID again.
as the?
Comment #92
hchonovBut the original revision id has been previously set, here we just update it to the new value. "Populate" however means it is not set and we are setting it for the first time.
We have the method EntityInterface::setOriginalId which is used exactly the same way in the EntityStorageBase::doPostSave as we have to update the original revision id in ContentEntityStorageBase::doPostSave so probably we could offer a similar method ContentEntityInterface::setOriginalRevisionId?
Comment #93
catchsetOriginalRevisionId() seems fine yeah.
Comment #94
timmillwoodaddressing #91.
Comment #96
timmillwoodThis is why I shouldn't write patches while in a meeting.
Comment #97
timmillwoodComment #98
hchonovHmm since it is a setter now it has to have a parameter for which to set the original revision id, right?
Comment #99
timmillwoodI don't think we should allow the original revision id to be set to anything, it should only ever be allowed to be set to the current revision id.
Comment #100
hchonovThen probably it would be better if the name of the method is updateOriginalRevisionId instead of setOriginalRevisionId, which implicates that there has to be a parameter as this is a setter?
Comment #101
timmillwoodyes, update could would. @catch, what do you think?
Comment #103
timmillwoodComment #104
timmillwoodAfter talking to @catch on IRC switching
setOriginalRevisionId()
toupdateOriginalRevisionId()
.Comment #105
catchupdate is OK with me, I misunderstood hchonov's comment in #92 to think they were /suggesting/ set*.
However this brings up another question. While we can't make this protected because the storage handler needs it, we really, really don't want people to call this method - should we add a note that it should be left alone? Otherwise looks RTBC for me.
Comment #106
hchonovHmm what just catch's mentioned makes me thinking that the current implementation of updateOriginalRevisionId is dangerous -
A comment is not something that might help not making such a mistake and having custom and contrib all together we do not know what could happen at some place. So I would say that if we go with updateOriginalRevisionId it should be retrieving the original revision id in a way not loosing it e.g. from $entity->values and ensure it is set or we go with the more stable solution setOriginalRevisionId which has a required parameter and if it is not set throw an exception. However I would like seeing the code example as a test not breaking the original revision id. This is something that we can solve.
Comment #107
timmillwoodFeels like we're going around in circles but it'd be better if we needed need the
updateOriginalRevisionId()
method at all. The only way to do this would be to update the original revision id only in ContentEntityBase and not in the storage handler, but as we discovered there's no nice solution for this either.Comment #108
timmillwoodAdded a short comment to show what might happen if updateOriginalRevisionId() is used, and also made it internal.
Feel free to change the wording of the comment on commit.
Comment #109
hchonovIf we go with this function then I would the assignment if ::getRevisionId returns something otherwise not
so this probably should be:
Comment #110
timmillwoodI guess that kinda makes sense.
Comment #111
hchonovCan we add a test ensuring that updateOriginalRevisionId is not breaking the original revision id in this use case:
$entity->setNewRevision();
$entity->updateOriginalRevisionId();
Comment #112
timmillwoodAdding a test to make sure calling
updateOriginalRevisionId()
doesn't screw things up.Comment #113
hchonovThis looks ready for me. Thanks.
Comment #114
jibranLet's update the issue summary so that we can remove the tag and committers can't knock it back to NW.
Comment #115
timmillwoodComment #116
alexpottSo I think this is unavoidable and it akin to the call to
setOriginalId()
in the parent. I've done quite a bit of thinking about whether we should put this functionality in setNewRevision when it is passed with a FALSE value but I don't think that that makes sense.Given that this analogous to the setOriginalId() I'm concerned by file_copy(), file_save_data() and file_save_upload() since they call setOriginalId() and do odd things but #2241865: Do not create a new file entity in order to overwrite an existing entity should solve the issue is files become revisionable - ponder about contrib though - ie. file_entity.
The discussion over the correct-ness of $entity->original containing the current default revision is interesting. I think the HEAD behaviour is completely unexpected. But as discussed this is an API change. One of the original directions of this issue was to add a ->previous. However maintaining both "original" and "previous" would be expensive. But in some ways adding a proper previous entity feels more correct than the current patch. Unfortunately the issue comments don't explain why this approach was dropped. That said at least having the original revision ID allows us to load the previous revision if required so this is definitely an improvement.
For the moment just writing some thoughts as they might spur other and better ideas. Leaving at rtbc because I have no definite plan of action.
Comment #117
catchWe discussed this a bit in irc in relation to #2833049: ContentEntityBase::hasTranslationChanges will compare a forward revision with the default one instead of the newest forward revision. The big issue is that the revision we should be comparing against depends on context.
1. If you load the default revision, and save it as a new default revision, you should compare the old default and the new default.
2. If you load a non-default revision and save it as a new default revision (reverting, or publishing a draft), you should compare the old default and the new default.
In both cases $entity->original as it is now works for this case.
When saving a non-default revision it gets more complex.
1. No existing forward revision exists, saving a new non-default revision based on the current default. In this case you should compare against the default (which is simultaneously the most recent and the one you loaded)
2. Load and edit an existing forward revision, then save it as a new forward revision with a new workflow state. In this case you should compare against the loaded revision (which is simultaneously the most recent, but not the default).
3. You're reverting a forward revision back to a previous one - i.e. loading revision 1, saving revision 3. In this case the comparison ought to be against revision 2 (which is neither the revision that was loaded nor the default, but it is the most recent)
What this points to is that any one time, there are up to three revisions with different relationships to the one we're about to save:
1. The current default revision
2. The 'source' revision - i.e. the version we loaded as the basis of the one we're saving
3. The revision we're 'replacing' - this could be the default revision but it could also be the revision under consideration in a workflow.
(1,2) (2,3) (1,3), (1,2,3) can always point to the same revision (which we assume in the case with $entity->original)), but they can also all three be different.
Moving back to CNR not because this invalidates the patch, but it'd be good to confirm we all agree on the above since this doesn't solve all of the problems with $entity->original as it is.
Also because berdir mentioned in irc he's run into problems saving a within hook_entity_insert()/update().
Comment #118
hchonovThis issue here is not intending of solving all the problems with $entity->original, for this @catch already created an another issue but this one would be needed there - #2833084: $entity->original doesn't adequately address intentions when saving a revision.
Also this one is needed for #2620980: ContentEntityStorageBase::loadRevision() should use the static and the persistent entity cache like ContentEntityStorageBase::load().
@berdir what kind of problems have you run into with this patch? Would you please provide some details in order for us to cover the use case?
Comment #119
BerdirRight. I'm using this API in #2640496: Revision ID in {node} and {node_revision} can get out of sync. What I'm doing it is prevent that you can re-save an existing revision but change the revision ID. This can for example happen when you run migrate updates that keep the revision ID while also manually updating/working on the target site.
So what I'm doing is, if no new revision is to be created, compare the current revision ID with getOriginalRevisionId(). And there are a few fails. One interesting problem is \Drupal\node\Tests\NodeSaveTest::testNodeSaveOnInsert().
What we're doing there is $node->save() in hook_node_insert(). That is sometimes required and a valid use case. But here is what is happening now:
1. Create a new node and save.
2. "new" revision, no existing revision ID, we're fine
3. hook_node_insert() is called
4. we're saving, no new revision, we now have a revision ID but getOriginalRevisionId() returns nothing, as we only reset it *after* the hooks. That means I now have a mismatch and I'm throwing an exception.
I think I can work around in that issue but maybe that's something that we should handle here. What should the expected behavior in that case be, though? Both during the second save but also in other insert hooks that are called *after* the one that re-saves. Note how this means that hook_node_update() can be called *before* hook_node_insert() for a module.
Comment #120
Berdir#2640496: Revision ID in {node} and {node_revision} can get out of sync now has a workaround, see latest interdiff, but we still might want to do something about that here, for example update the original revision ID before calling the insert hook for new entities. Not sure.
Comment #121
catchI really dislike recursive saving, but yes people do it. I guess what we do in that case depends on whether we consider the recursive save to be conceptually part of the same save, or a separate one? And that again might depend on context.
I'm wondering if we should call the method getLoadedRevisionId() so it's explicit that it's the revision ID that was loaded. This might make it easier when we try to name/document all the scenarios in the other issue.
Comment #122
BerdirI think we chose getOriginalRevisionId() for consistency with getOriginalId() but getLoadedRevisionId() would also work for me.
Comment #123
hchonovI am fine with the method name getLoadedRevisionId as well.
Comment #124
catchI would expect insert hooks to not look for $entity->original, so this seems potentially harmless. Let's try that here and see what it looks like. Marking CNW for that and the method rename.
Comment #125
timmillwoodRenaming from OriginalRevisionId to LoadedRevisionId.
Comment #127
timmillwoodForgot to rename file when renaming class.
Comment #128
BerdirWhat I suggested is something like this. Not 100% sure we need that complexity but I think it makes sense.,
Didn't test, so something might fail.
Comment #129
timmillwood@Berdir - That works for me, and it looks as though it still passes the test included in the patch.
I would RTBC if it was moral to do so, maybe @hchonov or @catch could?
Comment #130
hchonovI do not like that we make difference between saving a new entity and updating an existing entity, so I think we should make it consistent and decide whether we want that the original revision id is updated to the new revision id before or after the post save for both new and existing entities. However I think the post save hooks might still need an access to the previous revision id, so regarding them we have to update the original revision id after, what we actually still do for updates. New entities do not have an original revision id, so I guess we do not really need this logic and returning NULL is fine and returning something might even lead to misinterpreting the result. Take a look at the original ID, it is updated at the end after the post save, so if we do update the original revision id before the post save we would introduce an inconsistency between at which point we do update the original id and the original revision id. So if we do not have the original id during the post save hooks it does not make sense for me to have the original revision id either.
That are just my thoughts... If you are still not convinced then I guess that it would be great to hear what @catch thinks about this as well.
Comment #131
BerdirThe concept of original ID doesn't exist for content entities anyway, they don't support changing the ID and #2640496: Revision ID in {node} and {node_revision} can get out of sync will enforce that. We generalized that concept at some point, but it really only exists for config entities or at least not for content entities. And original revision ID on the other hand doesn't exist for config entities. So I think we don't have to make them consistent. Both ID's for content entities are usually auto-increment or something like that, while config entities have them set explicitly. They are very different anyway.
See #119 for why setting it for the insert hooks initially might make sense. If you call $entity->save() in hook_entity_insert() then you are now updating an entity without having a loaded revision ID. That could be unexpected for code that expects one in a hook_entity_update(). We could also set it when someone actually calls save() not sure. Keep in mind that such save calls can also lead to cases where other insert hooks are called later, and possibly even after corresponding update hooks for the same entity were called. (This is also just one of in total 4 possible situations.. you could also explicitly save as a new revision in hook_entity_insert() and you could save with or without a new revision in hook_entity_update(). All of those can cause a lot of confusion in other hooks.
But we do support this and have explicit test coverage at least for saving the same revision in hook_entity_insert() so we should try to make it as reliable/predictable as possible. I'm not 100% sure but I think my suggestion is the most predictable (update hooks always have a loaded revision id, insert hooks always have it updated already, does not matter if they are before or after some other hook that resaves)
Comment #132
hchonovBy saying consistent I've meant consistent when the original revision id is updated for both new and existing entities. About #119 - if getOriginalRevisionId returns NULL then you know there was no previous revision - and this is the correct way and to be expected in hook_entity_insert. I think and here you could use the already set revision ID through ::getRevisionId.
Re-saving an entity in hook_entity_insert is bad practice, but sure possible, and if you want to do this then I think you have to manually update the original revision ID.
Just thinking about this there are two hooks called after an entity is saved - hook_entity_insert or hook_entity_update, so if we do have the new entity revision ID set as original revision ID in one of those hooks this makes it inconsistent for me and confusing as well.
I would prefer this over updating it inconsistently in the storage based on if it is a new or an existing entity.
We would also need tests for this new use case.
Comment #133
timmillwoodHere's an 8.2.x and 8.3.x version of the patch which updates the loaded revision id in save() as discusses with @hchonov and @Berdir on IRC.
Comment #135
Berdirnever override save(), this is just a shortcut for $storage->save($entity). It needs to be \Drupal\Core\Entity\ContentEntityStorageBase::doPreSave().
Also, we should only do this if this is a) an update (entity is not new) and there is currently no loaded revision ID. And we should explicitly document that this is for cases like a save within hook_entity_insert() when we immediately save before having the "loaded" revision id assigned.
Comment #136
timmillwoodUpdating with suggestions from #135.
Comment #137
hchonovHere we have to reset the variable set into the test - just to ensure the test is working correctly.
$entity2, but $loadedRevisionId3... probably you could isolate each test case so that you can just use $entity and $loadedRevisionId.
Those are my last remarks. After this I would RTBC it.
Comment #138
timmillwoodIn this version of the patch I have split up the test into 4 tests:
- General test
- Save without creating a new revision, plus clone and duplicate
- Multilingual entity
- Hook_entity_insert
Comment #139
hchonovNot needed empty line.
Missing doc of the test method.
I would say "Tests the loaded revision ID for translatable entities".
I would say "Tests re-saving the entity in hook_entity_insert."
We need an empty line between the end of the last method and the closing bracket of the class.
Comment #140
timmillwoodFixing the nits.
Comment #141
hchonovThank you.
Comment #144
catchCommitted/pushed to 8.3.x, thanks!
Not really sure what to do here about 8.2.x given both the criticality of the issue and the interface change, so leaving RTBC against there a bit longer. Possibly we should continue with the other bugfixes then cherry-pick at once?
Comment #146
timmillwoodPutting back to RTBC
Comment #147
e0ipsoAdding this two methods to the interface may be breaking BC.
This patch may be assuming that the
ContentEntityInterface
can only be implemented via theContentEntityBase
.I could fix the Simple OAuth module because I got an early report, but others may not be as lucky.
#2839585: Bearer Token Authentication broken on 8.3.x HEAD
Comment #148
BerdirSee https://www.drupal.org/core/d8-bc-policy, this part:
So that's OK for 8.3.x. but I guess it means that this can't be added to 8.2.x. The argument against would be that it is necessary for a critical issue to be fixed.
Comment #149
catchYes exactly, we don't want people getting fatal errors on 8.2.x sites so this usually wouldn't go in. However data integrity issues are as bad/worse than fatal errors, so not ruling out an exception here. Either way we should postpone this on #2640496: Revision ID in {node} and {node_revision} can get out of sync being fixed in 8.3.x and revisit it then.
Comment #150
BerdirThat went in, lets see if this still apples against 8.2.
Comment #151
BerdirStill passing, so setting to RTBC so that someone can make that decision :)
Personally, I'm not sure. Based on #147, we'll break at least one module by adding those new methods, but would be broken anyway in 8.3.x. But people might not be aware that updating core might break their existing site, e.g. in case of a security update that isn't properly tested before deploying.
And while I agree with alex pott in #2640496: Revision ID in {node} and {node_revision} can get out of sync that an exception is better than corrupt data that also leads to exceptions later, this situation is different. We might break a bunch of sites that have no data integrity problems at all and never will.
I guess that means I'm leaning towards -1 and not +1 on committing this or not. Anyone thinks it is very important to get this into 8.2? Then speak up now :)
Comment #152
alexpottIf this is going introduce breaks then I don't think we should commit this to 8.2.x
Comment #153
catchYes I agree with Berdir's leaning towards -1 here. Also we haven't yet tracked down the code path that leads to this exception being thrown in the first place (although there are suspicions it might be #2748609: [meta] Preserving auto-increment IDs on migration is fragile. Given that I think we should leave this issue at 8.3.x and fixed.
Comment #156
timmillwood#2846830: Add changelog for Drupal 8.3.0
Comment #158
quietone CreditAttribution: quietone at PreviousNext commentedpublish change record