Problem/Motivation
The ContentModerationState
entity type is @internal
so any time users interact with it directly, they are risking API breaks. One of the convenient touch points with the entity is implementing hook_entity_(insert|update)
as seen in the content_moderation_notifications. Implementing these hooks gives you a single place to react to content states changes.
Proposed resolution
Provide an alternative to these hooks so users have a consistent and @api
way to react to any item of content having its state changed. An event suits this purpose nicely.
Remaining tasks
#39 = https://www.drupal.org/project/drupal/issues/2873287#comment-12304979
Change record.
Review and commit.
User interface changes
None.
API changes
An additional event.
Comments
Comment #2
timmillwoodAs I mentioned in IRC we used to have events that fired when a moderation state was changed, this is how Workbench Moderation does it, but when we moved to store the state in a ContentModerationState entity we decided that the event wasn't needed because we have the create, update, and insert hooks. Although if we're not supposed to interface directly with ContentModerationState entities maybe we should add the events back in?
Comment #3
Sam152 CreditAttribution: Sam152 commentedYeah, if it's marked as internal, we don't want to expose that entity to developers if it can be helped. Implementing an update hook and using the fields for the @internal entity wouldn't be a stable API.
Comment #4
jhedstromComment #5
jhedstromThis adds and fires create and update events.
Comment #6
Sam152 CreditAttribution: Sam152 commentedLooking good, some feedback.
The point of the issue was that users wouldn't ever interact or know about the entity. Instead of passing this into the event, we should pull out what is relevant and put into the event. That can safely keep the entity type @internal while providing @api :)
Can cache the event dispatcher.
We should remove all entity-centric things that allude to the ContentModerationState entity from the event. Maybe this should be one event, but with a method to get the entity whose state has changed, so developers can check isNew()?
I wonder if we can mock event dispatcher and put it into the container and assert the dispatch methods are called instead?
Looks like symfony does something similar in \Symfony\Component\HttpKernel\Tests\Fragment\InlineFragmentRendererTest::testRenderExceptionIgnoreErrors, but I couldn't find an example in core.
Comment #7
jhedstromAh, good call. So perhaps the event can have something along these lines for methods:
getModeratedEntity
getWorkflow
getNewState
getOldState
(Those last 2 methods I haven't thought through if we have that information or not...I assume we do.)
Comment #8
jhedstromWe could potentially do that, but calling the actual event listener seems valuable too, since, in this case, we can verify the event listener had access to the moderation state entity (when refactored, we could verify access to the moderated entity, the workflow, the transition, etc).
Comment #9
Sam152 CreditAttribution: Sam152 commented#7 sounds good to me.
Comment #10
Sam152 CreditAttribution: Sam152 commentedMoving this along a bit. Right now it's red based on another bug I'll open as a blocker. Getting the original state was surprisingly annoying, but seemed to work.
I was able to test the event by replacing the dispatcher in the container, seemed to work pretty well.
Comment #11
Sam152 CreditAttribution: Sam152 commentedThis should go green once #2881645: ContentModerationState 'workflow' column empty for all non-default translations. is addressed.
Comment #13
Sam152 CreditAttribution: Sam152 commentedThis is still blocked, but fixing the fail introduced by the changes. Now that we fire an event which loads some extra information for the event, we can't just create phony ContentModerationState entities for the storage test.
Comment #15
timmillwoodAdded this as a should have in #2755073: WI: Content Moderation module roadmap
Comment #16
timmillwoodThis is currently blocked on #2881645: ContentModerationState 'workflow' column empty for all non-default translations.
Comment #17
bojanz CreditAttribution: bojanz at Centarro commented1) Can't we throw the event on the workflows module level?
Otherwise people using workflows without content_moderation will once again have the same need, and an issue will eventually get raised duplicating this work.
2) Would be great to have pre_transition and post_transition events, instead of a single changed event (the equivalent of post_transition).
Comment #18
Sam152 CreditAttribution: Sam152 commentedThis particular issue is designed to specifically replace the use case of implementing hook_content_moderation_state_(insert|update). This is important, because we're trying to reduce all touch points with the content_moderation_state entity, given it's marked @internal and therefore could be an implementation detail that changes down the track. It's acknowledging that an api exists to accomplish something, but given it's a bad idea to depend on something @internal which could go away, have something better in its place.
The event and hook importantly contain the entity being moderated. I don't it makes sense for that particular information to be in a generic workflows event and I'm not sure how we'd go about making something generic that would accomodate this use case.
Comment #19
bojanz CreditAttribution: bojanz at Centarro commented@Sam152
I literally have the same event in state_machine: https://github.com/bojanz/state_machine/blob/8.x-1.x/src/Event/WorkflowT...
As you can see, it's almost the same, just some wording differences.
So, definitely generic enough.
(And the reason why I'm saying this is because I'd love to see workflows in core achieve feature parity with state_machine at some point, so that I can deprecate state_machine. The event is half of that).
Comment #20
tacituseu CreditAttribution: tacituseu commentedSince Workflow is just a ConfigEntity now and the storage isn't handled in generic way (each WorkflowType implementation is responsible for it), it would be tricky/hacky to get $entity as context in the event,
Content Moderation uses bolted on ContentEntity for storage, having plenty of issues trying to keep it in sync with translations and revisions of the moderated entity (also views integration), this feels like a lot of effort just to duplicate what @FieldType would get out of the box.
Comment #21
Sam152 CreditAttribution: Sam152 commented#20 is spot on, there currently no solution which provides generic field/entity based storage of a state, with appropriate transitions wrapped around them, so there really isn't any generic context on how to fire an event when a workflow is being used.
If core workflows provides the mechanism for configuring states and transitions, the type plugins serve to integrate that configuration into whatever storage or feature can use them. In the case of content_moderation, it's an entity type, but as explored in #2835545: Provide a Workflow FieldType that references a workflow entity and tracks the current state of an entity, field types could easily satisfy other use cases. I think a
@Fieldtype
and@WorkflowType
together would serve the use case state_machine current provides.For that reason, it's up to the module implementing the plugin to provide context and events around how the workflow is being leveraged.
Comment #23
timmillwoodMoving this back to 8.4.x, this is a "should have" Content Moderation issue and blocking it becoming stable in 8.4.0
Comment #24
Sam152 CreditAttribution: Sam152 commentedRerolling with some minor changes the the function name for consistency. Back to active again now that the blocker is resolved.
Comment #25
timmillwoodComment #26
Sam152 CreditAttribution: Sam152 commentedComment #27
Sam152 CreditAttribution: Sam152 commentedComment #28
Sam152 CreditAttribution: Sam152 commentedComment #29
Sam152 CreditAttribution: Sam152 commentedComment #30
Sam152 CreditAttribution: Sam152 commentedComment #31
Sam152 CreditAttribution: Sam152 commentedComment #33
timmillwoodRe-roll
Comment #34
jhedstromSince this patch adds the official way to react to moderation state changes, it would be really good to get into the first stable version of content moderation (eg, 8.4.0), so that contrib can do things the correct way, and not have to refactor for 8.5.
Content moderation notifications is one such example.
Comment #35
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThe patch looks great to me, I just have one question:
I'm very curious why this test needed to be changed because I don't see how these changes could affect the rest of the patch.
And another question for @bojanz: do you think that #20 and #21 address your concern properly?
Comment #36
Sam152 CreditAttribution: Sam152 commentedBoth these test cases were creating a ContentModerationState entity that didn't correspond to a real entity that had moderation enabled. It wasn't enabled for entity_test and node 9999 doesn't exist.
Now that we're dispatching an event with the moderated entity, it needs to be a real entity. So in the test case, we're creating real moderated entities that cover the same circumstances.
Comment #37
jhedstromThis missed 8.4.0, but I'd still like to advocate for it getting into 8.4. Otherwise contrib modules will start to leverage entity events on the moderation state entity that we are trying to keep internal here, and then need to be re-written for 8.5, which will be difficult to maintain BC for both versions at that point.
Comment #38
Sam152 CreditAttribution: Sam152 commentedDo we allow new features in beta experimental modules in patch releases? I do agree though, without a supported alternative, implementing the hook will cause some pain in the future.
Now that we're beta, perhaps this could do with a change record, which would also serve to make people aware this exists.
Comment #39
xjmAlright, I discussed this issue with @timmillwood and @Sam152 this morning, and based on that discussion I came to some decisions about this issue:
@internal
on the entity type itself. A developer implementing the hooks wouldn't have a very direct way of knowing, so if we truly considered them internal we'd have to raise that to the developer some way more explicitly, or even prevent them from being implemented somehow. @Sam152 said he'd like to explore this. However, I think that by their very nature hooks are API, so I don't think it's even good DX to try to make them internal.I'm less sure about that, and it's not in scope here, but it's therefore also not in scope to decide events are preferable without that having been adopted core-wide already. We shouldn't make decisions based on a policy/best practice we haven't agreed to adopt. (Put otherwise, #1972304: Add a HookEvent and #1509164: Use Symfony EventDispatcher for hook system do not have consensus, so we should not make decisions as if they do, and we should not add events in a fragmented way so that it's a mixture of hooks and events and you can't know whether to expect one or the other except based on the subsystem maintainers' or patch authors' preferences).
So, TLDR, let's stick with the hooks for now. I recommend either postponing this issue on an issue to deprecate the moderation state entity and its hooks, or probably wontfixing it entirely. Setting NR for now in the absence of those issues and to give others a chance to review this.
Thanks everyone! Sorry for the delay; it took me some time to get to the bottom of what the issues were surrounding this.
Edit: Adding whitespace for readability.
Comment #40
Sam152 CreditAttribution: Sam152 commentedThe ship may have sailed for the
ContentModerationState
entity, but I've opened #2917276: Allow entity types to be @internal in such a way that removing them doesn't violate any BC to see if it'd even be possible for some future entity type.Re: the implementation of an event in this issue, I suppose my understanding was also aligned with 6. The movement as I've seen it has been towards implementing events, tagged services and plugins (other stuff too?) where appropriate instead of firing hooks. These concepts individually might not cover all of the semantics of hooks, but if the shoe fits, I think it's hugely preferable to choose one of these patterns. I'd rather think about it as creating APIs with the tools we have (whatever those are) instead of replacing hooks 1-for-1 with some other single concept. If what we have is unsatisfactory for a particular use case, what design pattern can address that?
I did think in this case an event was perfectly suited. The semantics are such that order isn't really important, the data is flowing in one direction and the encapsulation of the data in the event object is nice to work with, type hinted etc.
We have #2551893: Add events for matching entity hooks which is kind of related, but the idea behind this particular issue was "forget about specific entity types, what happens when any content changes state" and also a way to decouple that entirely from the
@internal
entity type.Comment #41
jhedstromI think we still either need something like the event proposed here, or to make the
ContentModerationState
not internal only. The content moderation notifications module has to do some acrobatics to get the previous state of a moderated entity during node save, whereas with this event, that would be much more straight-forward. Alternatively, if the content moderation state entity implemented a public interface for read-only operations (eg, the moderation state has changed from x to y, etc), that would work as well.Comment #42
samuel.mortensonI'm working on adding Content Moderation support to Workbench Email in #2931097: Support the core Content Moderation module and they rely on the existing events to know when a state has changed.
Comment #43
hass CreditAttribution: hass commentedPatch is still green and was already rtbc.
Comment #45
Sam152 CreditAttribution: Sam152 commentedRerolling this. I think even if there is no policy to replace all hooks with events, I think in this case it does make sense. I think @jhedstrom nailed it in #41, to get the equivalent information from a hook (which the ContentModerationState entity internally already mostly understands and has loaded), acrobatics are required. Pulling out the relevant code into the hooks, the DX looks something like:
Comment #46
Nigel CunninghamIs there discussion somewhere regarding why the entity is considered internal?
I have a client wanting transitions to be able to require comments on the reason for the transition; it sounds to me like having a fieldable transition entity is exactly what I'd be after.
Comment #48
marcoscano+1 for this, it will be really useful, especially for people struggling with the
$entity->original
weirdness. #2914901: content moderation state entity with no original value is an example of a scenario where having this event would make developers lives much easier.Patch #45 didn't apply cleanly anymore. Attached re-roll.
Comment #50
Manuel Garcia CreditAttribution: Manuel Garcia as a volunteer and at Appnovation for Pfizer, Inc. commentedArrived here from this rules task #2923001: Support new core Workflows events - which is blocked on this.
I had a look a the latest patch, the test was failing because we now need to use the
ContentModerationTestTrait
to create the editorial workflow, which was introduced in #2952307: Move workflow config into standard profileShould hopefully come back green now :)
Comment #51
jonathanshaw@xjm in #39 suggested this should be won't fix or postponed, but asked for feedback on that.
#40, #41, #45 and #48 have some experienced core devs pushing back in favour of this issue, so setting for RTBC for @xjm to reconsider.
Comment #53
alexpottNote this is an uncached read. Is
->original
never populated?Does dispatching an changed event for a creation really make sense? In order situations we're defining the before state as the initial state which is confusing. See #2914873: The "from state" used for calculating available transitions is changed when using the content moderation state widget and preview mode.
Can be FALSE
Comment #54
xjmAlso #39 has not been addressed? There have been a few comments in favor of the idea of this issue, but no changes to the patch nor specific response to my points.
Comment #55
energee CreditAttribution: energee as a volunteer commentedI vote event instead of hooks, what is required to commit to core beyond #2873287-50: Dispatch events for changing content moderation states? Removal and upgrade path for hooks?
Comment #56
jonathanshawWhat is required are specific (and persuasive) responses to @xjm's points 3-6 in #39
Comment #65
BerdirJust a minimal update for D10, changed event base class.
Comment #66
pooja saraah CreditAttribution: pooja saraah at Srijan | A Material+ Company for Drupal India Association commentedFixed failed commands on #65
Attached interdiff patch
Comment #67
Anchal_gupta CreditAttribution: Anchal_gupta at Srijan | A Material+ Company for Drupal India Association commentedI have fixed CS error. Please review it
Comment #68
pradhumanjain2311 CreditAttribution: pradhumanjain2311 at OpenSense Labs for DrupalFit commentedTry to fix PHPStan error.
Comment #69
pradhumanjain2311 CreditAttribution: pradhumanjain2311 at OpenSense Labs for DrupalFit commentedComment #70
BerdirFixing the dispatch call.
Comment #72
penyaskitoThat also needed to happen in tests.
Fixed that, and the method signature forces us to return something (usually the event itself). This should be green.
Next step should be fixing the php10 deprecation warnings.
Comment #73
penyaskitoThat was the interdiff only... not even properly named.
Comment #75
penyaskitoRewritten the test based on #3306554: InvocationMocker::withConsecutive() is deprecated in PHPUnit 9.6 and removed from PHPUnit 10. Added a todo if this lands before that one. No deprecations anymore.
I feel like writing tests will be even harder now :-(
Comment #76
penyaskitoMostly code standards, but I changed an exactly(5) to any() in previous patch and I reverted that back.
Comment #77
penyaskitoPer #39 still NW.
Comment #79
BerdirJust another rebase, conflict on the save method changes.
Comment #80
smustgrave CreditAttribution: smustgrave at Mobomo commentedUpdated remaining tasks to include #39 + change record.
Comment #81
gbotis CreditAttribution: gbotis commentedpatch from #76 does not apply to 10.2.2
Comment #82
edmoreta CreditAttribution: edmoreta commentedfor 10.2.x use patch from #79
Comment #83
willeaton CreditAttribution: willeaton commentedHi, I was looking for this functionality, its great for being informed of a transition event, but what about being able to veto the transition?
Use case: Product publication.
A user wants to validate a product ready for publication. The system needs to check that the product is really ready. Maybe they haven't filled in all the required fields for publication (fields which cannot be required to create the initial entity/node). Maybe they haven't prepared related content which is required for publication.
The system should be able to validate certain data before allowing the product to pass to a validated state.
Previously we used the "Workflow" module but we decided against it because the module doesn't seem to be maintained and the code base is still in a state of temporary migration from Drupal 7 style code. Content Moderation seemed to be the more accepted option however it is limited in this respect.
Comment #84
dww@willeaton: You might find State Machine suitable for your needs. It provides / supports "Guards" which are plugins you can define to prevent specific state transitions if certain conditions are not met. But we're getting a bit off topic from this issue.
I also needed something like this for a project, but this issue seems a potentially doomed in deadlock between release manager and subsystem maintainer. I'm not sure I can be more persuasive than what @Sam152 already said in #40 and #45. Yes, a mix of hooks and events is somewhat crappy DX, but so is the current situation. I'm not sure how we provide 100% BC without a mix of both unless we want to make
ContentModerationState
public API. Drupal seems to love to provide a lot of different ways to alter the same thing 😅 so I'm not totally convinced that having both Events and hooks is really all that terrible. 😂+1 to doing this. If I can find more time for a direct (and hopefully persuasive) response to every point in #39, I'll try.