Problem/Motivation
When presenting an entity for interaction by a content author, many factors are considered:
- What is the entity type?
- What is the entity ID?
- What is the revision?
- What is the translation?
- Other things?
Some of these things are codified in places like \Drupal\Core\ParamConverter\EntityConverter
But that is not an API that is reusable.
Additionally, the current best practice for retrieving an entity is:
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
Yet this bypasses all access, revision checking, translation handling, etc.
Core provides a service for loading entities: \Drupal\Core\Entity\EntityRepositoryInterface
But it only provides other helper methods for edge cases, not the 80% case.
Proposed resolution
Expand EntityRepositoryInterface
to include methods for:
- retrieving the canonical entity variant matching the specified context: this is typically the variant that would be displayed on the entity's canonical route.
- retrieving the active entity variant matching the specified context: this is the variant that is suitable for editing and previewing.
In this issue the only contextual information actually used is the (content) language, however the plan is to implement a more complete revision negotiation logic allowing to attach contextual information to each revision and allowing to support multiple modules relying on pending revisions at the same time, without them having to code against each other. This follow-up work is tracked at #3023194: [PP-2] Add parallel revisioning support: there we will expand on the implementation provided here to implement the full solution.
The API introduced here will only take care of negotiating the fittest variant for the specified context, access control will need to be applied on top of that.
See #31, #35, #49, #53, #81 for details.
Remaining tasks
Find consensus on the method names: the debate is currently between keeping a terminology close to what we have been using so far in core (active variant vs canonical variant, where variant is the generic terminology we use for both revisions and translations) and adopting something closer to what is used in RFC 5829. See #80 and #82. In the former case we still are not sure whether a "variant" suffix in the method names is useful to improve DX or on the contrary makes things unnecessarily scary.
User interface changes
None
API changes
Only additions
Data model changes
None
Release notes snippet
The Entity system now provides an API for retrieving an entity variant that is safe for for editing and previewing in editorial workflows, depending on the specified context, by default the current context.
Comment | File | Size | Author |
---|---|---|---|
#149 | entity-editable_api-2942907-147.patch | 71.46 KB | plach |
#149 | entity-editable_api-2942907-147.interdiff.txt | 2.73 KB | plach |
Comments
Comment #2
EclipseGc CreditAttribution: EclipseGc at Acquia commentedSo, I've been thinking about this a bit and how specifically loading revisions can be super nuanced and potentially even vary under different circumstances. To that end I wrote up a little event subscriber based approach to hopefully help inspire a more wholistic solution.
Features of the approach:
Things I might should have done too:
It seems like we could pass an optional string of some sort that denotes the situation in which it's running (kind of like the context parameter for translatable strings). That would mean that layout_builder solutions could be handled differently (if that's desirable) than quickedit. I wasn't sure if that was a step too far, but it'd be a simple, optional addition if we wanted to do it.
I showed this to Tim before posting it, and he mentioned how it's "needlessly bound to the DB, instead of entityquery" which I fully acknowledge. This was just a POC to try to help bootstrap this conversation.
Eclipse
Comment #3
hchonovWhich entity revision has to be loaded depends on the use case and there is no general answer. Couple of use cases:
- the entity has only default revisions - safe to edit revision is the current default revision
- the entity has default and forward revisions and the module providing the forward revisions allows or disallows the concurrent editing of the default and the forward revisions - a safe to edit revision depends on whether or not the concurrent editing of default and forward revisions is allowed - if not, then the safe to edit revision is the latest forward revision, but if the concurrent editing is allowed then it depends on the context and the changes to be made which revision should be edited - the default or the latest forward revision.
-- even with forward revisions there are more use cases that have to be considered:
--- the forward revisions are per user - the revision to edit depends on the current user
--- the forward revisions are shared among all users - the revision to edit does not depend on the current user
--- a project might introduce additional contexts next to the user based on which a different revision has to be loaded
Comment #4
tim.plunkettYes, I understand there are MANY things to consider when making that determination.
But as I understand it, none of those cases are decisions that the *consumer* of this API is responsible for.
Comment #5
plachA (very long) call was held today among some interested folks, the full notes are available here:
https://docs.google.com/document/d/1ku6y-eYkUQXGlstVDoFdWOyHhpYQ6FA35rdC...
A very short summary is: yes, we badly want this, and we need it in 8.6. It's too late in the 8.5 life-cycle to design an API properly.
The end goal is having something exposing a couple of methods allowing API consumers to retrieve the right entity object (correct revision/translations already instantiated) for edit and display without having to worry about the gory details. It would be an object you can safely save or render (respectively) in the current context. A good candidate to host these methods is
EntityRepositoryInterface
.It seems clear the we need to come up with a strategy allowing multiple implementers to collectively partecipate in the negotiation process, however hopefully this won't mean they will have to explicitly know each other. Some interesting ideas were mentioned around this subject, but we agreed to have the discussion around the details here.
Comment #6
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedWhile the internal details of how to determine the currently active entity revision which is safe for editing are being worked out, we can start to discuss the API method that we want to put out there with a strong message that everyone should be using it instead of direct
$storage->load()
calls.Here's a proposal for this new method.
Comment #7
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedAnd a few more discussion points:
- should this method handle multiple entities instead of just one? I think that modules like Inline Entity Form can display many entities to edit at once, so it would make sense for them to have a single call for getting all the entities that are being edited.
- should we introduce a similar method for "view" operations? My initial impression is yes, we need the same API when rendering entities, but let's gather some more feedback first.
Comment #8
timmillwoodThe second paragraph of the docblock explains the "for editing" part, but not really the "active" part, should we add a paragraph about what an active revision is?
"ForEdit" sounds a little odd, and using a present participle such as "Editing" in a method name doesn't seem right either. Maybe
loadActiveEditableEntity
?Comment #9
timmillwoodMaybe we add a load, and a loadMultiple like we already have for entities and revisions?
Yes, even if it's just to go through the thought process (and testing) of what revision should be loaded for viewing and end up deciding the existing load method does this already, or view and edit actually need the same revision (which I don't think will be the case).
Comment #10
hchonovWhat about including into the getter method name a word from the issue title -
safe
? Active would be depending on some context, but the method in the entity repository is only interested into getting an entity which is safe for editing. I know, that we've agreed on using the word "active", but I think that here it makes sense to use "safe" instead. What about "loadSafeEditable
" Or it could be something likeConfigFactory::getEditable()
- probably simplyloadEditable()
?Comment #11
tim.plunkettgetEditable()
andgetViewable()
work for me (not load, as it's a repository not storage)Comment #12
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedNice, so we agree that we also want a corresponding method for view.
I like
getEditable()
andgetViewable()
as well. How about the "multiple" part? Should we simply make these two handle it?Comment #13
Sam152 CreditAttribution: Sam152 as a volunteer and at PreviousNext commentedI wonder if we need to be explicit that we're loading revisions,
getEditableRevision
/getViewableRevision
.Question: if you aren't loading an entity to display it or modify it, what are you doing with it? Do we need to deprecate
EntityStorageInterface::load
? Does the default revision have much meaning, other than being the revision most likely to surface forgetViewable
in an anonymous request? Should we redefine default to mean exactly that?Another thought: how do we reconcile that loading the default revision was previously the correct way to display something "viewable"? I imagine in the same way we would treat
getEditableRevision
. Both ways are okay, one is preferred and you'll be much more compatible with the broader ecosystem if you use the new methods. I think we probably do have to make sure there are no drastic consequences as a result of displaying content from the default revision however.Lastly, is there any additional context that a future revision negotiator might want to use to figure out the correct revision? Would it make sense to call
getEditableRevision('node', 1, 'entity-form');
orgetEditableRevision('node', 1, 'rest');
. It feels to me like it could be a path for breaking encapsulation, but possibly still worth discussing.Comment #14
timmillwoodIn theory
EntityStorageInterface::load
loads a revision too, but we don't state it. I think we should make it as invisible as possible as to which revision is being loaded. Just that we're loading the editable version of the entity or the viewable version of the entity.Creating or Deleting it? But I guess those could be classed as editing.
Would we then change
load
to callgetEditable
orgetViewable
, is the storage calling the repository a little odd?Yes?
Comment #15
hchonovActually the repository will invoke the storage, because most of the logic will land there as the context awareness and the revision branches are concepts, which have to be handled by the storage.
We will be introducing new methods and I don't think we have to deprecate the existing entity retrieval methods from the storages.
The new concepts are pretty complex and therefore I would not force developers to learn them - not in D8 and neither in D9. I would rather leave the current entity retrieval methods as they are in the storage.
Additionally we will have to make the storage load method load by default the safe to edit revision in order to make it possible to keep the current projects as they are, but allow them to enable modules, which are creating revision branches. Thinking about this makes me wonder if we need the methods in the repository?
Comment #17
Wim Leers@effulgentsia pointed me here from #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API., the confusion there would be solved by this. Also, @tim.plunkett++ for creating this great issue summary!
Comment #18
jibranI think this should be classified as critical because it can cause potential data loss if the user is not editing the correct revision whenever edit form is not used.
Comment #19
tim.plunkettThis is shaping up to be the single biggest blocker for Layout Builder, and is also an issue for JSON API. And pretty much anyone that wants to allow saving of revisionable entities outside of their standard edit form.
Comment #20
johnwebdev CreditAttribution: johnwebdev commentedIn an attempt to bring this biggie back to life, here is where I think we stand right now:
Comment #21
Sam152 CreditAttribution: Sam152 as a volunteer and at PreviousNext commentedI wrote up an issue which possibly means this doesn't have to be a layout builder blocker, due to the nature of the way content moderation already integrates with core: #3004536: Move the Layout Builder UI into an entity form for better integration with other content authoring modules and core features. Might be worth reviewing that if followers of this issue are interested in the compatibility of the two.
Comment #22
AaronMcHaleJust reading through this, and I can see how this could be a difficult concept to understand, so my only thought right now is that we must be absolutely clear in the approach, in the issue summary and in documentation what the differences are between the various ways of loading entities (assuming we still have different ways of loading entities after this is done).
Comment #23
Wim LeersThis now also blocks #2992833 — see #2992833-176: Add a version negotiation to revisionable resource types.
Comment #24
plachI spent the last few days talking about this issue with @amateescu. We covered several topics:
What follows is a summary of our discussions. Feedback would be highly appreciated, especially around the use cases, because we need to make sure it's crystal clear what issues this new API is aiming to solve.
Given that amount of new logic the proposed solution implies, and its complexity, I'm marking this as needing release and framework manager feedback. In fact adopting this new API will require a significant change in the way we handle entities.
Use cases
To validate our design we chose to use primarily a fairly advanced, although strictly-real life, use case: editing, in the context of a "stage" workspace, a translated and moderated entity, whose edit form has an hypothetical autosave functionality relying on revisions enabled.
Each entity translation goes through an independent editorial workflow. Once all translations reach an approved state, the workspace is ready for deployment to live. While editing the entity, autosave would trigger the creation of revisions that are only visible to their owner, the active user.
Other use cases we took into consideration, assuming the scenario described above, were:
API overview
As discussed in the previous comments, the new API will allow to retrieve the most suitable entity object to populate an entity form and to be displayed with respect to the current context. After discussing this with @amateescu and @tim.plunkett, we agreed that to reduce coupling and make the API easier to test, the caller should be responsible to provide the API implementation information about the current context. For this we would use the core Context API that blocks are already leveraging to implement visibility conditions. This is how the signature would look like:
Contexts would provide all the required information to perform the revision negotiation. In our reference scenario they would be:
As mentioned earlier, the API, to be complete, will have to take care also of entity display. In fact in many cases the editable revision for a particular context will be different from the displayable revision, as access control and fallback logic may all determine different outcomes. For instance, language fallback rules are applied on display, but only a translation matching the active language should be used to populate an entity form.
The API will also have to deal with multiple items both for edit and display. The typical example here would be those mentioned above: a multiple entity reference widget and the core revision UI.
API implementation plan
As mentioned in #5, we need a way to allow multiple implementers to collectively participate in the revision selection process without having to hardcode assumptions about each other.
We propose to organize revisions in a hierarchy that would represent how the implementers handle individual revisions. Each revision would have a reference to its parent along with additional metadata about the context the revision was created in.
Each implementer would be defined via a "revision type" plugin, which might be tied to one (or more?) context. The definition would include a weight that would be used to build a hierarchy between context items, for instance: workspace → active language → user.
The revision graph (think of a tree allowing two parents for each node, technically a directed acyclic graph) represents the revision branches that could be generated when editing an entity in multiple contexts. In our reference scenario an entity could be edited in multiple workspaces, in multiple languages, by multiple users each one triggering their own autosave revisions.
This graph structure will also be used for conflict resolution: when needing to merge revision branches a new merged revision having two parents will be created.
As mentioned above, each revision will have contextual metadata attached, allowing to look up the fittest revision for the specified context. In fact a revision matching exactly the specified context might not exist, so a fallback logic will need to be available. The context hierarchy defined through weights will be used to support that: more and more generic fallback contexts will be built until a suitable revision is found. For instance we may have the following situation:
Assigning a sensible weight to each definition is likely to imply some degree of mutual knowledge among revision types, however theoretically this would not require explicitly coding against each other. It should even be possible to alter definitions to change weights and thus the way revision types play with each other.
OTOH altering revision type definitions and changing context info might make some revision contexts invalid, in which case the fallback logic would have to kick in again with possibly unexpected results.
In our reference scenario, we would have three context-defining revision types: Workspace, Content Translation, Autosave. Content Moderation does not require any special kind of context, so it would not need to affect the revision lookup logic. On the contrary, adopting this new API should allow us to simplify/clean-up the current limitations around revision translation and possibly deprecate the
revision_translation_affected
flag and the logic around it. This is not an immediate goal, however, as the focus is on stabilizing Workspace.While the public API mentions variants, since it's dealing with both revisions and translations, under the hood we will just use the revision graph to identify the revision most suitable for the specified context and then instantiate and return the entity translation matching it.
Implementation process
Introducing this API will require a lot of work, so we propose to split it in the following sub-tasks:
revision_parent
field with the parent reference(s) and the context metadata (parent issue).We should probably synchronize the commit of these issues to make sure we don't introduce half-baked solutions in an official core API.
Edit: updated the example image.
Comment #25
plachUploading an updated example image depicting also revision translation to be embedded in the previous comment.
Comment #26
plachA few additional notes from my discussions with @amateescu that people might be interested in.
The proposed plan addresses also the revision translation use case pretty well, via "translation branches", as it allows to tackle the two main issues we had to face while trying to make Content Moderation play well with Content Translation:
Both issues are cleanly addressed by the revision graph, which enables both reliable parent lookup and conflict resolution.
We also briefly discussed whether we should try to generalize the revision graph to deal with variants, that is any possible variation of an entity along any additional axis besides revision and translation, e.g. per-user entity variants. This were my conclusions after some mulling:
Comment #27
gabesullice👏 👏 👏 👏 @plach++ & @amateescu++!
That's a really incredible summary and it's very exciting!
I had a few miscellaneous thoughts that came up as I was reading it:
entity_reference_revisions
module? It'd be awesome if this meant revision references made it into core.For JSON:API, we have a few needs that I think are implicit in this proposal but should probably be made explicit (especially because this proposal has implications beyond the original scope of this issue):
/jsonapi/node/article?resource_version=id:42
, we should be able to check access to that entity, but also that particular revision (there's not a generic API for this today)Not all of that would need/have to come at once, but we should be careful not to preclude any of those capabilities or make them difficult to add in after.
Overall, this is really really impressive. Your use case was really well designed I think.
Comment #28
plach@gabesullice, #27:
(thanks! :)
1: If I understood @amateescu's plans correctly, the tree field will extend the entity reference field, but won't deal with revisions explicitly. It will be the revision graph-specific customization (a "provider" in the tree field's parlance) to define the revision references, so I don't think the current plan entails bringing ERR to core. @amateescu is this correct?
2: Agreed :)
3: Again, I'll leave a proper reply to @amateescu, but I think the "protocol" approach you are suggesting to implement "horizontal" extensibility without sacrificing strict typing (and IDE autocompletion, I'd say) is interesting and similar to the approach we took in Content Translation with the
ContentTranslationMetadataWrapper
: that wraps an entity object and provides additional accessors methods around fields added by CT. Does this make sense?Thanks for making the JSON:API requirements explicit: this exactly the kind of feedback we need! Although I feel these are follow-up material, since this issue is going to be quite big already.
4/5: In this new revision world supporting revision branches, we no longer have a single official revision history, so I'm not sure what that would return. Maybe only default revisions? While discussing the core revision UI with @amateescu, we were thinking that a possible sensible way to populate that page could be to provide a list of all the ancestors of the active (displayable) revision for the current context.
6: Noted :)
7: I'm not sure how a revision access API would look like, maybe just running entity access check on an entity revision object could be enough as a first step. I'm not sure how such an access API would work in the reference scenario we are discussing, but I can see how the things could be related. Thanks for bringing this up.
8: The Context API is a pure API relying on the current request/session, so it should be possible to make it work with REST APIs just fine AFAICT.
Comment #29
cosmicdreams CreditAttribution: cosmicdreams at Nerdery commentedYes this seems like an idea born out of attempts to fix the Content Moderated, Translatable Paragraphs issues.
----
If I understand 4 & 5 correctly, JSON API is seeking to maintain all administrative functionalities available to standard admin scenarios. Therefore the revision history here is similar to looking at the revision history of an entity's revision page / the revision history of a specific entity within a specific workspace. Regardless of the JSON API / standard perspectives we'll need to maintain this key functionality.
7) I can imagine the following use cases that may have you checking the access level of a specific revision:
As a content creator, I want to provide access controls on a specific workspace.
As a content creator, I want to provide access controls on a specific moderation state.
In each case you'll have to look up contextual access information for a specific revision.
Comment #30
AaronMcHaleThis all sounds very exciting, there is possibly a few other core issues that are looking to introduce a core entity hierarchy API, so perhaps this could feed in nicely.
I do a lot of work which involves creating custom content entities, so with these proposed changes it would be great to see if can also reduce the amount of boilerplate code that is needed to create content entities which have revision support.
Comment #31
Sam152 CreditAttribution: Sam152 as a volunteer and at PreviousNext commentedThanks for distilling your conversations down into such a succinct summary and making sense of all the discussions that have been taking place. Here are a couple of initial back-of-the-napkin thoughts on the plan:
Don't feel the need to respond to all of these points if they're far-off concerns that might distract from getting stuff down now.
Comment #32
plach@cosmicdreams, #29:
Well, that's one use case, and a tricky one, since parallel editing of revisioned Paragraphs means sorting out conflicts between entity revision references, which is definitely not straightforward. I wouldn't say that's the main driver though: to stick with core, we need Layout Builder and Quick Edit to play well with Workspace, Content Moderation and Content Translation (and everything else of course).
Of course we'll need to keep that working somehow, but what we should clarify is what we are expecting to see/return in that case once we have revision branches. Our suggestion is to display (return) all the ancestors of the revision matching the current (display) context, this way we can always have a straight sequence of revisions going from root to leaf. With this logic, if the default revision matches the current context, we would replicate core's behavior (without CM enabled). If we have a single main branch with pending revisions generated by CM, again we would replicate the current core behavior. So this approach seems to be "backwards-compatible" with respect to what we have now.
And, yes, it definitely seems we should add some kind of advanced access control to our reference scenario, thanks for pointing that out.
@AaronMcHale, #30:
I'm very sensitive to this topic, I'd also like to improve the core entity API so that implementing a custom entity type is just matter of providing an entity class plugin, but this sounds definitely out of scope for this issue and friends.
@Sam152, #31:
Good point! I think we started referring to this concept as "active" revision at some point in our earlier discussions. Maybe we should stick with that?
I'm not sure I follow, can you elaborate on this?
Yep, I think this solution is definitely worth exploring. It might help us decouple this issue from the tree/hierarchy work that's definitely not trivial. One possible approach could be to use
TranslatableRevisionableStorageInterface::getLatestTranslationAffectedRevisionId()
and possibly combine that with some hard-coded assumptions as you were suggesting. I was also thinking that we should probably mark everything as@internal
(core-only usage) until we are confident the API and its implementation are mature enough.Quite possible: Andrei was mentioning he has plans for an alternative storage. It would be great to have something similar to what https://www.drupal.org/project/taxonomy_edge provides.
I think those would all end up in the global context, as CM does not require one. An exception could be translated entities: we might be able to re-create translation branches and the related contextual metadata via the RTA flag.
Comment #33
gabesullice<bikeshed>JSON API has already settled on "working copy" for this concept, as in "the copy on which work is to be done" (we took this from RFC5829).</bikeshed>
Comment #34
gabesullice100%. I didn't know about
ContentTranslationMetadataWrapper
! That's great to see that the rough idea has already had some validation. I imagined that if it were more fully fleshed out, "Protocols" would be plugins that could define a set of base field definitions. Entity types would then opt into various protocols via an annotation (and modules could "opt them in" via an alter hook). Once opted in, an implementation ofhook_entity_base_field_info
could then aggregate the base fields of each protocol implemented and add them all to the entities base fields (with a field name prefix ofprotocol_
for example).Comment #35
plachAfter more discussions in Slack with @amateescu, @catch, @effulgentsia and @tim.plunkett, we decided to move forward as suggested by @Sam152 in #31 and provide a minimal implementation of the API based on what's currently available in core. I will create a follow-up issue to track the work proposed in #24.
@effulgentsia also asked me whether editable vs displayable should be considered part of the provided context as well, instead of being separate concepts hardcoded in the public API signatures. I didn't have an answer during our call, but the more I think about it, the more I'm convinced that this is an "orthogonal" way of differentiating between variants. I think this ultimately boils down to the concept of "canonical representation" I was mentioning in #26:
That said, I think it would make sense to add the following methods to
EntityRepositoryInterface
:::getActive()
::getCanonical()
::getActiveMultiple()
::getCanonicalMultiple()
Attached you can find a patch with an initial implementation of the solution suggested above.
Comment #36
plachA couple of notes:
This is a workaround needed to make the logic work, but I think all defined language types should be returned among available contexts, since it possible to retrieve the negotiated language for any language type from the language manager.
This is only part of the public API to improve BC in
EntityConverter
, but it should be removed from the interface before 9.0.0, or even in 8.7.x if release managers are fine with that.Comment #37
Wim LeersThis is what #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API. should use, regardless of whether Content Moderation is enabled?
Comment #38
plach@Wim Leers:
Yes, I think the ultimate goal is that whenever we have to modify an entity, either via forms or programmatically, we should adopt this API.
Comment #39
plachCreated #3023194: [PP-2] Add parallel revisioning support as a follow-up issue.
Comment #40
plachHere is a new iteration implementing the missing bits (canonical variants and multiple support) and incorporating some feedback I got from @amateescu and @pmelab, coming from their work on https://github.com/amateescu/revision_tree : the patch changes the method signatures to accept entity type and ids to avoid having to load all entities in advance when dealing with multiple ones and makes specifying contexts optional, given that in 99% of cases (tests excluded) we need the currently available ones.
The interdiff is not very useful but I'm uploading it anyway for completeness.
The patch should be ready for final reviews.
Comment #41
gabesullice👏this is great :)
Comment #42
tim.plunkettOverall this looks great!
Nit: These are all going to need @trigger_errors in this patch
This label part seems odd. Aren't those just magic strings? Or worse, strings that might be translated?
Doesn't the order matter here?
Ahh ignore the above, I see how/why this is done. Tricky!
Ooof these multiline ternaries are throwing me off.
This is great example of using this!
Now, that's kinda odd. Maybe this doc should be reworded for this piece of code?
Comment #43
plachThanks!
This should address #42:
1: Fixed
2: That code is definitely suboptimal, see #36.1. However, even if we introduce a reliable way to retrieve content language from contexts, we will still have to compare labels because that's what
CurrentLanguageContext
uses to distinguish language contexts. OTOH comparing labels should be safe enough, as we are retrieving them from the authoritative source, which takes care of possible translations (that's why we're casting to string).5: Sorry :)
6: Yep, aside from the fact that in 99% of cases we won't have to specify contexts explicitly.
7: Added a comment about that.
Comment #44
plachSorry, the interdiff was wrong, although the patch is correct.
Comment #46
plachRandom test failure, created #3029999: CKEditorIntegrationTest fails randomly for that.
Comment #48
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedVery nice work overall! There are a couple of non-trivial points below so setting to NW for them.
8.7.0 -> Drupal 8.7.0 :)
The same change is needed in the deprecation tests for them.
Would it be possible to write a "multiple" version of
getLatestTranslationAffectedRevision()
? OtherwisegetActiveMultiple()
is kind of pointless :/context -> contexts :)
These should be
int|string
.identifier -> identifiers.
Shouldn't we replace the
if
code block below this with an "else -> $repository->getCanonical()"?If we do that, we will lose the ability to pass
['operation' => 'entity_upcast']
toEntityRepositoryInterface::getTranslationFromContext()
. Could that be considered a slight BC break, given thatConfigurableLanguageManager::getFallbackCandidates()
fires an alter hook which contains information about the operation context?Is the usage of UID 1 intentional? If not, it would be nicer to use another UID which doesn't have any special meaning.
Same here.
We shouldn't lie about what the variable holds :)
Also, crediting @pmelab for his help with shaping this new API.
Comment #49
plachThis should address #48 and #42.2 (the interdiff is rather big, individual commits available in the sandbox):
I discussed #48.2 with @amateescu and we agreed to work on that in a follow-up if for any reason #3023194: [PP-2] Add parallel revisioning support cannot happen in time: #3031082: Optimize EntityRepositoryInterface::getActiveMultiple().
This patch also includes some changes to the method names to (hopefully) improved DX: we are adding a
Variant
suffix to clarify that we are dealing with entity variants, which is the official name we use to refer generically to entity revisions and/or translations.I was attempting to clarify the documentation linked above when I realized that the distinction between "active" and "canonical" I proposed in #35 does not cover the full picture. I think "editable" vs "displayable" are two modes that apply orthogonally to both "canonical" and "active" variants:
In this light @effulgentsia's idea to make editable vs displayable part of the contextual information makes much more sense to me: even if edit and display are well defined operations and have distinct logic applying to each one in our implementation, this might not be the case with alternative implementations of the entity repository. Contrib/custom code could provide alternative operations that make sense for their specific implementation.
All that said, I think the new API should expose three methods (and their "multiple" counterparts):
::getCanonicalVariant()
, assumes the "view" operation. Optionally supports the "edit" operation to skip language fallback rules and return an existing translation (or a new one) matching the current context.::getActiveVariant()
, assumes the "edit" operation by default, and returns an existing translation (or a new one) matching the current context. Optionally supports the "view" operation context to apply language fallback rules.::getDisplayableVariant()
, thin wrapper around::getActiveVariant()
hardcoding the "view" operation.Thoughts?
Comment #50
tim.plunkettGlad to see this changed!
+1
Comment #51
plachTODOs for myself:
We likely need a way to statically cache this, it doesn't feel ok to perform it every time we perform a param conversion.
Missing keys and related test coverage.
Is this still needed?
Update method names.
This should be
@entity.repository:legacy_context_operation
.::getContextValue()
Comment #52
omrmankar+1
Comment #53
plachFrom #49:
I discussed this topic at length with @amateescu: our conclusions were that maybe we don't need to differentiate between "edit" and "view" after all.
The two main areas we covered were language fallback and access control:
In all cases, assuming all users have edit access on an entity, there may be revisions they have access to and other they don't have access to. For access logic to be required to be part of the negotiation process, though, we would need some kind of access fallback rule: "you don't have access to this revision, so here is another one you can manipulate". However, this is never required in the examples above and keeping negotiation and access control separate feels cleaner from an architectural POV.
More specifically, considering the use cases above:
The common theme seems to be that if you don't have access to the active revision, there is no possible access control-driven fallback logic.
To sum up, if people agree with our findings above, there's no need to differentiate between "edit" and "display" contexts, and we can just rely on the canonical variant (default revision) vs active variant (latest revision) split.
Edit: fixed typos/grammar.
Comment #54
tedbowGreat work here!
Totally agree!
In layout builder we separate our access to control the layout for an entity from access to editing the entity.
i.e, you can have access to edit the layout with edit access to the entity.
so we would not want
getActive()
to not return back anything if the current user cannot edit the revision. I think it should be the callers responsibility to check access.$load_entity_revision->access()
would check access for the revision but it does not. This is an existing issue and if we could solve in separate issue that would be great.Because if we baked into this issue that would still not solve the problem of
$this->entityTypeManager->getStorage('node')->loadRevision('2')->access()
not actually solving checking access for the revision. maybe ultimately we would need
checkRevisonAccess()
but we should have access checking for any revision not just the `active` or `display` variants.Comment #55
plachThis should address #51.
Comment #56
plachMore static caching:
Comment #58
tedbowdrupalci fail
Comment #60
plachI'm afraid we have to use proper static caching here.
Comment #61
plachMinor clean-up
Comment #62
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedWe're adding this static cache in two places already in the patch, shouldn't we move it to the context repository directly?
I'm not a big fan at all of the "variant" suffix, I think it makes the method names look scarier than they should be.
Thinking about this for a while, I'm warming up to the proposal of using "working copy" instead of "active". It's a terminology that's already established in a few places outside of Drupal, and I think it conveys the message for "give me an instance of this object that is ready for editing" quite well.
Does this mean that we can remove all the test changes related to the current user (including the trait) from this patch?
Comment #63
plach1: Unfortunately both
EntityConverter
andEntityRepository
may retrieve contexts independently, meaning on different execution paths, so we cannot share a static cache unless the context repository itself does it. Btw, I cannot locate that hunk in the current code, was it from a previous patch?2: I have to think about this, I'll come back to it with my thoughts. Of course method names are still up for debate as anything else :)
3: Nope, the current user stuff is now needed because the current user context relies on at least one user existing in the database, it's unrelated to that @todo.
Comment #64
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThat's what I proposed :)
Very likely, I think I added the comment in an old tab. That's also the reason for the status change..
Comment #66
plachD'oh, reading whole comments would help from to time, sorry.
I didn't go that way because: 1) I'm not sure that's desired 2) it might be tricky to do safely, because current contexts are global to the request, but I'm not sure that's true for any context, so we'd need a separate issue to address that carefully.
It would be great to have Tim Plunkett's feedback on this.
Comment #68
plachThis should fix test failures.
Comment #70
plachBetter fix
Comment #71
tim.plunkettnit: This should no longer be needed since #3028319: Specialized entity interfaces should extend the main EntityInterface landed.
I didn't even realize this function was still in core!
Is this still the best practice way to do this?
The two methods added have almost identical one-line descriptions, with only one word different.
getActiveVariant()
saysfor editing
getCanonicalVariant()
saysfor display
Back in #11 I suggested
getEditable()
andgetViewable()
View vs Display isn't important. But I think if they are as split as "for editing" and "for display", then Active vs Canonical is confusing for no real gain.
Comment #72
jonathanshaw`editable` and `viewable` do not suggest a single variant, let alone a very particular single variant. `activeVariant` and `canonicalVariant` have more of that flavour.
Comment #73
plachThat documentation is outdated and wrong: the split is no longer between editable and viewable, but between canonical variant (default revision + translation negotiation) and active variant (latest revision + translation negotiation). See #53 and previous comments for details.
Comment #74
tim.plunkett#72 I wasn't suggesting a return to that naming.
#73
Leaving NR to get other feedback, but let's make sure the docs are fixed.
Even now I'm not 100% sure which one I'd call when, but that might just be me.
Comment #75
johnwebdev CreditAttribution: johnwebdev commentedThe names varies on the revision state, why not use latest and default instead? getDefaultVariant and getLatestVariant.
Comment #76
tedbowNeeds reroll I think because of #3023981: Add @trigger_error() to deprecated EntityManager->EntityRepository methods
Comment #77
AaronMcHaleNot sure how useful this is, but when working with Entity Types canonical refers to the route/controller that is used when the user is viewing the Entity, e.g. entity.node.canonical. So there is precedent for the use of canonical but it's important that use of the term is accurate to mean the revision the user is currently viewing, as we shouldn't cause any unmercenary confusion.
I've only loosely been following this discussion so not sure how useful my input is but thought I'd mention it either way.
Thanks
-Aaron
Comment #78
plachThis updates the documentation to reflect what we was described in #53 and should address #71.
I discussed static caching with @tim.plunkett and it seems that would be better addressed in a separate issue. I'll ping @catch about this.
@amateescu, #62.2: I thought about the proposal of using "working copy" terminology a bit. It would work for me if people feel that's preferable, personally I'd still prefer "active variant" because it seems that the examples you mentioned only take revisions into account, whereas here we are trying to reinforce the concept of variant, i.e. something that may vary based on multiple axes. However I realize this might be subjective :)
@AaronMcHale, #77: that was definitely the main reason why we adopted the "canonical" term :)
Comment #79
plachComment #80
effulgentsia CreditAttribution: effulgentsia at Acquia commentedSame here. However, there's a bit of difficulty in mapping our terminology to RFC 5829. I might be wrong, but as far as I can tell, their use of "working copy" isn't compatible with versioning of the working copies. In other words, the viewable resource is the one with a version history, including a "latest version" (which would correspond with our "default" variant), but the "working copy" is a separate resource which then becomes the "latest version" of the original resource when it is "checked in". I suppose nothing in that spec prevents the separate "working copy" resource from also being a versionable resource with a "latest version" (which would correspond with our "active" variant), but my reading of that RFC is that that is a different model than what we have in Drupal, where we maintain a version history for the not-published (active) copies as well as for the published (default) copies as a single history of a single entity.
I think there might be a way to adapt the language to clarify this more, but still retain some correspondence to that RFC.
How about:
getCurrentCanonical()
(because there are multiple versions that have been the canonical one at some point, and we're interested in the one that's canonical now)getLatestWorkingCopy()
(combining the terminology of "latest" and "working copy" to clarify that our working copies are themselves versioned)I'm totally open to these suggestions being rejected or further improved.
In my opinion, there's nothing in "getCurrentCanonical" or "getLatestWorkingCopy" that implies a lack of axes such as language. The "current canonical" can be language-dependent as can the "latest working copy".
Comment #81
plachI spoke with @effulgentsia yesterday about #53 and the overall architecture and he was onboard with the route we took so far. We went through several use cases, but the main outcome of our discussion was that the following statement from #53 is actually correct:
The reason is that contextual information acts as an identifier itself, thus, even if we may apply fallback rules to identify a revision matching the current context, once we identify a matching context, that's the active revision and there's no further fallback possible, because a specific (revision) resource is unequivocally identified by the specified contextual information.
Comment #82
plachIf we can achieve that it's great, but honestly I don't think it's a requirement. I wouldn't do that at the cost of making the method names less clear or descriptive.
So, I'm not sure about
getCurrentCanonical()
: personally I have the same issues with it that I had withgetCanonical()
, I can't help but thinking "get canonical what?". On top of that, I'm not sure adding "current" really buys anything in this context, because the API doesn't let you retrieve a previous canonical version. Unless we add that, this sounds like cognitive overhead to me.getLatestWorkingCopy()
sounds better to me, although I'm a bit concerned about the "copy" terminology: here we are not dealing with a copy of the original/canonical resource, but with a variant of the same resource. I think that "copy" is potentially confusing from this POV.Were I to choose, I'd still go with active/canonical + the variant terminology, since "variant" is the official term we introduced to refer generically to translations and revisions in our documentation. However I think it's more important that people that aren't deeply involved with this issue provide their feedback, since they are those who will benefit/suffer the most from this choice. For that reason, I think I'm the last person that should be making this choice :)
Comment #83
plachBtw, "active" is consistent with the "active language" and "active translation" terminology we use in the Entity Translation API.
Comment #84
plachI spoke with @tim.plunkett and @catch (separately) about statically caching available contexts: Tim suggested to address that in a follow-up and Nat confirmed that a major followup is acceptable because param conversion is only triggered when rendering tabs, so the performance hit is more or less constant. Additionally, this is not an issue introduced by this patch, rather by the way we do tabs/contextual links.
With this change I think the code reviews above are all addressed and we just need to find consensus on the method names.
Comment #85
tim.plunkettNit: these no longer match the names that might change again anyway :)
Nit: This should either be one line or be:
I prefer one line
Nit: I can't find anywhere that says not to do this, but it looks super weird without another chained call. I write these on one line.
Is this still necessary?
Can remove the @var
so it is *the* responsibility
I don't follow this change. And this code comment will read as very odd in future versions of core. How will we know when this can be removed? Or can it never be removed?
Any reason not to inject this service?
It's also not clear if this @todo is in reference to the service (which would explain why it's not injected) or just the addition of the legacy value
Honestly this issue is going to cause a lot of these changes. Might be worth mentioning in the CR. Especially since it's a new trait.
Comment #86
BerdirHere's a first review, I read the IS (which I guess isn't up to date or at least it's very short) but almost none of the comments, I did follow this issue for a while though but usually just *very* quickly skimmed through the notifications.
One thing I don't understand yet is how the contexts are playing into this, so it would be very helpful to have that explained in the IS. It likely is already in some comments, but this issue has a lot of discussion and longs comments.
I finally got around to do a review because I've been working on #2988331: Content translation moderation quite a lot recently, and it is very closely related to this. It's about finding the right revision/translation, both in a hook and a controller and displaying all kinds of information whether it is the default revision or not, if there are pending revisions, if the current entity object is a pending revision and so on. That was surprisingly hard (this issue is critical for a reason) I learned a few things in that issue, so I wanted to share it here, I don't think this issue can/should address all of it, but maybe a few things could be improved or we can create follow-ups.
* \Drupal\content_moderation\ModerationInformation has several methods that seem weird/wrong and we should consider to deprecate them. Most obvious is isLatestRevision(), which currently doesn't work at all with translations. Another is isLiveRevision(), the concept of "live" is very strangely defined there, it "live" when it is the latest *and* default revision and is published. That doesn't make any sense to me and the method does in fact seem to be completely unused in core. Unsurprisingly. I guess they should have been deprecated already when $entity->isLatestRevision() and $entity->isLatestTranslationAffectedRevision() was added. Because contrib modules like moderation_sidebar still use them.
* getLatestTranslationAffectedRevisionId() can return an old non-default revision if the another translation was saved last and then isDefaultRevision() returns false but it is not a pending revision. See the changes in ModerationSidebarController::sideBar(). To reproduce, create an EN entity, then add a DE translation as published, then call getLatestTranslationAffectedRevisionId($id, 'en'), which will still return the revision that EN was, well, last affected. This seems rather confusing and I don't think some of the comments that are in this patch are aware of that.
* I also had troubles to figure out if the currently entity is a pending revision, bu I forgot about \Drupal\Core\Entity\ContentEntityBase::isLatestTranslationAffectedRevision(), that should allow me to simplify my patch a bit.
isn't the loaded entity always the default translation?
configurable or configured?
I'm not sure I understand how a non-internal interface can have internal methods. Changing anything would break all implementations of it?
What exactly is the removed parameter for here? The language manager was not at that position, so AFAICS, we could just remove it, including the documentation? Subclasses can have additional constructor arguments and also pass them to the parent: https://3v4l.org/gpS9D.
the language manager service is most certainly not deprecated, only passing it to the constructor is :)
I'm not sure we need to be that fancy with the deprecation messages, this is quite a lot of code for BC.
Why don't we use if ($this->enityTypeManager->hasDefinition() { throw exception) instead of a try/catch-rethrow?
As comments usually don't explain the history of something, I'm not quite sure what it is trying to tell me. As far as I see, this can not be a temporary BC thing, so it still and will continue to throw that type of exception indefinitely (seems like the fact that it throws this exception and not the other is pretty random and only because we called getStorage() first).
this is an explicitly tagged internal protected method, can we just drop it?
In #2244513-186: Move the unmanaged file APIs to the file_system service (file.inc), we just added such a method as private to prevent it from being used.
these methods are definitely going to conflict a few times unless it gets in very soon if we don't get them in soon as I'm continously adding more deprecated tests here as I'm adding @trigger_error() to more and more methods.
Maybe add them further up in the class, between some other methods to limit that risk? not pretty, but it's just a BC test.
Comment #87
plachThanks for the review, @Berdir!
The IS is definitely outdated, since we didn't find consensus on the overall strategy until very recently. I will update it ASAP, however the most important thing you should be aware of in the meantime is that the implementation of the API in the current patch is meant to be replaced by the more complete one outlined in #3023194: [PP-2] Add parallel revisioning support. The IS over there is up-to-date.
Btw, since you are not familiar with the previous discussions yet, any early feedback on the method names?
Comment #88
plachComment #89
plachThis should address the review above, a few replies follow:
@tim.plunkett, #85:
1: Fixed
2: Fixed. When you say "should" are you referring to the PEAR coding standards? I couldn't find anything relating to ternaries in ours.
3: Fixed. I prefer multiple lines when a single line risks not to fit a normal-sized viewport.
4: Not strictly, I thought it would still be good to have it since it's used in two places and makes the logic easier to override for subclasses. Not feeling strongly about this, I can remove it if it's undesired.
5: Fixed
6: Fixed
7: Removed that part entirely: I realized that
EntityConverterTest::testConvertWithInvalidEntityType
was expecting the wrong exception, because the previous code was callingETM::getStorage()
and that does throwPluginNotFoundException
when the entity type does not exist.8-9: Added a comment to make things more explicit.
10: Good point, I need to remember about this when writing the CR.
@Berdir, #86:
1: Yep, which reminded me that we were planning to perform translation negotiation also when retrieving the active variant. Fixed that and added the related test coverage.
2: Configurable. If I'm not mistaken
ConfigurableLanguageManager::getLanguageTypes()
returns all configurable languages, regardless of their configuration being actually customized.3: Very good point. The idea was to be able to iterate on this, but clearly we can't do that if implementors break. What about adding a separate interface only the core
EntityRepository
implements?4: That's the only way I found to keep BC: the current code supports both passing two parameters (correct) and three parameters (deprecated). I agree the PHP doc and the variable name are a bit confusing but I don't know how else to document this.
5: Fixed the message. If I'm not mistaken most of the logic there is to support both 2 and 3 params and retain BC, it's not about being fancy :)
6: See bullet 7 in the previous list.
7: Well, given that keeping it working is cheap and this
EntityConverter
is actively being extended, even in core itself, I think it's worth the effort. Usingprivate
for this stuff is controversial AFAICT, so I tend to avoid it for now.8: Moved them above, I'm not sure that will be enough, since we are removing the last method.
Comment #90
plachThis implements:
Working on the IS update now.
Comment #91
plachUpdated IS, I'd like to wait for the terminology discussion to settle before creating a CR draft.
Comment #92
plachCreated #3035037: Entity param conversion may be slow due to available context resolution to track the performance regression.
Comment #93
plach@Berdir, #86:
Yep, once this is committed, CM and CT will need to be updated to adopt the new API, as well as other places probably.
The implementation provided here is only the minimum required to make the API usable in core and let core modules play decently together. If all goes as expected, in #3023194: [PP-2] Add parallel revisioning support (or in a follow-up) we should be able to deprecate all the logic revolving around the latest and the latest translation affected revision and exploit revision parents and branching to properly track the active revision for each translation.
Comment #94
gabesulliceFrom the IS:
Is there already an issue for a follow-up which addresses revision access control?
Comment #95
plachNot that I know of.
Comment #96
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@plach and I discussed the naming again and bounced around various suggestions. Here's some of them:
-
getCanonicalOf()
/getWorkingCopyOf()
which lead to-
getCanonicalCopyOf()
/getWorkingCopyOf()
Those lead to
getMultipleWorkingCopiesOf()
, which makes it sounds like you are getting multiple "copies" of the same entity, and it doesn't make any sense. At that point we both thought that it's best to go back to the original names, which are very simple and easy to remember after all:getCanonical()
/getCanonicalMultiple()
andgetActive()
/getActiveMultiple()
.Comment #97
larowlanthis will trigger a warning in older versions of php because you can't pass non-variables by reference
nit: we could return here instead of breaking
no need for the else here
shouldn't we be doing an instanceof check in the constructor to make sure we've got the correct service and then trigger a deprecation error if not and grab the service from the \Drupal singleton?
we do that below in EntityConverter but not here?
we can return here and avoid the else
its a pity we didn't make entity_upcast a constant, follow up? (out of scope here)
should we make constants for this? would be on the experimental interface for the 'may change' signal
same here, would have expected some instanceof checks and some trigger_error ?
this looks great and in my opinion should be split off into its own issue as a) it is useful on its own and b) that would reduce the patch size here
Comment #98
plach@larowlan, #97:
1: According to the docs,
current()
doesn't expect a reference: PHP 5.0.5 does throw a notice, but I think it's just a bug.2: 👍
3: 👍
4, 8: I'm not sure I follow: by passing those parameters to the parent constructor we do trigger notices and instantiate services via
\Drupal
.5: 👍
6: Actually we can do even better, we can get rid of that entirely in #3031124: Deprecate the "entity_upcast" operation specified in EntityConverter, since apparently no one is using it ;)
7: Good point, however I think it's the responsibility of the exposing service to define those constants, so in the case of those two it would be
CurrentLanguageContext
. This patch adds a constant for@entity.repository:legacy_context_operation
.9: Yup, this is now postponed on #3035825: Make it easier to set up the current_user service in kernel tests. See you there ;)
Attached you can find a complete patch, a review patch, and two separate interdiffs as I messed up my local branches, sorry :)
Comment #99
plachAnd this reverts the method names to the original form, as mentioned in #96.
Comment #101
plachRerolled on top of #3035825-12: Make it easier to set up the current_user service in kernel tests. No review patch as it is the same of #99.
Comment #103
plachAnother reroll
Comment #104
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI looked at the 'review' patch from #99 and i could only find two minor nits:
This was already mentioned in #85.4 and IMO we should remove it.
Shouldn't we move this above all the entity schema installs, like we do in every other test which needs it?
Comment #105
AaronMcHaleIn my opinion “canonical” is fine because that’s an understood term to mean what appears when the user is viewing the Entity, but “active” seems a little too ambiguous to me.
I’m concerned “active” could be confused to mean “I am viewing the Entity therefor I am viewing an active revision” and “I am editing the Entity therefor i am working with an active revision”, see what I mean?
Essentially what I’m trying to say is that developers and administrators could misinterpreted “active” and use the “active” revision when they meant to use the canonical revision, because it is “active” therefor it is “live”, “currently available for use”, “therefor it should be used”, etc.
Comment #106
plachThis addresses #104.
Comment #107
plachCR draft added at https://www.drupal.org/node/3036722.
@AaronMcHale, #105:
It seems the naming issue is not settled yet: given that the API is marked as experimental, to allow unblocking the many issues that depend on this, I'd suggest to commit this as-is, given the current solution is something most people that provided feedback can live with. We can iterate over the method names in a follow-up, we could even create a poll about those.
Comment #108
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedFully agreed with #107 :)
I think we're finally ready to go here! This issue still depends on #3035825: Make it easier to set up the current_user service in kernel tests, but setting to RTBC for extra visibility.
Comment #109
catchJust here quickly to say 'canonical' seems good, will try to do another review early next week.
Comment #110
plachThe blocker was committed, one more reroll.
Comment #111
AaronMcHaleRegarding naming: in that case I'm fine with #107, should we make a follow-up issue to discuss naming further then?
Comment #112
plachYes :)
Comment #113
tim.plunkettTagging. Glad to see this RTBC.
Comment #114
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedOpened an issue for using the active variant in entity forms by default: #3037144: Ensure that entity edit forms use the active variant of an entity
Comment #115
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedFound a few CS problems while working on other followups.
Comment #116
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI just realized that we should probably deprecate all the methods introduced in #2926483: Add API methods for determining whether an entity object is the latest (translation-affecting) revision and provide a single
isActive()
replacement for them. This is the method that, for example, Quick Edit should use instead of$entity->isLatestRevision()
inquickedit_entity_view_alter()
, which would ensure that Quick Edit works well with Workspaces by default.This could be a followup, but we're running short on time until 8.7.0-alpha1 so I'd like to propose this interdiff. Looking forward to any opinions :)
Comment #117
tim.plunkettWe're short on time, so I'd prefer to get this in as-is.
Fixing tag
Comment #118
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedOk, here it is: #3037247: Add an API for determining if an entity object is the active variant
Comment #119
Berdir> retrieving the active entity variant matching the specified context: this is the variant that is suitable for editing and previewing.
Not sure if "previewing" is used anywhere in the code or so, but it clashes a bit with the node preview feature, which is completely unrelated to this issue, this is more about the "latest version" thing.
+1 on the new method names, no need for any suffixes.
This introduces a new concept (the experimental namespace), a bit worried this will result in discussions on naming and so on. Not sure I like "Experimental" :)
I know this was done based on my feedback of having @internal on a non-internal interface, but unsure about this approach. Having two interfaces with the same name might confuse people relying on autocomplete and so on (this being internal will help, but still) and it also means everyone who uses it will see a hard break in case this is then merged back into the main one.
I think having input from @alexpott on this would be useful. Other maintainers of course too :)
I still think that $removed_parameter is not necessary for BC since it is the last argument anyway. I can try to work on an alternative patch but likely not before tonight and I don't want to hold this up. We have to touch it anyway in the 9.x branch to add the type hint for $entity_repository, so its not a big deal.
Comment #120
xjmWhy is adding an
Experimental
namespace more desirable than simply providing BC according to our continuous deprecation policy?FWIW I agree with @Berdir that using
@internal
on code that's intended to be extended is bad practice and just contributes to no one paying attention to@internal
.Comment #121
xjmAlso I'm -1 on allowing interface method signatures to change (something @plach mentioned in Slack). We tried this concept with experimental modules themselves -- people were told the whole module was not to be extended -- and then BC breaks in said modules resulted in significant disruption for the ecosystem anyway and led to perceptions that Drupal 8 is unstable. Essentially this is just a developer-centric recycling of that same concept. Unless the code is only alpha and therefore not shipped in any release, we should expect to provide BC for it.
Comment #122
xjmNW for the above. Let's just do our best to get the interface and methods right here, and if we need to change them in the future let's follow the deprecation process to the best of our ability.
Comment #123
plachThis addresses #122 and indirectly #119.1.
@Berdir, #119:
I updated the CR to try and follow more closely the PHP docs but sill provide a high-level summary: https://www.drupal.org/node/3036722/revisions/view/11325181/11325221.
2: Well, I think it's good to use the injected entity repository rather than retrieving it directly from the container, if people bother passing it, even it's in the deprecated position :)
Comment #124
plach@xjm:
The reason why I proposed to use that
Experimental
namespace was twofold:@internal
methods on interfaces is suboptimal and something to be avoided as much as possible, so I think it would be great to have a way to mark an API as "not mature enough yet", rather than using@internal
, which additionally conveys the wrong idea, because early adopters that know what they are doing would be welcome to use such APIs, whereas proper@internal
stuff is "hands-off" (and as such it would be great if it didn't live on a non-internal interface).We do have experimental modules and IMO we should have experimental APIs. This was probably not the right issue to introduce this idea, because one if its main drivers is to make Layout Builder stable and as such we cannot remove the API from 8.7, but I do think that a developer-centric recycling of [the experimental module] concept is something worth having or at least discussing.
Comment #125
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThe interdiff looks good to me.
I'd also like to note that #3037144-7: Ensure that entity edit forms use the active variant of an entity converts all entity forms to use the new API added here, and it seems to work very well.
Comment #126
jibranCan we please add a paragraph in CN about what does it mean, which version will be loaded, if we use old methods like
$storage->load()
and$sotrage->loadRevision()
?Comment #127
jibranI think we need a follow-up issue for views to provide the field plugins for
getCanonical
andgetActive
?Comment #128
hchonovFrom the CR :
Reading only the CR it does not become clear how could custom contexts be defined and used to influence the decision. I think it would be really helpful to add an example for that.
Comment #129
plachThis hopefully addresses #126 and #128: I added a paragraph about the storage layer, more info about contextual information, and another example using custom contexts.
Sorry if the examples are a bit vague, but unfortunately this API is rather new for myself as well, so how contextual information could be used besides what we have in mind in #3023194: [PP-2] Add parallel revisioning support is not completely clear.
I'm not sure I understand #127: Views targets specific revisions and has already extensive support for translation handling, including translation negotiation, so I'm not sure anything is needed in the CR. Of course we may want to be able to list the active variant for each entity, however this seems like a new feature to me, rather than something requiring an update. I might be wrong of course :)
Comment #130
hchonovDoes this mean that the new API does not make it possible to use custom contexts to influence the decision?
Comment #131
tim.plunkettIn addition to the examples given in the CR, you are free to register any context provider as a tagged service.
Comment #132
hchonovAnd then how can I influence the decision which entity revision will be loaded based on my new context?
Comment #133
plach@hchonov:
In the current (minimal) implementation only the language context is considered, however in #3023194: [PP-2] Add parallel revisioning support we are planning to allow to register revision type plugins that would be tied to one or more contexts, including custom ones.
Comment #134
hchonovSo here we introduce a new API, which cannot be fully used and there are plans to introduce a mechanism for fully using it later?
If there is already a plan on how the API will be modified to allow custom contexts, then why isn't that part included in here and later used by the referenced issue?
The new API is confusing without this ability, at least for me, because we have a new API and we can only pass the langcode and nothing else as a context.
Comment #135
plachBecause there is no logic that can leverage additional contexts in core at the moment. The rationale behind this choice was that this way we can unblock several issues that are currently blocked on this API not existing. Those can work well enough with what core provides (translation support + pending revisions), without us risking to introduce a full-fledged solution that has lots of moving parts and dependencies and that will never be ready in time to be shipped with 8.7. One of the main drivers for this issue is allowing Layout Builder to be marked stable and that requires at least a "stub" implementation of this API.
The API was originally meant to be marked "experimental" to reduce the potential confusion deriving from the minimal implementation, however this is no longer on the table (see #120 - #122). OTOH an alternative implementation of the entity repository could be provided in contrib and that could leverage contexts as it sees fit (https://github.com/amateescu/revision_tree is doing exactly that), so I'm not sure how to alleviate this concern besides describing in the CR how things are currently working.
Comment #136
tim.plunkettFollowups have been created.
WRT to #134, we need the API methods in place _right now_. Figuring out future improvements to the internals is a followup.
But as #135 points out, there are already ways to improve upon this from contrib/custom code.
This codifies what exists right now. Thanks for the incredible work, it's time to get it in!
Comment #137
BerdirThis conflicted with one of my entity manager deprecation issues on EntityManagerTest, sorry :) Rerolled that.
Also, the constructor stuff is itching me, so here's a proposal for making that a bit simpler based on how we did that in recent issues. The entity_repository argument is new in 8.7, nobody outside of core is implementing that yet, so there is no reason to care about that. There is no need to babysit different historic versions of deprecated arguments as long as it is not broken. Either it's injected properly, then we use it, or it is not. Unit tests are broken by it anyway and in the end, those are the only thing that really care about it being injected.
We could even remove the "if ($entity_type_manager instanceof EntityManagerInterface) {" block based on the latest agreement because that will eventually get the trigger_error() due to still being called. But that's out of scope.
I'm only aware of a single override of EntityConverter::__construct() in contrib, that's in search_api and that doesn't even provide $language_manager yet, which is also fairly new (8.5?). This is 20 lines of code less, and the worst case is that a class that already injected language_manager gets a *slightly* less helpful deprecation message, that will be obvious when he looks at the new constructor.
Comment #138
BerdirAnd now with the correct variable name for the deprecation message.
Comment #139
tim.plunkettThanks for the reroll and the cleanup!
Comment #142
geek-merlinAs of #124:
> I agree that having @internal methods on interfaces is suboptimal and something to be avoided as much as possible, so I think it would be great to have a way to mark an API as "not mature enough yet", rather than using @internal
My gut feeling is that i'd prefer @deprecated for this. It's "the stuff that works now and i'll have to touch later". In this case "may or may not have to touch"...
Comment #144
jibranRE: #129
hehe, I was not talking about CR at all in #127. I was just saying, did we need dedicated views field plugins for
getCanonical
andgetActive
? Now that these concepts/function exist how can I make sure my node listing view is showing the correct version of node.Comment #145
plach@Berdir, #137:
I didn't realize this, that's why I was insisting on supporting three parameters. The change makes sense to me, however deprecation tests need to be updated as well.
@jibran, #144:
I'm not sure about field plugins, I'd have to look at the Views codebase, but I think it would make more sense to open a follow-up to discuss that in detail.
Comment #146
plachComment #147
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThe interdiff looks good, hopefully the RTBC sticks this time :)
Comment #149
plachMinor clean-ups suggested by the bot.
Comment #150
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThat's weird, I already did those changes in #115..
Comment #151
plachI likely forgot to add that interdiff to my local branch :)
Comment #153
plachBot fluke
Comment #154
larowlanI see both sides of the argument here.
I agree with @hchonov here, we're not signalling where the API extension point will be.
And we've been burnt before by not having an actual implementation.
However, I'm fairly pragmatic and I realise that this just isolates and consolidates what we're already expecting modules to implement on their own in order to interact with non-default revisions and the model that CM uses (which is obviously not the only one out there)
So as it stands it makes it easier for modules to do the right thing, but it doesn't open up all the possibilities that non-default revisions could entail
So...
If I understand correctly it is likely that this will be the actual extension point and that it will involve passing along the contexts, so would it make sense to isolate this into a protected method that also accepts the context argument.
That way, we can implement whatever the API ends up being for filtering the entities based on context in that method but also, if people want to override the entity repository in advance (which at the moment seems to be the only option available), they can do so without need to re-implement all the other logic in this method. And obviously we need to be considered about this, because protected or not, this is a new API that we'll have to support with BC layers going forward.
Would that make this more palatable in the current form @hchonov?
Comment #155
plach@larowlan:
Nope, that will not be an extension point, sorry. In the final implementation we are hoping to deprecate (or more correctly generalize) all that logic and implement something more flexible relying on parallel revisioning, which is a plan we discussed in generic terms in a previous call (see #5).
I really don't see the what's the problem with core not supporting other contexts than language: it's not like we are preventing people from providing an alternative implementation, however at the same time we are leaving open the possibility to implement the plan we all, including @hchonov, agreed upon during the call. If there are concerns about the implementation proposed there, they should be expressed in #3023194: [PP-2] Add parallel revisioning support, this is only formalizing the status quo.
@hchonov's concerns about a missing extension point can be addressed by working all together in a more complete implementation of the API, be it in contrib first or directly in core in the follow-up issue. The important aspect is that this needs to be a collective effort and not something any party takes on in isolation.
Other people working on a theoretical alternative implementation should be aware of what is going on: it was stated fairly explicitly in the CR before, and we can restore that part without calling the current logic experimental: this is not done, this is a work in progress. We need to be practical here: does it make sense to sacrifice important product management goals just to ensure all the work is done in a single chunk? Does it even make sense? Normally we evolve Drupal through specific milestones and this has been the case also here. The plan has been laid out two months ago and discussed with a release manager and a framework manager, among the rest (see #35). That was the moment to express concerns, now it is simply too late.
Comment #156
plachI had a discussion about this with @hchonov in Slack: his main concern is that API methods introduced here might not be enough to address some advanced use cases, so he'd prefer if we didn't introduce a public API in this issue. FWIW I share the same concerns, I just didn't realize this is what we were talking about. Ideally we would like to avoid a widespread core/contrib adoption of the new API until it's deemed ready, so definitely not in 8.7.
@catch suggested yesterday in Slack that a possible alternative to adding an experimental API could be introducing the new methods without adding them to
EntityRepositoryInterface
. Then Layout Builder and any other part of core needing the new logic to behave properly could usemethod_exists
to call it conditionally.Building on this proposal, I believe we could then mark the new methods as
@internal
, which would allow us to change their signatures, if needed.Comment #157
tim.plunkettThis introduces a huge problem for LB and any other code:
What do you do if the method does not exist?
Either it would be up to that code to duplicate the entirety of this logic again, in which case the new method won't be of any use anyway.
Or, the fallback would be to load an entity without any revision or translation logic. Which would mean that a contrib or custom override of EntityRepository will completely break any systems that attempt to switch to it.
-1 on #156, +1 for the RTBC patch in #149
In response to #119 through #124:
Within the Layout initiative, we also attempted to use an
Experimental
namespace.See #2296423: Implement layout plugin type in core, comments 132, 133, 158.
This was ruled out as an option until #2833347: Allow experimental modules to rely on \Drupal\Core\ namespaced classes that are only loadable when the module is enabled was solved, which is not going to happen in time for 8.7
Instead, we've taken the approach of making our best attempt at getting things right, and relying on the deprecation policies to change things when we need to.
Comment #158
hchonov@larowlan, like @plach already mentioned we've talked in Slack about that and it looks like we share the same opinion :) :
I would prefer to have some more advanced use cases implemented for real or at least in a test before making the API publicly available. But if it is not an option to keep it internal or experimental or anything alike then I will just give my +1 on the current approach.
Thank you all for the amazing work.
Comment #159
xjmYep, agreed with #157. Let's go ahead with #149, and we will use our BC and deprecation process if we need to make improvements in the future.
Comment #160
larowlanOk, it seems we have consensus that this is what we want for now and we'll have to provide a BC layer for it if we change it later.
Crediting those who shaped the patch with reviews/input etc
Comment #162
larowlanFixed on commit
Committed 094890e and pushed to 8.7.x. Thanks!
Thanks all for the respectful discussion here on what is a complex problem space
Comment #163
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedYay, glad to see this in. Thanks everyone!
See you in #3037144: Ensure that entity edit forms use the active variant of an entity and #3037247: Add an API for determining if an entity object is the active variant :)
Comment #164
plachThanks all!
I just repurposed #2938929: Make the entity repository required for EntityConverter to reflect the changes to the
EntityConverter
constructor.Comment #165
jibranAdded #3038910: Adds views plugin for \Drupal\Core\Entity\EntityRepository::getActive() and \Drupal\Core\Entity\EntityRepository::getCanonical() for the discussion about views plugins in #127, #129, #144, and #145.
Comment #166
kattekrab CreditAttribution: kattekrab as a volunteer commentedComment #167
plachComment #169
hanoiiI just submitted #3116468: _skipProtectedUserFieldConstraint is being forcefully reset which is a bit of a follow up to this, if you can please review it and chip in there it would help. The fix on this issue broke something I was doing on an external module in relation to _skipProtectedUserFieldConstraint which also relates to #2904451: Allow to change password through RPC endpoints through the reset password workflow.