Problem/Motivation
Currently, setting revision user, revision log message, and revision creation time on a new revision is a manual process for every entity type. In Core we have individual implementations for Block Content, Media, and Node. All 3 do things slightly differently. Contrib then has to repeat this process for any entity type that supports revisions and these fields (usually when an entity type implements RevisionLogInterface
Proposed resolution
This can be done generically in entity base classes (ContentEntityBase and EditorialContentEntityBase).
Set revision user, revision creation time, and revision log message based on an entity implementing RevisionLogInterface automatically
Remaining tasks
Framework manager signoff for using user module code in the Core namespace. See #68
Refactor code in EditorialContentEntityBase to use RevisionLogInterface if possible.
User interface changes
None
API changes
Deprecating getRequestTime in Media entity.
Data model changes
None.
Comment | File | Size | Author |
---|
Issue fork drupal-2869056
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
dawehnerComment #3
BerdirThe thing is that the owner thing is IMHO wrong, see #2376999: Non-existent node author user IDs are silently converted to 0 (?) (without validation errors). So I think that should not be done by default.
Added the revision log author and also the preSaveRevision() part, but that's bit tricky as well, as we need the method from the trait to get the field name (unless we want to duplicate that logic) but overriding methods like that from a trait is tricky, so I put it on EditorialContentEntityBase.
Comment #4
dawehnerI'm wondering whether all this logic should actually be moved to a field type level.
Comment #5
BerdirMoving an assertion for media from #2880199: Revision user not set when saving media.
@dawehner I'm not sure, I think fields having business logic that essentially spans across multiple fields is tricky
Comment #8
seanBReroll since the patch no longer applied, no changes..
Comment #9
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThis looks great to me.
Comment #10
larowlanWe shouldn't be referencing node in ContentEntityBase in my opinion
Can we get a change record here too
Comment #11
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI'm not really sure we need a CR here because I don't think there's any action to take by a module developer after this change..
Comment #12
dawehnerWell, I still think its valueable to inform people that this happens now automatically:
Comment #13
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedFixed #10.
Comment #14
amateescu CreditAttribution: amateescu for Pfizer, Inc. commented@dawehner, ok, do you think this helps? https://www.drupal.org/node/2908984
Comment #15
dawehnerThank you @amateescu!
Comment #16
larowlanThe issue summary is about revision user, not log messages - is this out of scope?
Also, I note BlockContent does the same dance, so if this is now part of its parent class, that dance can be removed too - assuming scope is correct.
Comment #17
BerdirBlockContent didn't extend from this yet when this was RTBC'd, but yes it can be included now as well :)
Also updating title to include revision log, now it's in scope I'd say ;)
Comment #18
BerdirUpdated the patch.
Comment #19
vijaycs85Adding some of the changes from #2897026: \Drupal\media\Entity\Media::preSaveRevision() handles the revision timestamp compared to other entity types
Not sure if we need this change though
Comment #20
vijaycs85Comment #22
dawehnerIt always feels wrong to add test coverage in some random test when its actually about base classes. Could we add it to some of the
entity_test
based tests instead?Comment #23
seanBThis should fix the tests.
I agree with #22 but that test was only added so we could close #2880199: Revision user not set when saving media. This issue fixes that one as well.
There is no base test for
EditorialContentEntityBase
as far as I could see, but I guess most of the things we test for in Node/Media are actually functionalities added by its base classes. We could/should probably refactor a lot of the content entity stuff (implementation details, forms, tests) into more usable base classes. While working on media I found that there is way to much duplicate code between content entities and sometimes this also leads to unexpected implementation differences. This is far beyond the scope of this issue though.Comment #25
seanBAlmost, let's try again..
Comment #26
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedEntity types can implement
RevisionLogInterface
without extendingEditorialContentEntityBase
, so this needs to go into intoContentEntityBase::preSaveRevision()
.We can use
get|setRevisionLogMessage()
here.And
setRevisionCreationTime()
here :)This is used for the 'created' base field, not for the 'revision_timestamp' field, so I don't think we can remove it.
Is the assertion message correct here (the "not" part)? Isn't the intention of the assert to check if the new revision owner is the same as the original entity owner ID?
Comment #27
Berdir@amateescu:
1. See my comment in #3 why I put that part there, we rely on methods that don't exist on ContentEntityBase and that are not o nan interface. I guess we could add an instanceof check for the trait or a method_exists but that's not very pretty.
5. assertion messages in phpunit are different from simpletest, they are shown only when an assertion fails and should explain what went wrong. See the other assertion messages in the context there.
Comment #28
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedRight, disregard #26.1 and .5 then :)
Comment #29
seanBThanks for the review! New patch is attached. Some remarks.
getRevisionLogMessage()
on$this->original
, but since$record
is astdClass
, we can't usesetRevisionLogMessage()
right? At least added the change forgetRevisionLogMessage()
.setRevisionCreationTime()
on thestdClass
.REQUEST_TIME
fromDrupal\Core\Field\Plugin\Field\FieldType\CreatedItem
? But maybe I'm missing something.Comment #30
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedAgreed with the remarks from #29, let's do this :)
Comment #31
chr.fritschComment #32
plachFirst review with my new shiny framework manager hat on, so please bear with me :)
Ouch, this is really not an option, sorry. We cannot depend on module code from a Core namespace (see http://cgit.drupalcode.org/drupal/tree/core/lib/Drupal/Core/README.txt). This is a clear signal we need some decoupling here:
UserInterface
extendsAccountInterface
so the two systems are cleanly decoupled. We should do the same here by introducing\Drupal\Core\Entity\OwnedEntityInterface
, to be extended by\Drupal\user\EntityOwnerInterface
. We could even deprecate the latter and move all methods up to the former. Additionally we should introduce\Drupal\Core\Entity\OwnerInterface
which\Drupal\user\UserInterface
can then extend.It would be nice to also decouple
RevisionLogInterface::setRevisionUser()
, but that will require a follow-up because, to avoid BC breaks, we are basically forced to move every single method currently defined inUserInterface
up toOwnerInterface
, which kind of defeats the purpose of this decoupling. More discussion needed on this probably. A @todo would be enough for now, I can create the follow-up if none beats me to it.ContentEntityBase
has no concept at all of user/owner whatever, not a single line dealing with that. This strongly suggests this code actually belongs inEditorialContentEntityBase
. If for whatever reason a subclass does not extendEditorialContentEntityBase
, IMO it's fine for it to handle its own "owner" logic (pun non intended :).More coupling here :(
Aside from that, we should really stop invoking
\Drupal::
from non static contexts. Maybe we can just move the original static method ontoEditorialContentEntityBase
, mark it as @internal, and restore the default value call? We can then open a follow-up to explore the possibility to make dependencies available to default value initialization methods. Again, I can take care of that.I was initially thinking this would be a BC break because of the
{@inheritdoc}
PHP doc, however that is actually a static method on the class, it doesn't appear on the interface, so it's not part of the public API. If we go the way I suggested above, we don't have to worry about this though.Can we also get an updated issue summary, please? The issue title is clear, but the IS itself looks a bit outdated: it took me a while to understand what was going on here :)
Comment #33
plachAh, I forgot: the CR seems outdated...
Comment #37
plachCrediting people from the issues closed as duplicate of/obsoleted by this.
Comment #38
plachScratch #32.3: I didn't realize that's the revision created time...
Comment #39
plachI've created #2925682: [PP-1] Decouple RevisionLogInterface from the User module to discuss the follow-up work for #32.1.
Comment #40
Berdir1. Hm, depending on a module is indeed problematic, but..
a) There are several other such dependencies that already exist beside \Drupal\Core\Entity\RevisionLogInterface (which you mentioned) like \Drupal\Core\Field\Plugin\Field\FieldWidget\EntityReferenceAutocompleteWidget.
b) I'm not sure that an entity owner can be an account, I think it needs to be a user entity. To be honest, I'm not sure I fully understand the difference and point of AccountInterface anymore and I'm pretty sure I introduced that in the first place :) (Checking.. yep that was me :p). Also since \Drupal\Core\Session\UserSession also heavily depends on role entities from user module.
c) I don't think we can have a new OwnerInterface and extend the existing one from it with a different type hint: https://3v4l.org/js8rM (that's what I understood you suggested)
Drupal\user is a module but it is a required module and while we try to avoid it, Drupal\Core, unlike Drupal\Component is allowed to have "drupal dependencies". I think it's more like that we have to move in the opposite direction and move user entities to Drupal\Core?
2. Hm, good point, but technically, EditorialContentEntityBase doesn't actually have any existing owner logic either, so that's not really a better place for that :)
3. We can't inject dependencies into entity classes at the moment.
Comment #41
plach@Berdir:
1.a: I know we have other "backward" dependencies, but I don't think we should be adding more, that means increasing technical debt instead of reducing it, so I'd like to explore a more "correct" solution, if we can find one :)
1.b: Sorry for not being clear, that was just an example of how other Core code solved a similar issue, and I knew it would sound familiar to you ;)
By no means I meant that an entity owner has anything to do with an account. The whole point of what I was suggesting was exactly to decouple the concept of logged in user from the concept of entity owner, which theoretically might even not be a site user at all.
1.c: What I'd like to do is abstracting the generic bits that characterize an entity owner out of
UserInterface
and bring those to core. I don't think the full User module functionalities and the related concepts (registration, profile, roles, permissions and so on) belong to the entity system and those are all in some form represented inUserInterface
. This is what I originally had in mind: https://3v4l.org/n7fa6. However the::getOwner()
method returns aUserInterface
, so we cannot change that without breaking BC badly. Hence what I'd suggest we do is the following: https://3v4l.org/L5ApP. That way all module-provided entity classes are free to depend on the User module, but code living in Core, including future traits generalizing this logic, can deal with IDs only and don't need to know aboutUserInterface
at all. This is not fully fixing the issue, as we still have an actual coupling, but at least interface relationships should be cleaner :)2: Well, it implements
RevisionLogInterface
, so IMO that's a reasonable place for that hunk. At least the concept of revision "owner" (or "author") is definitely there :)3: Yep, that's why I was initially suggesting to restore the default value callback: I didn't realize that that code was concerning the entity created timestamp, not the revision one. Actually the following change was introduced in #23 and looks unrelated to me. On a second look the fact that it's needed to fix test failures is concerning, am I missing anything?
Aside from that, it's a shame that we need to couple the entity system with the request time component, since again we had no concept of request in the (Content)Entity class so far. Every time we decide to provide a default value inside a method, we are at risk of (needlessly) introducing a new dependency, and even if we may be able to inject these sooner or later, I'm not sure an entity is the right place to do that. Anyway, at least that's a Component, so it's not as bad as depending on a module, I'd just wish we could revert this trend, instead of adding more and more of these
\Drupal::
occurrences, that imply more technical debt and (often) a more coupled system. It would be better if there were a way to set this value from outside the entity class, but I don't have a good suggestion, so I guess this will do for now.A couple of minor things I found while reviewing the code again:
Can't this be simplified to
if (!$new_revision && isset($this->original) && empty($record->$revision_log_field_name)) {
?This can be simplified to
if ($new_revision && empty($record->$revision_created_field_name)) {
. I guess0
is not a valid value for this field, right?Comment #42
BerdirHi @plach
Just some thoughts, need to think more about this stuff. I understand your reserveration, but I'm not sure how to address that properly.
One problem with this issue is that while it is classified as a task, it also actually *adds* this mandatory functionality to media entities which are missing it right now and that is a bug. So as this seems to be more complicated, we could re-open the media bug report and fix it with copy & paste there. But the fact that this logic is mandatory and easy to forget is IMHO also an argument to find a generic way to solve it.
1b/c) I'm not sure that just moving the ID methods to OwnedEntityInterface would really help. For this request yes, but an ID *without* having any meaning whether it is a user entity or not is not enough, getOwner() is nice for DX when working with the owner (e.g. loading the name or some other information from it to display it). We couldn't deprecate EntityOwnerInterface then. And sure, we definitely shouldn't move the whole of user module into Drupal\Core, if anything then just the user entity.
The dependency on the current time (which itself is depending on the request) is not new. We're just making it more explicit since we deprecated REQUEST_TIME, which we've been using for a long time in ChangedItem/CreatedItem for example. That's what moving things to components does, it makes dependencies more explicit :)
One random idea would be to find a way to move this logic to user module, at least the parts that depend on an owner. Then we don't introduce a dependency on it. But I don't think we have a hook/event right now for preSaveRevision() and not sure we want to introduce that. It would also result in this functionality being more hidden.
Comment #43
plachMaybe the User module could define a
\Drupal\user\OwnedEntityBase
class extendingEditorialContentEntityBase
and implementingEntityOwnerInterface
? That way all module-defined classes could safely depend on the User module without "polluting" the Core entity system?Comment #44
BerdirPossibly, but the problem with base classes is always that you can only have one. So if we add OwnedEntityBase then you can't extend from something else anymore. EditorialContentEntityBase is a bit different because it combined quite a few things together which is helpful.
Comment #45
plachWell, this would be just a User-flavored version of
EditorialContentEntityBase
, so if you're fine with the latter, you should be fine also with the former IMO. A User-defined trait would have the opposite advantages/drawbacks...Comment #47
matsbla CreditAttribution: matsbla commentedI created this issue that might be a good solution for this issue #2978701: Replace the revision log message with a comment field
Comment #50
AaronMcHaleOne possible solution to avoid depending on
EntityOwnerInterface
would be to use$this->getEntityType()->hasKey('owner')
to determine if the Entity supports having an owner, you can use a similar abstracted approach for getting and setting the actual value. See \Drupal\user\EntityOwnerTrait for more details.Having said that there already seems to be president in the core Entity API for depending on the User module, see \Drupal\Core\Entity\RevisionLogInterface and \Drupal\Core\Entity\RevisionLogEntityTrait for example. So given that either we could go with my suggestions above to abstract it out a bit, or we could just use the existing president I just linked and continue on, then later open a meta issue or a task issue to fully decouple the Entity API from the User module.
Comment #54
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedHey @Berdir @plach - just wondering what the way forward is here. A legitimate bug was closed as a duplicate of this Task and the conversation has essentially been dead since 2017. I just came across this bug with a different entity type so agree 100% that it should be fixed in some generic way.
I've rerolled #29 against HEAD, setting to NR to make sure things still pass.
Comment #56
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedFixes deprecated calls.
Comment #57
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedGah...fixing whitespace.
Comment #59
kristiaanvandeneyndeThis also seems related.
Comment #63
larowlanmicro-optimisation: should we store the result of ::getEntityType in a variable instead of calling the same method twice?
do we need !isset and empty? I think empty covers both cases
I don't think we can delete this, we'll need to trigger a deprecation and keep it until D11.
Public methods are technically an API
Love the cleanup here
Comment #64
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedFixes #63, the interdiff is pretty scuffed since #57 applied with quite a bit of fuzz. I'm not sure if more work needs to be done to decouple stuff as per #32?
CR drafted - https://www.drupal.org/node/3349765
Comment #65
BerdirI've already discussed #32 with @plach then and still think that Drupal\Core doesn't have to be free of Drupal\user references, would be nice, but Drupal\user is a required module and the difference between a Drupal\Core component and a required module like user and system is IMHO minor and mostly historical/due limitations what exactly Drupal\Core components can contain.
I can see that other parts of that comment have been addressed, the logic was moved to EditorialContentEntityBase. That said, wouldn't RevisionLogEntityTrait be a better fit for this logic? EditorialContentEntityBase is just a handy aggregation of several traits and currently doesn't have its own logic.
Comment #66
smustgrave CreditAttribution: smustgrave at Mobomo commentedWonder if this could still get an issue summary update please per #32
Comment #68
lauriiiDefinitely agree that we shouldn't be referencing module code from
Drupal\Core
and it's a policy that has been documented in https://git.drupalcode.org/project/drupal/blob/HEAD/core/lib/Drupal/Core.... However, I think it would be fair to ask for an exception from the framework managers based on the fact that there are several pre-existing uses of the same interface in theDrupal\Core
namespace. We could open a follow-up for implementing a solution for #41. It doesn't look like implementing that becomes any harder if we fix this issue separately first.Comment #69
AaronMcHaleCould we move
Drupal\user\EntityOwnerInterface
toDrupal\Core\Entity\EntityOwnerInterface
, and then have it useDrupal\Core\Session\AccountInterface
rather thanDrupal\user\UserInterface
?Technically it might still work, as
AccountInterface
has anid()
method andUserInterface
extendsAccountInterface
so it wouldn't be far removed from the current implementation.Doing that would allow us to decouple from the user module.
Comment #70
Berdir> Could we move Drupal\user\EntityOwnerInterface to Drupal\Core\Entity\EntityOwnerInterface, and then have it use Drupal\Core\Session\AccountInterface rather than Drupal\user\UserInterface?
An "account" is currently an abstract concept that might or might not be a user entity, in theory it could be something else. Yes, UserInterface extends AccountInterface, but that means that a user is an account, not necessarily the opposite. Of course, most places we assume it is and every User::load($currentUser->id()) would fall apart badly if it weren't, but technically, that is how it is defined. So we'd first need to redefine that an account id does in fact need need to be a user id. At that point, having two different interfaces wouldn't actually make too much sense anymore.
We'd also definitely need to to #2345611: Load user entity in Cookie AuthenticationProvider instead of using manual queries as well.
So that would have pretty big implications, and would set us back at least one major release cycle before we could rely on that.
Comment #71
AaronMcHaleI was thinking about that in the back of my head, but I was also thinking that, but in theory a site could have its own "user" implementation which extends AccountInterface, and in theory not even use the user module. Again, this is just a theory, I've never practically tested that, and it's very likely that it just wouldn't work, but in theory we could support that scenario by purposefully not coupling to the user module.
Comment #74
mstrelan CreditAttribution: mstrelan at PreviousNext commentedRe-rolled #64 for 11.x and converted to MR. Hiding patches. Have not addressed any recent comments.
Comment #76
ankithashettyUpdated version to
10.2.0
.Thanks!
Comment #78
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedPushed an updated 11.x branch to the fork to fix the spellcheck error, see https://www.drupal.org/project/drupal/issues/3401988#comment-15400828
Comment #80
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedUpdated IS, added framework manager tag based on #68. FWIW - I really hope we can use the interface there because the magic getters/setters are not great in the current state.
Comment #81
Tomefa CreditAttribution: Tomefa as a volunteer commentedTrying the patch for Drupal 10 in comment #64 and when creating a new media, the revision user is now correctly set.
But when editing the same media with another user, the revision is still set to the owner user id and not the user that is editing the media.
In media_form_alter, i have to manually set it to finally have the current user in the revision log.
Comment #82
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedNW based on #81 - great find @Tomefa
The weird part is, that code looks to have come from Node (setting the revision author to the author if it's not set) so I wonder why that same issue doesn't already apply to Node.
Will try to dig into it today.
Marking as Needs tests to cover that scenario (editing with a different user)
Comment #83
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedTo answer #82 - it comes from here https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Co...
Manually testing Media works fine for me.
However, it doesn't make sense to set the revision author in 2 places, and it especially doesn't make sense to set it to the author. I think we should remove it from the form and fix the entity base implementation.
Comment #84
acbramley CreditAttribution: acbramley at PreviousNext for Service NSW commentedPushed test coverage for #81, still unsure how to reproduce it via the UI since Media goes through that same form base.
The
!$this->getRevisionUser()
check in ContentEntityBase also fails on API based updates so we may need to remove that and always hardset to the current user.