Closed (fixed)
Project:
Drupal core
Version:
8.5.x-dev
Component:
entity system
Priority:
Major
Category:
Task
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
27 Nov 2017 at 14:03 UTC
Updated:
17 Jan 2018 at 03:19 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
amateescu commentedAnd a patch.
Comment #3
plachLooks good to me, I have only one doubt:
Shouldn't we use $this->getRevisionId() here? If the revision is marked as new, I'm not sure it's correct to consider it equal to the latest revision: the same (latest) revision may be marked as new in two concurrent requests and none of the two would be correct in considering itself latest until they are stored, go ask Schrödinger ;)
Comment #4
plachComment #6
amateescu commented@plach, we can only know the state of the entity during the lifetime of our current request, so if the entity is the latest revision at the beginning of the request, it will stay like that until the end of it, unless we save a new revision for that entity and at that point the "latest revision" assumption will be re-calculated if some other code calls
isLatestRevision()after we saved it.I've now coded that assumption in the test and also fixed the fails from #2.
Comment #7
amateescu commentedThis might seem like splitting hairs, but I opened yet another issue to discuss the introduction of the new
RevisionableEntityStorageInterfaceinterface: #2926540: Split revision-handling methods to a separate entity storage interfaceComment #8
plachSpoke about this with @amateescu in IRC: we agreed to add also the
::getLatestAffectedRevisionId()from #2924724: Add an API to create a new revision correctly handling multilingual pending revisions here.Comment #9
sam152 commentedComment #10
sam152 commentedDespite the name of the issue, this is very similar to what the latest patches in #2784201: Track the latest_revision and latest_revision_translation_affected ID in the entity data table for revisionable entity types are doing. I'd be happy to close that one in favour of this.
Comment #11
plach@Sam152:
Wait, aren't we going to denormalize the values returned by the methods introduced here into entity fields over there?
Comment #12
plachThis is blocking a string of critical and major tasks.
Comment #13
amateescu commentedHere's the new
getLatestAffectedRevisionId()method, as discussed in #8. Since this now depends on #2926540: Split revision-handling methods to a separate entity storage interface I'm also posting a combined patch.@Sam152, like #11, I also thought of this issue as the split-up mentioned in #2784201-118: Track the latest_revision and latest_revision_translation_affected ID in the entity data table for revisionable entity types, so we can let that issue deal with the denormalization part.
Comment #14
plachWe're missing test coverage for the new method :)
Comment #15
plachThis makes sense only for revisionable + translatable entity types, so IMO it should live in
ContentEntityStorageInterface.Comment #16
sam152 commentedRe: #13 okay that sounds good :)
Comment #17
plachPer #14.
Comment #18
amateescu commentedI'll be afk for a few days, so feel free to work on it if you have time :)
Comment #19
plachOk, working a bit on test coverage.
Comment #20
plachThis addresses #15 and completes the implementation of the latest affected revision stuff.
Comment #22
sam152 commentedThis sounded very familiar, it actually fixes the problem described in #2859042: Impossible to update an entity revision if the field value you are updating matches the default revision.. I ran the test in that issue against this patch and it passed.
I assume we've already got coverage in this issue for that as well, but means we can close that as outdated. The perf impact of loading the revision was mentioned in that issue, possibly worth checking here too?
Comment #23
plach#2924724: Add an API to create a new revision correctly handling multilingual pending revisions is no longer critical.
Comment #24
plachThanks for the pointer @Sam152, I split out the fix for
$entity->originalso we don't need to depend on it. Posted fixes for that at #2859042: Impossible to update an entity revision if the field value you are updating matches the default revision. and #2833049: ContentEntityBase::hasTranslationChanges will compare a forward revision with the default one instead of the newest forward revision.Comment #25
timmillwoodOnly query was regarding the interfaces the methods ended up in, @plach confirmed via IRC all methods combining multilingual + revision support are on ContentEntityInterface.
So looks good to me.
Comment #26
xjmStill reviewing all the entangled bits of this -- meanwhile, just a small thing:
So this is definitely internal (both marked as such and named with the reserved underscore prefix) but it would probably be harmless to deprecate it and make it a simple wrapper? As per https://www.drupal.org/core/deprecation#internal.
Comment #27
xjmOh, we also still need to update the CR from #2926540: Split revision-handling methods to a separate entity storage interface.
Comment #28
xjmWhy does the method return
NULLif the entity is not revisionable, but just the current revision if it's not translatable? Isn't just the entity itself the latest revision if it's not revisionable?Comment #29
xjmI think we should also be updating the entity system documentation for all of this (in
entity.api.phpwith maybe a link fromlanguage.api.php). It seems almost like there should be a new topic on entity revisioning (including translations) that would be added to the top-level docs on https://api.drupal.org/api/drupal.Open to discussing what issue should have this for its docs gate but I sort of feel like we missed the docs gate for this on some earlier issues.
Comment #30
plachGood points, thanks!
Well, a non revisionable entity type has no concept of revision, so returning any valid value may lead to code that works in contexts assuming to deal with an entity revision, which in turn may lead to data loss, as entity data may be unintendedly overridden. Returning NULL would break the API consumer code, thus forcing developers to take also this scenario into account. A stronger possibility would be to throw a
\LogicException, however we don't do that in most cases (e.g. revision-related methods on config entity types), so that approach would be less consistent.OTOH a revisionable and non-translatable entity does have the concept of revision, and the latest affected revision is always the the latest revision, so I think in this case there is no harm in not breaking API consumer code dealing with non-translatable entity types.
That said, I'm fine with retuning NULL, if you think consistency should prevail over DX :)
That's a very good point: I'm not sure this is the right issue to do that, but I can totally see a follow-up adding that documentation being a release blocker, especially given how much we are going to augment and refine the API in 8.5.0.
Comment #31
amateescu commentedUpdate the CR (https://www.drupal.org/node/2927226/revisions/view/10739784/10743711) and changed
_quickedit_entity_is_latest_revision()as requested in #26.I fully agree with @plach answer to #28, so I think the current patch is doing the right thing :)
Comment #32
xjmYeah hm, I would probably have preferred throwing exceptions over null return if you're trying to interact with the revisioning of non-revisionable things, but if the NULL is what we already do elsewhere then it'd be a BC break to change it to exceptions. Followup discussion maybe?
This made me notice something else:
Does this actually belong on
ContentEntityStorageInterfacerather than one of the revision interfaces?Well, the thing is, there are no release blockers -- releases need to go out on schedule no matter what, which is why we have to keep HEAD in a shippable state and why we have the core gates and such.
I'd suggest that we start by adding at least a stub topic here with @todo to fill in most of it, but a very minimal of this that we're adding in this issue. It gets much harder to do that after the fact.
Comment #33
timmillwoodThis is something I questioned with @plach, and he said:
Which makes sense to me.
Comment #34
xjmI wonder, should we have
RevisionableTranslationInterfaceor such?Comment #35
xjmOkay, so there is something in
entity.api.phpalready that we should be extending as we fix these. Grep forentities_revisions_translationsand you'll find this:What's there needs some work (the analogy to spreadsheets is a bit weird) and we should file a followup for that, but this patch should at least add a few links and definitions. Like:
I know the "latest affected" concept exists internally with the
revision_translation_affectedkey, but let's document it explicitly since we're now making it part of the public API. (And since it's confusing.)Comment #36
plachFair enough, I'll work on the documentation updates and follow-ups tomorrow morning if none beats me to it.
@xjm:
It would certainly be consistent with what we have been doing so far. However I'd call it
TranslatableRevisionableInterfaceas we are dealing with revision translations, not translation revisions ;)OTOH if we add that,
ContentEntityInterfacewould become just aFieldableEntityInterfaceextendingTranslatableRevisionableInterface. Probably worth doing for consistency, in which case we would need to similarly addTranslatableRevisionableStorageInterface.I guess this would be another follow-up, right? Seems out of scope here.
Comment #37
gábor hojtsySomething that did not seem to came up above is BC around adding methods to the existing interfaces. I checked and it seems like we only add methods to interfaces that are neither @api nor @internal. As per https://www.drupal.org/core/d8-bc-policy this is applicable then:
Comment #38
plachOn this
Comment #39
plachDiscussed this with @xjm and we are going to postpone this to the interface clean-up + docs updates. Here's the issue for that: #2929496: Add dedicated interfaces to group methods dealing with revision translation and clean up the related documentation.
Comment #40
plachComment #41
plachAdded the blocker issue to the IS.
Comment #42
plachAfter working on #2929496: Add dedicated interfaces to group methods dealing with revision translation and clean up the related documentation, I think we should rename
::getLatestAffectedRevision()to::getLatestAffectingRevision(), since it's translations that are affected by revisions. Thoughts?Comment #43
plachRerolled after #2929496: Add dedicated interfaces to group methods dealing with revision translation and clean up the related documentation went in and implemented #42.
Comment #45
amateescu commentedThe name of this method sounds a bit.. weird :) Without the knowledge that this is about the 'revision_translation_affected' flag, it doesn't really mean anything on its own.
How about
isLatestTranslationAffectingRevision(), or some other variation of that? I feel that putting 'Translation' in there is important :)Comment #46
plachI hope this looks better now.
Comment #47
plachComment #48
amateescu commentedThanks @plach, the method name looks way more clear now :)
The concerns that were raised in #35 have been resolved in #2929496: Add dedicated interfaces to group methods dealing with revision translation and clean up the related documentation, so let's get back to RTBC.
Comment #49
effulgentsia commentedThis doesn't look right.
Comment #50
effulgentsia commentedAlso, #26 asked for
_quickedit_entity_is_latest_revision()to be left in as a deprecated wrapper, and #31 implemented that, but looks like that's been accidentally dropped from following patches?Comment #51
effulgentsia commented#35 also suggested adding docs explaining
'revision_translation_affected'and/or the new related methods introduced by this patch. It suggested adding some examples, which I don't see either in HEAD or in this patch. To be honest, I'm still confused by the concept. At least when I use core's UIs, even with Content Moderation enabled, my'node_field_revision'table shows'revision_translation_affected'as always 1. What are the conditions in which it can be 0? Does the test that's in the patch cover any case in which it's 0 or is$en_revision->isLatestRevision()false simply by not having an'en'record saved at all for thevidcreated by$it_revision->save()?Comment #52
effulgentsia commentedSpecifically, what I'm asking in #51 is:
This filters out:
$langcode$langcodebut whose'revision_translation_affected'is 0What are the conditions that result in each one? I think we maybe need docs explaining the difference in meaning between the two possibilities and test coverage for each one?
Comment #53
plachThis should address the feedback above. I had to get a bit "creative" to provide coverage for #49, since that's just an optimization, so there's no real behavior change. Hopefully that's ok.
Even without using CM, you need to enable content translation on
article(remember to add a new language, e.g. IT). Once you created an article, add an IT translation while creating a new revision. At this point you should get revision 1 with EN affected and revision 2 with IT affected.Unless the value is set explicitly, it can't be 0: it's either NULL or 1. Anyway what matters is the 1 value: 0 or NULL both mean "not affected". A missing record can only happen when querying for a language not having a corresponding translation.
Your example (assuming you meant$en_revision->isLatestTranslationAffectingRevision()) could happen only if$it_revisionwere the default language and$en_revisiona translation.Edit: now I get your question.
$en_revisionis not the latest revision simply because a newer revision was created after it (it's revision 2, while$it_revisionis revision 3). However it's the latest revision having changes to the English translation, i.e. affecting the English translation.All relevant cases should be covered by
EntityRevisionsTest::testIsLatestAffectedRevisionTranslation().Comment #54
effulgentsia commentedThanks! #53 answers all of my questions and concerns.
Here's an attempt at clarifying the docs a bit more. What do you think?
Comment #55
effulgentsia commentedRe-reading all this, I think we should rename all the methods from
"TranslationAffecting"to"TranslationAffected". The suggestion in #42 was sensible up until #45 improved it. With the word "translation" back in the names, then since per #42, "it's translations that are affected by revisions", let's name accordingly. I think it's important, because "TranslationAffectingRevision" can read as the translation affecting the revision, rather than being affected by it.Comment #56
plachThis should address #55 and slightly adjust the new docs added in #54. In particular, I tried to make the wording more consistent with the existing docs around revision translation. I also tried to clarify that we are never saving a single translation, but the entity (revision) as a whole.
Comment #57
plachtypo
Comment #58
amateescu commentedI'm not sure why the return type of these method was changed in #53, the interface says that they return integers but now we're returning strings?
We should change the
(string)cast to(int):)Comment #59
plachWe had this inconsistency between the new methods and what is returned by
::getLoadedRevisionId()(string), that would make the optimization in the param converter fail due to the strict equality check. I thought it was better to be consistent and align with what currently exists. Otherwise we could relax the check in the param converter and use != or explicitly cast values there.Comment #60
amateescu commentedHmm, then we should also change
getLoadedRevisionId()to return (int) IMO. As for the param converter, I would cast the values there.Comment #61
plachDone
Comment #62
amateescu commentedNice, that looks much better now! :)
Comment #63
effulgentsia commentedUgh, sorry I didn't catch this in earlier reviews, but why this inconsistency?:
Should the latter be
int|string, or is there a reason why translatability requires integer entity IDs?Comment #64
jibranWhy can't a revision ID be string?
Comment #65
effulgentsia commentedHm, indeed. In HEAD,
EntityStorageInterface::loadRevision()says it can beint|string. But that's now deprecated, andRevisionableStorageInterface::loadRevision()says justint. I don't see via a quick scan of #2926540: Split revision-handling methods to a separate entity storage interface where that change was discussed.Comment #66
plachThis should address the reviews above. This is a very inconsistent area, but I agree that we should not limit an implementation detail like the revision ID type in interfaces, even if core uses only integers.
However we should at least achieve docs consistency, opened the following issue for that: #2933570: Clean up PHP doc @param and @return types for entity revision IDs.
Comment #67
amateescu commentedOk, #66 addresses #63, #64 and #65 so let's get this in and figure out whether we want to restrict revision IDs to integer or not in the clean up issue mentioned above.
Comment #68
larowlanThat is an api break. I have client code in D8 that has string revision IDs. If its something we want to do, I think it is D9 territory.
#2926483-13: Add API methods for determining whether an entity object is the latest (translation-affecting) revision mentions that it added additional methods that don't appear to be listed in the issue summary, so tagging for an IS update.
Comment #69
plachGood catch! Updated the IS, however the CR looked already up to date to me.
Comment #70
sam152 commentedI did one last full scan over the latest patch. The following are purely observations and don't block commit.
Is KeyValueContentEntityStorage being used in the wild to store entities in more "primitive" storage back-ends? I'm wondering if a follow-up could attempt to provide a (probably) very slow implementation of these methods.
Alternatively it could just be setting a bad expectation that any entity stored in a key/value store supports more advanced features, like working pending revisios.
uber nit, double space in the middle of the comment.
Yay for getting rid of these! :D
An interesting follow-up to this might be a test storage handler implementation which collects some stats about certain calls, for testing exactly this kind of thing.
In the long run, this could be somewhat fragile.
Comment #71
sam152 commentedLogged #2933930: Provide a test entity storage class that logs the number of times entities are loaded from storage. as a follow-up for that last point.
Comment #72
effulgentsia commentedThank you for everyone's reviews! Ticking credit boxes for reviewers.
Comment #74
effulgentsia commentedPushed to 8.5.x!
Comment #75
sam152 commentedI filed #2934026: Deprecate isLatestRevision, getLatestRevision, getLatestRevisionId in the ModerationInformation service as a follow up to this as well.