Problem/Motivation
The ordering of listeners defined by new Hook attributes does not have a modern solution.
We want to replace hook_module_implements_alter in it's current form to handle all current ordering scenarios.
- set a hook first
- set a hook last
- order a hook before another module
- order a hook after another module
- remove another module's hook
- reorder another module's hook
- reorder a hook relative to alter hooks with extra types
#3485659: Remove HookOrder HookOrder was temporarily removed because it does not quite work with extras.
#3484754: Ordering of hooks needs some attention Resolves the BC break (this issue is the followup to address point 3)
#3479141: Implement FormAlter attribute is related since it is the most common form (lol) of an alter with extra types.
Steps to reproduce
Try to order an OOP implementation of a hook. Observe there's only a very clumsy legacy mechanism.
Proposed resolution
https://git.drupalcode.org/project/drupal/-/merge_requests/11437
- Orders procedural before OOP
- Does as much ordering before adding to the container as possible
- Preserves run time ordering.
- Groups alters together
There are CRs to address each piece of hook_module_implements_alter functionality needed to handle hook ordering.
Ordering own implementation
See this CR for more complete examples: https://www.drupal.org/node/3493962
Add order parameter to the #[Hook] attribute.
The order parameter accepts the following simple ordering options.
Order::First
Order::Last
The order parameter also accepts the following complex ordering options which also accept parameters.
OrderBefore()
OrderAfter()
These can accept the following parameters:
modules: an array of modules to order before or after.
classesAndMethods: an array of arrays of classes and methods to order before or after.
Here is how it works for CKEditor5.
#[Hook('form_filter_format_form_alter',
order: new OrderAfter(
modules: ['editor', 'media']
)
)]
OrderAfter specifies this hook should run after the parameters passed.
modules: ['editor', 'media'], specifies this ordering cares about the editor and media modules.
Removal
See this CR for more details: https://www.drupal.org/node/3496786
Add #[RemoveHook()] attribute.
The #[RemoveHook()] attribute accepts details about the implementation to remove.
#[RemoveHook('help', class: LayoutBuilderHooks::class, method: 'help')]
This will remove the layout builder hook_help implementation.
Ordering other implementations
See this CR for more details: https://www.drupal.org/node/3497308
Add #[ReOrderHook()] attribute.
The #[ReOrderHook()] attribute accepts details about the implementation to reorder.
class WorkspacesHooks {
#[ReOrderHook('entity_presave',
class: ContentModerationHooks::class,
method: 'entityPresave',
order: new OrderBefore(['workspaces'])
)]
public function entityPresave()
This allows the workspaces module to move the entity_presave implementation in content_moderation before the implementation in workspaces.
Deprecation
See this CR for more details: https://www.drupal.org/node/3496788
Deprecate hook_module_implements_alter implementations without: #[LegacyModuleImplementsAlter]
Remaining tasks
Review
Here is a summary of who I have seen reviews from and the current status. Please comment if you reviewed or if I have misinterpreted your reviews.
These reviews were for the current api, but the implementation has changed.
Decide if the CR,Procedural hooks are gathered before object oriented hooks for a given module, should be published.
Authors
- nicxvan (nlighteneddesign)
- ghostofdrupalpast
- donquixote
Reviewers
The underlying approach has changed since this list was generated, but the dx and deprecation have remained the same.
- catch - High level review, frequent spot reviews in slack for questions, sign-off for including deprecation - believe all comments addressed
- larowlan - Deep review of the code, deprecation method sign-off, all comments addressed
- berdir - Architectural review, not implementation, some reservations about extraTypes on ordering, but last comment was in agreement it is needed, all comments addressed
- amateescu - Review of the Workspaces conversion, unsure if review extended beyond that
- donquixote - Deep review of the code, many comments addressed, a number were explored and not necessary. Pivoted approach.
- godotislate - Very early review, approach changed significantly since
User interface changes
N/A
Introduced terminology
Order
OrderAfter
OrderBefore
RemoveHook
ReOrderHook
API changes
A new enum and two new classes assist with ordering, these replace hook_module_implements_alter:
OrderOrderAfterOrderBefore
Two new attributes to assist with modifying other hook implementations.
#[RemoveHook()]
#[ReOrderHook()]
Do not delete hook_module_implements_alter until you drop support for Drupal VERSION instead add the #[LegacyModuleImplementsAlter] attribute to hook_module_implements_alter once the module has set up the correct ordering attributes.
Data model changes
N/A
Release notes snippet
hook_module_implements_alter can now be implemented using PHP Attributes. Removing or running before or after another module's hook implementations is much simpler thanks to new option on these attributes.
| Comment | File | Size | Author |
|---|
Issue fork drupal-3485896
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:
- 3485896-ImplementationList-object
changes, plain diff MR !11676
- 3485896-parts
changes, plain diff MR !11783
- 3485896-order-operations-by-specifity
changes, plain diff MR !11579
- 3485896-test-file-scan-order
changes, plain diff MR !11497
- 3485896-nicxvan-order-at-call-time
changes, plain diff MR !11438
- 3485896-donquixote-tests-nicxvan
changes, plain diff MR !11429
- 3485896-attempt-simplification
changes, plain diff MR !10558
- 3485896-donquixote-suggested-changes-part3
changes, plain diff MR !11415
- 3485896-donquixote-suggested-changes-part2
changes, plain diff MR !11344
- 3485896-donquixote-suggested-changes-part1
changes, plain diff MR !11343
- 3485896-deprecatehmia
changes, plain diff MR !10756
- deprecateHMIAFresh
changes, plain diff MR !11233
- deprecatehmiav7
changes, plain diff MR !11224
- deprecatehmiav6
changes, plain diff MR !10824
- 3485896-hook-ordering-across
changes, plain diff MR !10497
- 3485896-donquixote-order-at-call-time
changes, plain diff MR !11437
- test-file-scan-order
compare
- 3485896-donquixote-order-at-call-time-nicxvan-changes-BAD
compare
- 3485896-donquixote-order-at-call-time-nicxvan-changes-TBD
compare
- 3485896-donquixote-tests-nicxvan-FAIL-side-effect
compare
- 3485896-donquixote-tests-baseline
compare
- 3485896-donquixote-tests-donquixote-2
compare
- 3485896-donquixote-suggested-changes
compare
Comments
Comment #2
ghost of drupal pastComment #3
nicxvan commentedComment #5
nicxvan commentedComment #6
nicxvan commentedComment #7
nicxvan commentedComment #8
godotislateI think that introducing (or re-introducing?) 5 different attributes here to control hook order makes the scope of the issue quite large, at least the testing aspect of it.
But before that, I think it'd be helpful to define what the expectation is of how ordering should (or will?) work. I understand the goal is to replace hook_module_implements_alter in a way that works with both procedural and OOP hooks, but I think a full example with the variations of hook implementations and how exactly they can be ordered in a business sense is something missing from the IS.
Comment #9
nicxvan commentedThis issue really isn't ready for review yet, we still need the runtime BC layer, but you can see how it works in ckeditor here:
I'll add this to the IS before putting it in needs review.
There is a new approach.
#[Hook('form_filter_format_form_alter')]#[HookOrderGroup(['form_filter_format_add_form_alter', 'form_filter_format_edit_form_alter'])]
#[HookAfter(['editor', 'media'])]
We kind of have to do this all at once, we're only replacing the one implementation in this issue though.
Comment #10
larowlan.
Comment #11
nicxvan commentedComment #12
ghost of drupal past(please continue not crediting me)
Comment #13
nicxvan commentedComment #16
nicxvan commentedUpdating the IS and CR.
Comment #19
nicxvan commentedComment #20
nicxvan commentedComment #21
nicxvan commentedComment #22
ghost of drupal pastComment #23
berdirI think we should consider to convert all implementations in core of this to verify the concept. There aren't that many and then we can immediately deprecate implementations hook_module_implements_alter().
That said, there are at least two common use cases for hook_module_implements_alter(). One is covered here, it's ensuring that certain hooks are called before/after others. The second is completely replacing hook implementations. This might be more common in custom code for some advanced use cases and maybe this is already possible through a service provider or so, but we need to document that if we want to deprecate the hook.
Looking through common core and contrib implementations as well as those in my project.
core:
* content translation is a trivial first/last (I assume if two modules want to be first then it would keep the order if it would have by default between those two).
* layout_builder is also a trivial last
* layout_builder_test is a before layout_builder.
* navigation has two, page_top is a trivial last, but the second part is an implementation of the second use case, it replaces layout_builder. so that use case does in fact exist in core.
* page_cache_form_test is also a variation of that, it removes system
* common_test_module() I think is just a general test for it, if we have tests for the new system, we could keep it just for BC testing?
* module_test too is more test edge cases that I think we should just use for BC testing.
* workspaces is a fun one: It moves itself first *but* also moves content_moderation "firster". How would that look with the new system?
contrib:
* address: last
* field_group: last
* google_tag: last + remove google_analytics
* metatag: last
* monitoring: remove a requirements hook for test purposes
* paragraphs: last
* webform: last (same hook as paragraphs)
* workbench_email: last (really, after content_moderation)
And one custom implementation that removes a hook.
Looking through this, I'm wondering about the group stuff. It makes sense that we tackled this by looking at the most complex implementation and made sure that we support that. But, I think such a complex thing that's based on two different hook that we have to put in a group is very, very rare. This is literally the only implementation that I found that doesn't just put itself first or last. We could either say that such a special case needs to handle this itself by altering the service container, or much simpler: We just solve it like everyone else does it and put ckeditor5 last.
I didn't review the implementation at all, but I suspect it would remove a considerable amount of complexity if we don't need to support that group thing? Thoughts?
Comment #24
amateescu commentedWorkspaces needs the ability to implement the entity presave hook twice, one that runs first and one that runs last :) The current code that moves CM's implementation before is just a workaround.
Comment #25
nicxvan commented@berdir thanks for your review a lot of good points! I'll reply more fully once I've thought through some things.
@amateescu great to know! That would be super easy in this implementation then!
Two hook attributes on the same method, one first, one last.
Comment #26
ghost of drupal past> Two hook attributes on the same method, one first, one last.
Or , depending , one hook implementation which runs first and one that runs last -- we now have the ability to implement the same hook twice. Yay.
Removing the group stuff as it would make alter quite a bit simpler and others a bit simpler.
Comment #27
amateescu commentedYup, that's what we need for Workspaces, to split the current implementation in two separate methods.
Comment #28
nicxvan commentedEven easier, I'll reach out if I have questions.
Comment #29
nicxvan commentedI agree, I was concerned about scope, I reached out to @catch on slack and since there are only a few hooks we can convert them here except
common_testandmodule_testMinor correction here, we will not deprecate hook_module_implements_alter in the traditional sense, we will deprecate hook_module_implements_alter without the #[LegacyHook] attribute. In order to support older versions of Drupal contrib / custom will need to keep their implementation and tag it.
Good catch! I think we can handle that here as well though with a more simple
removeorreplaceparameter. (What is replace other than remove the other one and add mine? So with this you can remove theirs and on the same attribute provide the replacement#[Hook])Opinions on the name of the parameter are welcome.I think functionally it has to be remove, but semantically replace might be easier to understand as a DXIn the case that you only wanted to remove you would just put an empty implementation and replace the hook you wanted to remove.There is one caveat to this approach that could be addressed in the future followup.
Remove for a given module / hook combination would be global, meaning if a module implements a hook multiple times all would be removed if this parameter is passed.Yes, this does complicate things and was where a lot of the effort went. It is an edge case, but I would say it's worth keeping because it does solve the extra types use case. Most of the comments on the MR from catch were about explaining this so it definitely needs more comments.
Putting it last won't work because it's across extra types, so alter doesn't know how to move it. In fact I think this will solve some unique ordering issues for alter hooks.
Comment #30
nicxvan commentedAssigning to myself per the assignment policy.
I am working on addressing comments on the MR and converting the rest of core.
I will also take a look at adding the remove / replace parameter name TBD
Comment #31
berdir> Putting it last won't work because it's across extra types, so alter doesn't know how to move it. In fact I think this will solve some unique ordering issues for alter hooks.
I didn't look at the implementation yet. I see that it gets tricky with multiple types either way. I assumed we could just give a very low priority/high weight, but we don't really have that, just an array. And we always process the first hook first, so ckeditor5 would be first since it implements the base hook that's specified first.
One option would be to register the ckeditor5 hook twice, once as add and once as edit form id, instead of using the base form id. Then each could depend on the respective other and they'd be same hook?
Comment #32
nicxvan commentedI tried that, it didn't work cause it then gets called multiple times since it's new hooks.
Comment #33
nicxvan commentedOk I rebased to fix the navigation performance test.
I also converted all core implementations except common_test and module_test which we've not converted anything for.
I also did not convert module_handler_test_all1 since it is testing hmia.
I did not convert workspaces as I am waiting on: #3488093: Clean up hook implementations in the Workspaces module
Comment #34
nicxvan commentedComment #35
nicxvan commentedI'm wondering if replacements is more confusing since it is really just removing those implementations. It could be called removals.Once a consensus is reached I can update it. In the mean time I finished converting navigation and wrote tests.I also added a new CR just for this parameter.
https://www.drupal.org/node/3496786
Comment #36
nicxvan commentedOnce that workspaces issue is in I'll convert it, I will look at deprecating HMIA too here since we've converted everything but the test ones.
Comment #39
nicxvan commentedWorkspaces is in, let me convert that one too!
Comment #41
nicxvan commentedOk workspaces has been converted, I also created yet another CR for deprecating hook_module_implements_alter, I also created a new branch that I hid to add the deprecation message where I think it needs to go, I'm running that test now, if we think we should deprecate as part of this issue it's easy to move it over to the canonical branch.
Comment #42
nicxvan commentedI think I'm going to rename replacements remove.
I think longer term it will be confusing to have it called something it's not doing.
Comment #43
nicxvan commentedComment #44
nicxvan commentedComment #45
nicxvan commentedIf you would like to see what is needed on top of this change to deprecate hook_module_implements_alter:
https://git.drupalcode.org/issue/drupal-3485896/-/merge_requests/1/diffs
Comment #46
nicxvan commentedComment #47
amateescu commentedPushed a commit to fix Workspaces' presave implementation, we don't want to run the whole thing twice :)
Comment #48
nicxvan commentedThat broke Drupal\Tests\content_moderation\Kernel\WorkspacesContentModerationStateTest
Since the trick being used right now in HMIA is running the whole thing twice I'd recommend leaving this conversion as is and optimize in a follow up.
Comment #49
amateescu commentedThe current code from HMIA doesn't run the implementation twice, it moves it first then moves CM's implementation before it. I'll look at the test failure tomorrow :)
Comment #50
nicxvan commentedAh I misunderstood what you were saying here:
Comment #51
amateescu commentedI looked into it and the test failure is genuine,
\Drupal\workspaces\Hook\EntityOperations::entityPresaveLast()runs before\Drupal\content_moderation\Hook\ContentModerationHooks::entityPresave().What I found is that
ModuleHandler::invokeAllWith()executes the callbacks in a foreach keyed by module name, andModuleHandler::invokeMap['entity_presave']looks like the screenshot attached.Comment #52
nicxvan commentedAnd what does it look like with my version? Cause that passed.
Comment #53
nicxvan commentedThat should do it! I'll keep an eye on tests.
Comment #54
nicxvan commentedComment #55
nicxvan commentedOk this is ready for review again
Comment #56
nicxvan commentedCleaned up the remove hook bit and the override order bit.
Gist is there is now a RemoveHook class to facilitate removing an implementation and an OverrideHook class to facilitate ordering another implementation.
I'll update the IS, CRs and docs later tonight or over the weekend. It's still ready for review.
Comment #57
nicxvan commentedComment #58
nicxvan commentedComment #59
nicxvan commentedComment #60
nicxvan commentedComment #61
nicxvan commentedI've updated CRs and the IS.
Comment #62
ghost of drupal pastComment #63
nicxvan commentedRenamed to ReOrderHook
Comment #64
nicxvan commentedComment #65
nicxvan commentedComment #66
ghost of drupal pastComment #68
nicxvan commentedComment #69
nicxvan commentedComment #70
nicxvan commentedI think we should just deprecate HMIA here, it's only +165 -68 https://git.drupalcode.org/issue/drupal-3485896/-/merge_requests/2/diffs
Comment #71
nicxvan commentedTagging for the deprecation question since this is unique.
Comment #72
nicxvan commentedComment #73
nicxvan commentedComment #74
nicxvan commentedComment #75
nicxvan commentedComment #76
nicxvan commentedComment #77
nicxvan commentedComment #78
nicxvan commentedComment #79
nicxvan commentedComment #80
nicxvan commentedComment #81
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #82
nicxvan commentedI will rebase this if necessary but this Needs Review either way.
Comment #83
nicxvan commentedComment #84
nicxvan commentedComment #85
donquixote commentedSome feedback.
I don't like attributes that are not related to the symbol they are attached to.
#[ReOrderHook] and #[RemoveHook] can be placed anywhere with the same effect, so perhaps there should be a different way to do this?
Comment #86
nicxvan commentedWhile I agree with your sentiment in the general sense, in this specific case these two attributes are likely going to be super rare.
Further, when they are implemented, they will almost always have a specific hook that they are being enacted FOR, so there is a specific hook they are related to even if it is not the hook they are affecting. It will be extraordinarily rare for one of them to be just alone, in that case you can throw them on a hook class with an __invoke method that does nothing.
Evidence that these are rare:
RemoveHookHas one implementation in all of core.ReOrderHookAlso has one implementation in all of core.The next question of course is if it is so rare why have it, is there another way to do what they are doing?
The answer to that is they both provide a specific piece of functionality that hook_module_implements_alter provided that is crucial we maintain for the modules that need it. We spent a significant amount of time exploring options and this is the cleanest we came up with.
If you have an alternate suggestion I'm all ears.
Comment #87
nicxvan commentedI created this CR @larowlan's request: https://www.drupal.org/node/3505563
Turns out it is already covered by BC policy: https://www.drupal.org/about/core/policies/core-change-policies/bc-polic...
I have requested it get deleted.
Comment #88
berdirI've given up trying to argue that we shouldn't support the complex grouping stuff, but I still think we can at least get rid of the use case we have in core: #3505641: Simplify ckeditor5_module_implements_alter() in favor of checking for ckeditor5 in media module. It might not save much in lines of code (given that we have to keep support for such use cases) but it makes it IMHO self-contained and easier to understand.
Comment #89
nicxvan commentedNot to repeat the whole comment, but I think that change should be postponed on this since for now that is a concrete way to show how groups work. I agree long term we can remove it, but since grouping is part of the most complex bit having a concrete example makes this issue easier to grok.
Comment #90
nicxvan commentedI rebased the main branch and the deprecation.
Comment #92
nicxvan commentedComment #93
nicxvan commentedComment #95
nicxvan commentedComment #97
nicxvan commentedI recreated the deprecation branch, rebasing has become painful so here is a patch with the changes only.
I'm going to hide it, but I'm attaching it here for posterity.
Comment #99
nicxvan commentedI had missed a couple of test updates, here is an updated patch.
Comment #100
quietone commentedI deleted the CR titled, "Delete this CR".
Comment #101
nicxvan commentedThanks!
Comment #102
nicxvan commented@larowlan signed off on the deprecation method on slack so I'm removing framework manager tag. https://drupal.slack.com/archives/C079NQPQUEN/p1740180561937599?thread_t...
Leaving the release manager tag for that decision.
I'll update the IS later.
Comment #103
nicxvan commentedUpdated the IS.
Comment #104
nicxvan commented@donquixote thank you for the thorough review.
I applied several suggestions and renamed a couple of variables you pointed out were confusing.
You're not the first person commenting on the term of Group, it'd be a lot of work, but if someone comes up with a better name for hooks that ordered together, I'm all ears.
There are some questions I want to review and think about.
There were also several suggestions and reorganizations that I think I see what you were going for but I disagree if they are clearer.
Specifically renaming the nested foreach in process and gatherOrderInformation, I don't think we should change those.
Comment #105
nicxvan commentedOk I think we need to rename group and the local variable orderGroup.
I asked in slack for suggestions and as of this comment @catch, @longwave and @donquixote participated.
Suggestions were:
Since they are the extra types we are ordering against in alter, I think the best option is the following:
on the ComplexOrder object:
extraTypesAs a local variable on HookCollectorPass:orderExtraTypes.Edited:
@kristiaanvde suggested collection
Another alternative is:
relatedImplementations,orderRelated, andorderedRelatedComment #106
nicxvan commentedComment #107
nicxvan commentedOk I renamed group to
extraTypes,$orderExtraTypesand$orderedExtraTypeswhere applicable, I think that might be a lot clearer.Comment #108
nicxvan commentedComment #109
catchUnless a good reason emerges which I can't currently think of, I would also lean towards doing the deprecation/bc layer in this issue.
Comment #110
nicxvan commentedGreat thanks! I'll pull that in and then update the IS
Comment #112
nicxvan commentedOk that has been pushed up
Comment #113
nicxvan commentedComment #114
nicxvan commentedComment #115
nicxvan commentedComment #116
nicxvan commentedComment #117
donquixote commentedSome higher level thoughts.
Divergence from symfony event system
We are using the symfony event system, but then we are reinventing the way that these events are declared, how we specify ordering, and how we invoke them.
Explicit vs calculated numeric priorities
Atm, if a module uses explicit event subscriber priorities, I suspect it does not really work so well in combination with the dynamic/calculated priorities produced by the hook ordering system proposed here.
If we want to stick closer to the event system, then the default way to specify ordering would just be to specify explicit priorities. There are reasons why we might want to avoid this, because modules will come up with random numbers like -999, -5 or -1000000. But this also applies to events that we already have in Drupal, and where we are ok with just priorities.
We could also consider to order event subscribers without setting a calculated priority.
If we have 10 subscribers with prio = 0, we can just order that array and set it, while the priority remains 0 for each.
We could either do this later in the process, when the tagged services have already been evaluated and applied as a service parameter, or we rely on the order of tagged service definitions in the container. Not sure if this works.
Relative ordering
One thing the symfony event system does not cover is relative ordering.
I remember older discussions where it was said the relative ordering is not really needed.
We only had one such case in core (ckeditor5) and here it was not even really required. But perhaps contrib has such cases.
In the past, with no more than one implementation per module (for regular hooks), relative ordering just meant ordering of module names relative to each other.
Even with advanced multi-hook alter hooks, where you could already have multiple implementations per module, we would group them by module for ordering, to have something in the format we can pass to hook_module_implements_alter().
Now we want to order implementations relative to each other, which is more than what we would need if we only want to preserve the old/existing ordering functionality.
In my older MR, we would order implementations relative to module names, which completely eliminates the possibility of circular conflict.
Module name per implementation
Also we need to know the module name for each implementation, which symfony does not really help us with.
We have to store the impl -> module map in a separate place, outside the actual event subscribers list.
Order lost when grouping by module
Currently in ModuleHandler::getHookListeners(), we are grouping implementations by module:
This means that previous ordering by implementation is destroyed, if we have multiple implementations for one module.
A solution here could be to let ::getHookListeners() return an iterator, which would allow to repeat the same key.
Of course the `$this->invokeMap` would need to have a different structure in that case. Perhaps it could contain iterator closures, or IteratorAggregate objects.
Ordering conflicts
With the system proposed here, there is always the potential for ordering conflicts.
E.g. A wants to run before B, but B also wants to run before A.
This type of conflict already existed (in theory) with hook_module_implements_alter(), and it almost never was a problem in practice, so I don't see it as a blocker.
And then we can have different implementations that want to run first, in that case the original module order decides.
Again, we already know this edge case problem from hook_module_implements_alter(). But explicit priorities or weights would avoid this fully.
Alternative
I want to recap the ordering logic of my older PR, from what I remember.
This was done without the event system, but in fact it would be very possible to integrate it with the event system, similar to what we do now, but without calculated priorities.
Basically, we have different levels of ordering, similar to semantic versions:
So it would look something like this:
The `$module_implementations` would include procedural _and_ OOP implementations, and a derivative of it is passed to hook_module_implements_alter().
This allowed to convert a procedural implementation with no impact on the ordering.
(The MR looked more complex due to micro optimization.)
This would also completely solve "combined" alter hooks, because we can just combine implementations with same prio and module.
Comment #118
nicxvan commentedThank you so much for your review and this comment, you've obviously put a lot of thought into this topic over the years and this issue specifically.
At a high level, I don't see anything here that should block this issue, once this is in there is some opportunity for clean up / expansion, but this is already far broader than a typical issue due to the nature of HMIA. I truly think for this issue the thought should be, "does this allow people the ability to replace hmia and improve DX"
Most of your comments could be addressed in follow ups that are far more narrowly scoped.
I will create those follow ups shortly with some details to come later.
Yes, hooks are different so the divergence is expected
These are not subscribers, so again them being different is expected. The initial OOP hook issue did use explicit priorities and that blew up which is why it was pulled out, this issue was specifically borne out of a BC way to allow ordering relatively.
This section only applies when it goes through the legacy ordering, and HMIA can't handle multiple implementations so them getting flattened when run through legacy hmia runtime is acceptable, once all modules convert to this system there will not be an issue.
Yes, we do that.
While true, let's handle it in a follow up, multiple implementations are new, going to be rare, and ordering them as a separate entity will be even rarer. That follow up scope will be much more narrow and easier to handle. Let's not hold this up for an edge case of an edge case of a new feature.
I agree with your assessment, this is not a blocker, this can also be solved with module weights, or by using ReOrderHook which was not previously available.
I think this is pure follow up material, again I and Ghost have ideas for further clean up too, but as I've mentioned, this issue scope is already huge, and we know it works, let's not get bogged down in perfection.
Comment #119
nicxvan commentedCreated two follow ups
Comment #120
donquixote commentedI regret the inconsistency in my level of participation and commitment.
In the MR for this issue we are introducing the different Order classes, including ComplexOrder with the ->extraTypes, which would be obsolete if we use a different model / architecture for the ordering.
(I need to do a more detailed analysis of these)
We should avoid adding features and extension points that we later remove.
We don't need to do everything in one issue, but we should know in which direction we are moving.
Can you remind me what is the "initial OOP hook issue" and how it failed? Maybe I missed it.
(I assume we are not talking about my older issue here, which mostly suffered from being overly ambitious. I don't recall that anything "blew up".)
This said, I can imagine how this could have failed, depending how it is done.
My point is the more we diverge and the less we reuse from symfony events, the more we have to ask what we gain from using the symfony system, or if it would have been better to do something custom.
Maybe it is too late now.
The MR provides a way to order specific implementations relative to each other.
If we assume all implementations of one module to have the same ordering position, then all we would need is to order relative to a module, not relative to an implementation.
To me this MR is incomplete if we don't properly support multiple implementations per module. It could even reveal shortcomings in the approach.
Comment #121
nicxvan commentedThere were two very long discussions in slack relevant to this.
The first regarding naming of extra types and other variables: https://drupal.slack.com/archives/C079NQPQUEN/p1740408731650039
All variables except extraTypes have been renamed.
There is still a point of contention about extraTypes on the order object. I have suggested relatedAlters @ghost has suggested extraHooks. Since this is the second rename I will not do this unless there is more consensus. I will post more thoughts on this later.
The second discussion was around higher level feedback most recently about ordering around extraTypes and ordering multiple implementations: https://drupal.slack.com/archives/C079NQPQUEN/p1740590792125979
Before I dive into that further I want to highlight the purpose of this issue and why it is so large.
The purpose of this was to provide the ability to order hooks and deprecate hook_module_implements_alter. In order to deprecate HMIA we need to replace all of it's functionality:
That is a huge ask, especially when there is the constraint of handling this in a BC compatible way so current modules that have not converted yet can still order hooks relatively. This issue has been able to achieve that.
I want to also highlight that an inordinate amount of attention has been given to ordering relative to extraTypes (formerly orderGroups). We have to support this to deprecate HMIA because core does do this, and inevitably some module somewhere does too. However, I cannot stress enough how rare this should be. However, as complex as this is, the situation is significantly better because now it is part of a discreet object and parameter so if we need to change it in the future, we can deprecate. As part of HMIA it's all or nothing deprecation so for this stage we must provide that option. I will likely update the IS and CR later highlighting how rare this should be, and if you find yourself using extraTypes, you should take a step back and consider deeply if you really need the approach you are taking. It did take the most time since it's the most complex bit, and it has faced the most scrutiny, but we know it works with ckeditor5 and the tests, and it really should be the least used bit of this. This is also why I will hold off on renaming again unless there is wider consensus, it's a lot of thought and attention something that will almost never be used, and it is named after how it is used in ModuleHandler::alter.
The other point of contention in this thread and slack is that if a module has multiple implementations they want to order that order is lost beyond module order . However, this should be a follow up, this is a current gap in implementation, and HMIA cannot order against multiple implementations anyway. The scope of this issue is so large we should not take on pre-existing bugs that are for a feature that is brand new and will likely not be used. Again, it is an edge case of an edge case and we have a follow up already for it. Remember, the scope of this is to replace HMIA, since HMIA cannot do this, we are not adding it here.
Now to address some specific points in the previous comment:
In general I agree, however I don't think that applies here for two reasons. 1. I strongly doubt we will remove this unless altering with extra types goes away and that's not happening anytime soon. and 2. this situation is much, much better because if we do change it we can deprecate just that one piece of ordering.
The one that is in the IS is where we discuss how it blew up #3484754: Ordering of hooks needs some attention
A couple of final comments on the approach you mentioned using priority in the slack message and why it blows up. If you use priority then you have to exclude that hook implementation from hook ordering in HMIA which means there is a line in the sand you cannot legacy order hooks using priority so you force other modules to update if they need to order relative to you. With our relative ordering approach it is far less disruptive.
I didn't want to add too much here since there is already information overload, but I suspect we'll likely go in this direction #3506930: Separate hooks from events But we can't start that until this gets in.
This is out of scope for an issue already this big. This already cannot happen, it remains to be a problem, but once this is in it will be a smaller issue to fix, and I created a follow up for it.
I really think this issue is close to being ready, it has been looked at by several people in depth. We converted all core hmia implementations so we know they are covered, and we have significant test coverage. I am starting to worry about attrition for this issue, let's not let perfect be the enemy of an issue that replaces one of the most complex and problematic hooks.
Comment #122
nicxvan commentedComment #123
nicxvan commentedComment #127
nicxvan commented@donquixote did a deep dive and has a lot of suggestions. Some are substantial that I need to review and give them due consideration. First review, I think one or two might be a gap that needs to be addressed before this is merged. I think most of them are just an alternate way of doing things, not sure if they are better, worse, or equivalent without more thought.
Many are still related to typing and naming, before going through that I'd like a consensus on a couple of questions:
1. A lot of the typing is on parameters that have not changed, I think that would be out of scope for this issue, should we avoid that?I only copied over the ones that are in scope.2. Same for constructors being moved to multiline, I prefer this myself and I know coding standards allow this, but I'm not sure it is in scope here.Since we did change the constructor this is in scope and I did it.3. Suggesting renaming
ComplexOrderwithRelativeOrderorRelativeOrderBase, I'd lean towardsRelativeOrderBase.4. Rename
valuefor ordering todirectionorposition, I'd lean towardposition.If I get consensus on 3 and 4 I'm happy to make those changes.
Here is the interdiff: https://git.drupalcode.org/issue/drupal-3485896/-/merge_requests/7/diffs
Comment #128
nicxvan commentedComment #131
nicxvan commentedComment #132
donquixote commentedAnother higher-level recap.
Basic concepts
(these are just observations to verify my understanding.)
The new attributes and attribute properties allow to specify ordering information for hook implementations, or to entirely remove specific implementations.
An implementation can be first, last, or before/after specific other implementations.
For relative ordering and for removal, other implementations are addressed by hook name, class name and method name.
(Consequence: When the class or method for a hook implementation get renamed, the ordering attributes that target the old name no longer work.)
Alternatively, a relative ordering can address module names, which will target all implementations by that module.
Ordering rules from attributes are applied _after_ the implementations have been reordered with hook_module_implements_alter().
Ordering rules from attributes are applied one by one, in the order in which they were discovered, but with the order on regular Hook attributes being applied first, then the order from ReOrderHook attributes.
An ordering rule applied later will overwrite / win over a conflicting order by a previous rule.
A relative ordering rule can have "extra hooks" specified, which means that this ordering rule should be applied for ->alter() calls with multiple hooks. (more on that below)
Conflict resolution
The resolution of conflicts is quite simple:
Any "later" ordering rule will overwrite any "earlier" ordering rule.
If we want to control this further in the future, we should provide ways to prioritize the ordering rules.
(This could bring us to a similar situation as explicit priorities, but we can discuss that in the future)
Overall this basic concept is fine!
It is different from what I proposed in my older MR, but it is a very valid way to order hook implementations.
Extra hooks logic
Tbh I find it hard to follow the "extra hooks" logic.
I mean I think I understand the intention, but then I have trouble to follow what exactly is happening in the code.
I also suspect that developers who want to apply order attributes may not fully understand the consequences of adding or not adding hooks in the order extra types.
At the same time I had the suspicion that it might not behave as we expect in many scenarios.
But it is hard to point out possible flaws when I might just not correctly understand it.
This is why my earlier comments were primarily focused on docs, naming and possible refactoring.
Naming of methods and variables
(these are generic comments, for specific comments see the review)
My comments about naming may appear nitpicky and pedantic.
In general there is no "perfect" naming, but there are some mistakes we can avoid.
Overall, I like it if the same thing has the same name throughout a subsystem, and more so within the same method, and different things to have different names.
E.g. when I see something like `foreach ($priorities as $weight)` or `foreach ($trees as $forest)`, I get confused.
Of course it is much easier to point out flaws than to propose better names.
Sometimes, the difficulty to find a good name can point to a design issue, where the variable or method should not really exist in that way.
Test coverage
Actually I have not looked at all into the tests.
But I suspect we need more, given that some possible problems I found in the code did not cause test failures.
If we could isolate the calculation some more, it might become possible to cover a lot of stuff with unit tests, which are much faster than
Follow-up issues?
To me, follow-up issues should be for additional functionality, or to fix things that we left unchanged.
I don't like to plan follow-ups to change things from a current MR.
We still have some time until the next release, so we _could_ change or rewrite parts of this ordering in follow-ups, but I would prefer if we can avoid it.
Alternative: Order at call time (cached)
I can imagine one alternative that (I think) would allow to drop the "extra hooks" or "extra types" in the order object added to an attribute.
We could simply apply the ordering rules from attributes at runtime:
At container build time:
- Calculate the order for regular single hook.
- Store ordering rules per hook.
At runtime for a multi-hook alter call:
- Load and merge the implementations for each hook.
- Group by module, apply module order, call HMIA with the primary hook (as was done in older versions of Drupal).
- Undo the grouping by module, so we have a list of individual implementations, by identifier.
- Apply the ordering rules for each of the hooks from the ->alter() call.
- Cache the result.
A tricky edge case here would be if the same class + method implements multiple hooks that are invoked together.
So, if something calls `->alter(['A', 'B'], ..)`, and we have `#[Hook('A_alter'), Hook('B_alter')]` on one method, possibly with different ordering information, then the targeting with class + method would become ambiguous.
To avoid this ambiguity there could be an additional parameter in the Order object, to determine whether we order relative to A_alter or B_alter.
But, I think it would be much better to just disallow that, or to order relative to both.
This alternative will lead to a situation where we order "combined" hooks at call time, but regular hooks at container build time.
We _could_ instead just do all the ordering at call time, which would probably reduce code duplication.
Note that in the current MR we already do ordering at call time for a multi-hook, if we do not find a pre-ordered combined hook.
Comment #133
nicxvan commentedThis section is generally correct.
Yes, but when doing relative ordering you have to know about the implementation you are ordering relative to.
For the hook providing the ordering directive.
Yes, that is how it works here.
Yes, it is hard to follow.
I can almost guarantee that almost no one will ever need this, it's needed in core so we had to provide it, this solves core's usage, proven by tests. That should be the bar for acceptance for this bit of the feature.
I can also all but guarantee that anybody trying to use this will probably not need it and it will be wrong.
We can't build this feature for what might be or how people might use it, it's far, far too much of an edge case. If someone opens a bug against it I will happily chase it down and try to find a solution, but we can't make guesses in this area, if it were not for HMIA supporting something like it, we would not have built it in the first place. As it stands we do need it unfortunately.
The bar for this really should be does it work in core, can we address contrib if they need it? The truth is if there is a contrib module doing some ordering on extra types this doesn't work for they can keep their procedural implementation and we can work on solving their use case before Drupal 12.
Not going to address anything here beyond what I've said before, if other's agree we should rename anything I am happy to change to a consensus. I'm not going to rename anything further before that. See IS for current options.
There is extensive test coverage, further, we have converted all of core which has some of the most complex HMIA implementations. I'm confident we have enough tests. This is a large enough issue that there are gaps, but we cannot find them without contrib using it which requires merging this.
To be fair my core contribution experience is more limited than yours, but from what I've seen everything I've called out as a follow up falls within standard usage. I mean look at the size of this issue, it's already at least 5 times larger than average and far far more complex. There are some edge cases we know about, the sooner we get this in the sooner we can track those down. Needing to rebase a 2000 line MR to try to solve theoretical edge cases is not the way to progress.
Runtime ordering is where almost all of this complexity comes from, so I would be strongly against adding more of that in the future.
Comment #134
donquixote commentedWe are designing a public API here, and anything we create will be very hard to remove later.
We can fix things before a release, but in general we should only commit what we are ready to release.
From what I remember, ckeditor5 would work just fine with "Order::Last" instead of "Order::After".
The main reason why we would try to support this case in core instead of replacing it with "Order::After" is that there are likely more such cases in contrib, and we keep this one as a canary.
(Or maybe we are concerned about BC impact of moving this hook priority? I don't think so.)
What we don't want is add a bunch of complex code and complex extension points, and then if we are worried it might not work we say nobody will use it anyway. If we only care about one specific case in core, we better to some kind of one-off hack.
This makes me wonder: Order::First and Order::Last do not allow to specify additional hook names.
How will first and last work if alter is called with multiple hooks?
To be clear I don't have any authority and have been a bit out of the loop in the last years.
I mostly express what makes sense to me.
I welcome any opinions from others.
Comment #135
nicxvan commentedI am 100% confident in how we designed this, but as mentioned in the MR even though it is an API it is marked internal so we can tweak it. The extreme complexity is limited to only one parameter on a parameter, that gives us the ability to deprecate if we have to change it wholesale, we do not have that ability to do targeted updates now which is why this issue is so massive.
I literally tried that multiple ways, it does not work and is the whole reason we created extra_types.
The same way it does now if you only move your implementation first without doing any array searching and slicing like ckeditor5 does.
Comment #136
donquixote commented$snake_case vs $camelCase
The 11.x version of HookCollectorPass and other classes has a mix of snake and camel case in local variables and parameters, with snake case being the majority.
The MR introduces new variables that are mostly lower camel case, although in HookPriority I see new snake case variables.
I have a personal preference which has shifted over time, currently leaning towards snake case.
But this should not be relevant here.
The coding standards regarding this have been relaxed. I even saw discussions to soften the rule of not mixing within a file. But this does not mean it should be arbitrary in Drupal core.
But I also agree with @nicxvan to avoid renaming until there is broader consensus. Don't rename things just for me.
Comment #137
nicxvan commentedComment #139
donquixote commentedhttps://git.drupalcode.org/project/drupal/-/merge_requests/11344/diffs
The current version of this (commit af51ab5bb1ef81e35658ffa2dd1514d7757669c2) shows how we can calculate everything first and _then_ write to the container, in HookCollectorPass.
(It also contains some unrelated changes.)
It passes existing tests.
However, it is very possible that we just don't have enough coverage.
In that case, we should improve the tests.
To improve test coverage for different combinations, we can create a hook with a bunch of implementations with different ordering info, where each does "return __METHOD__;", or sets `$arg[] = __METHOD__` for an alter hook.
Then, instead of asserting something in $GLOBALS, we can assert the return value which gives us a list of all the methods that were called, and their order.
There are still more possible simplifications we can do.
Comment #140
donquixote commentedTo me these are not good answers that justify to "resolve" a review comment.
We are designing a new system, and we want to support BC.
All of this has to work reliably and predictably, otherwise it should not be added.
Form alters are not an edge case. HMIA is not an edge case, it has to be 100% supported until we remove it in 12.x.
Also, the time spent on an issue does not determine the mergeability.
The next steps I see for me:
- Add tests to cover more combinations.
- Further cleanup and refactoring of the code in this issue.
Comment #141
nicxvan commentedCan you rebase against the main branch so the fork mr is accurate so I can see your changes.
I reopened those two threads.
However, in the one I responded:
Maybe edge case is the wrong word, it's more that once this ordering is the only way that clause in alter won't be needed so we don't have to fall back to legacy ordering. I closed it because of the comment about legacy ordering.
I would still consider that answered.
The other one I'm not sure is an issue either, the last one should win, just like with hmia now.
It does, and far more important it does something much better than hmia which is it is easier to understand how to implement your hook's ordering. It doesn't have to be perfect we can't achieve that in a world where we need to be bc with hmia.
I never said they were, but ordering across multiple layers of form alter very much is an edge case.
Same for HMIA, it runs first for build time, but if we detect something is missing it runs runtime. This is not treating it like an edge case.
If I could cut that out of this issue I would because 90% of all comments outside of typing and renaming are about extra types, and one hook uses extra types, but it is part of core so we need to keep it.
If you can find a contrib module doing this type of ordering using extra types I will gladly convert it to test this further.
Until we find other uses for it, this is all speculative. Modules can use legacy ordering and file an issue if we didn't meet their edge case, it's not the end of the world. Beyond the follow ups I created I don't know of any gaps.
Also, we solved a few issues since oop hooks came out that we missed, many were addressed as part of the conversion before release, but one or two were fixed after, all of them had a solution.
Feel free to add more tests in your branch we have many tests and we have tons of implicit tests with the hmia conversions we did, anything that tests any of the modules we converted can provide some coverage.
Feel free to experiment with refactoring too, but most of your refactoring I've seen are internal implementation details that would be far easier to get in and scope once this is in.
Other than your comment about module handler add I have not seen an architectural question you've raised we had not already considered which is one of the reasons I'm so confident. I'm not worried about module handler add because it has far deeper architectural issues and it's on its way out and this works just as well as it does in 11.1
Comment #142
nicxvan commentedI took a look at your branch and just reviewed HCP directly.
If I read it right you can remove Hook priority entirely right? I'd have to take some more time to look at it, but it looks interesting.
I still think it's far more efficient to store legacyImplementationMap and remove the one or two that need to be removed rather than loop over all thousands of hooks just to build it after, but I wouldn't block the issue on that.
Just to make sure I am reviewing everything, I don't think you made any substantial changes beyond what you changed in HCP and the really minor optimization in ModuleHandler:alter right?
Comment #143
donquixote commentedHi
Just a quick clarification.
Yes, we can remove HookPriority class, I just did not get to it yet.
The main change here was to change the algorithm within HookCollectorPass.
I wanted less interaction with the container, and only deal with priorities once everything is finished.
Once this is complete, we can run the main part of the sorting logic in a clean unit test without too much mocking.
I did try to keep the behavior of the existing MR.
This was successful as far as existing tests, _but_ as mentioned, it could be that additional tests will reveal functional changes.
In this version, we cannot remove the $order->extraHooks / ->extraTypes.
In a future version (which I think we can still do in this issue), we can try sorting at call time, which would allow us to get rid of $order->extraTypes.
(I say in this issue, because I much prefer not add something than to add and then remove.)
Comment #144
donquixote commentedWhat do we actually gain from this?
Worst case is that a ReflectionClass is created again, after it was already created in symfony.
I suspect the main cost of "new ReflectionClass" is just to load and parse the class file. Once the class is known to php, it will be very fast.
See also https://gist.github.com/mindplay-dk/3359812
symfony ContainerBuilder::getReflectionClass() actually does some crazy magic that we don't need, so it might actually be slower than just creating the reflection class again.
It definitely increases complexity and mysteriousness.
I would like to remove it to reduce the places that get passed a container.
Comment #145
nicxvan commentedI don't have a super strong opinion on this bit, it was more that we already have the reflection class so why not use it.
I know that @godotislate is experimenting with updating this elsewhere too.
Comment #146
godotislateIt was suggested as a possible performance enhancement on #3395260: Investigate possibilities to parse attributes without reflection #16. I have not done any metrics on it.
Comment #147
donquixote commentedLet's keep this as follow-up, with actual measurements.
As said, I suspect the main cost is loading the file. After that, I suspect we can call "new ReflectionClass" and "$reflector->getAttributes()" repeatedly without much extra cost.
Atm I think we should drop everything that smells like additional complexity.
In this case, dropping that symfony reflection cache allows to do most of the calculations without a container.
Comment #148
nicxvan commentedNo objection from me to remove that on your branch, let me know when it's ready for a deeper look.
Comment #150
donquixote commentedThe third MR extracts a new class HookCollector from HookCollectorPass which now does most of the logic and is mostly independent from the container.
https://git.drupalcode.org/project/drupal/-/merge_requests/11415/diffs
If you want a cleaner diff, look at v2 instead:
https://git.drupalcode.org/project/drupal/-/merge_requests/11344/diffs
I would still like to do more changes:
- I think we should use $snake_case for all new local variables and non-promoted parameters. But for now I want to reduce disruption. We can do this once we agree.
- I would like to rename more variables, but again want to reduce disruption.
- More tests.
- Order and alter hook implementations at call time, especially for multi hook alter calls.
Comment #151
donquixote commentedSome thoughts about relative ordering
In hook_module_implements_alter(), relative ordering always means to order one module relative to another module.
With the new code, we can order specific implementations relative to other specific implementations, OR to all implementations of a module.
As mentioned earlier, ordering relative to a specific implementation easily breaks when that implementation gets renamed.
Ordering relative to all implementations of a module might have unintended effects if one of these as a "first" or "last" order applied.
In my older proposal, there was the possibility to order relative to a module's default odering position.
So, even if the implementations of that module get moved around with first, last, before or after ordering, we still only order relative to the original position.
So, let's say:
- We have modules A, B, C, D, in that order.
- We have hook implementations A1, B1, B2, B3, C1, D1.
- We have ordering info "B1 first" and "D1 before B*".
With the current proposal we get D1, B1, A1, B2, B3, C1.
If "before B*" is changed to "before the default position of B", we get B1, A1, D1, B2, B3, C1.
We can even make this work if none of the B* implementations remain in the default position.
Another way to approach this is to define an order of ordering operations.
E.g. we could apply all relative ordering (before/after) before all absolute ordering (first/last). But this only gets us so far.
Comment #152
nicxvan commentedI think we're getting somewhere we may be able to agree on an approach hopefully. Should we take this out of needs review for a bit?
Too late for that lol.
Honestly I don't really have a strong opinion of snake case vs camel.
Things seem to be trending toward camel so I generally lean towards camel overall. In an issue of this size where we are talking about a whole new approach this feels like noise and we can address this when we settle on which approach.
Sounds good.
I still feel like this would be a mistake. If you want predictability we want as much ordering during build as possible. This will also be more performant.
Yes, this is a limitation, but in the interest of scope we should try to minimize changes. This limitation already exists for hmia.
Feels out of scope and something that could be added later.
Out of curiosity why would you want this?
Same scope concerns and as you pointed out it's just punts down the road.
First impression on the further changes.
I was on the fence about splitting hook collector out, but honestly pass is getting huge. I wonder if we have more files, one that does collection, one with parsing and ordering and we leave the registration in hcp?
Comment #154
donquixote commentedI created new branches with tests.
For now I created an MR only for one of them, for tests that include the 3485896-attempt-simplification and do not include my own changes.
In general I think this is direction that these tests should take:
We call a hook or an alter hook, we get a list of called implementations as from __FUNCTION__ or __METHOD__, and we compare that to the expected order.
The test has some additional magic where we want to check that `->alter(['a', 'b', 'unknown'], ...)` produces the same call list as `->alter(['a', 'b'])`.
Currently this is more like a proof of concept, we definitely don't want to merge as-is.
The tests confirm my suspicion:
An order operation with "extra" types has side effects beyond that one implementation being reordered.
We could probably craft something even more clear to confirm this.
The tests would become a lot more interesting if we fix the grouping by module problem in ModuleHandler.
I know this is currently planned as follow-up, but we might actually want to do this first.
Comment #155
donquixote commentedDone.
I am undecided about how exactly to split the classes.
For now I just wanted to see that we _can_ split this up, so that ModuleHandler no longer has to deal with a compiler pass directly.
A separate HookCollector feels like a more generic class to use in different places.
Currently we still have lots of stuff in that class. So indeed we might want to move some of it back into HookCollectorPass, anything that deals with the container.
But, any decision we take now about the final shape of HookCollector might become obsolete with further changes.
Comment #156
donquixote commentedNew branch to prove the side effect:
https://git.drupalcode.org/issue/drupal-3485896/-/compare/11.x...3485896...
This commit:
https://git.drupalcode.org/issue/drupal-3485896/-/commit/404bc461f0d3fa5...
So we add a #[ReOrderHook] that targets a non-existing implementation in a non-existing module.
This should have no effect on the order of the other implementations.
But the extraTypes messes things up.
We see the first fail, but I suspect there is more, including possibly implementations disappearing.
Comment #157
nicxvan commentedWe can check that the implementation exists before adding to the order group.
We had considered that so i want to look deeper.
What mechanism are you proposing that does that?
Comment #158
donquixote commentedThis is missing the point.
I just pushed another version of the side effect test, which is using existing implementations.
The "extra types" mechanism is just fundamentally flawed.
For an implementation to ask "I want to be ordered together with X" inevitably has an effect on other implementations and other groupings that can exist at call time. This can be mitigated to some extent, but the basic flaw is always present.
The correct thing is to order at call time.
Otherwise we would have to predict every possible combination of hooks that can be passed to alter().
Comment #159
donquixote commentedTo explain it differently:
The sorting done in one specific group of hooks can only be used when ->alter() is called with those exact hook names.
But this is not what the MR is doing.
Instead, when you call ->alter() with hooks/types ['a', 'b', 'c'], you might get the pre-ordered list from ['b', 'c', 'd'] or from ['b', 'x'], which gives you not only the wrong order, but can also give you the wrong implementations. Even if we correct the list of implementations at call time, by adding and removing implementations, the order would still be wrong.
There is no way to fix this within the current proposal, because naturally there won't always be a pre-ordered list for the current combination.
Comment #160
nicxvan commentedI do not think that's true, but let me work it out, thanks for the tests!
Comment #162
donquixote commentedI created a separate MR with call time ordering.
Still a bit rough, but this is what I have for now.
Comment #164
nicxvan commentedCreated one off yours to fix the first stage just so we can see the full test suite.
I'll take a look at your tests on my branch in a bit.
Comment #165
donquixote commentedI found that objects in a container service parameter definition are no good, it prevents dumping of the container. So I changed it to serialize them explicitly.
Now we have HookOrderTest failing in the pipeline but not in my local.
The reason seems to be that RecursiveDirectoryIterator / DirectoryIterator does not produce a predictable order.
https://stackoverflow.com/questions/29102983/order-in-filesystemiterator
And the way we look for hooks, we mix procedural and oop in the same scan operation.
This means it is not clear whether OOP hooks are first or procedural hooks are first, if from the same module.
Comment #173
nicxvan commentedWhy would this matter?
We key them by class hook and method.
Collection order should not have an effect.
Your tests look like they are failing due to deprecations.
I hid most of the branches.
Comment #174
donquixote commented#[LegacyModuleImplementsAlter] problem
We found one problem with #[LegacyModuleImplementsAlter], or the idea behind it.
The idea was that a contrib module can support Drupal 11.1.x and Drupal 11.2.x+ by:
Problem:
In 11.1.x, the hook attributes will be discovered, but then the `order: Order::First` part will blow up because:
- The enum for Order::First does not exist.
- The named parameter `order: ` does not exist.
Comment #175
donquixote commentedIt matters because if a module has multiple implementations, their order can be different on different system.
We key by class and method, but we don't sort.
Also, if we would sort by class, we would get a weird position for procedural based on the alphabetic order of ProceduralCall class.
Instead, we should decide whether procedural implementations should be always before or always after oop implementations from the same module (if no other order specified).
Comment #176
donquixote commentedThe solution could be a polyfill in a separate package outside of Drupal core.
Comment #177
nicxvan commentedThere is no issue with:
#[LegacyModuleImplementsAlter]
#[RemoveHook]
#[ReOrderHook]
They are just ignored.
The issue may be with the new enum and ComplexOrder object in the new order parameter for hook, I think it does get loaded so if contrib converts to the new ordering then is loaded on Drupal 11.1 then those do not exist so there is a fatal error. (The fact that the parameter is not parser does not matter because those missiles will have kept hmia around for ordering.)
I need to confirm this and look at a solution.
I will also look at the extra type tests you wrote.
Comment #178
donquixote commentedCorrect, technically these work as designed.
The problem is not with the #[LegacyModuleImplementsAlter] attribute but the greater purpose behind it.
Of course technically the problem is with the new order property in #[Hook].
We want modules to be able to use the new order property of the #[Hook] attribute, while at the same time keeping HMIA around to support older Drupal versions.
This will cause problems as described above in 11.1.x.
What these modules could do is declare compatibility with 10.x and 11.2.x but not with 11.1.x.
Comment #179
donquixote commentedWe could actually create a separate issue where we add a part of HookOrderTest for HMIA as baseline, to be merged _before_ this one.
This will make it more visible if or how the ordering changes.
Comment #180
donquixote commentedSome people in chat were concerned about serialization of order operations, I think mostly for security.
I am not strongly attached to serialize/unserialize, but I think the security should be ok if we use a hard-coded list of very simple classes for 'allowed_classes'.
From https://www.php.net/manual/en/function.unserialize.php:
In this case the data is not "untrusted".
But, even if it was, what exactly can go wrong if the classes we allow are as simple as here?
I would like to know.
(and don't tell me "if the class has a wakeup method...". the specific classes we are looking at don't have it.)
Comment #181
nicxvan commentedWe raised three specific concerns about that piece:
Speed, yes this is only for alters, but form alters can be called many times on a page.
Size/memory, in the beginning there are not many, but this api is much easier than HMIA so adoption will likely go up.
Security, you're hand waving this, but there are escalation attacks related to these functions and classes. why not avoid this concern entirely?
Looking at how you're using it is is so you can just call the
->applymethod right?Comment #182
nicxvan commentedComment #183
nicxvan commentedI think we will be consolidating on the new approach shortly. There is still some refinement to happen, but before I hide the attempt simplification branch and update the CRs and IS to remove mentions of extra_types I wanted to give some notice.
Comment #191
nicxvan commentedComment #192
nicxvan commentedOk I've officially updated the IS and CRs in order to get behind the new approach, we need to polish the approach.
Comment #195
nicxvan commentedI think this might be ready for review again!
The actual proposal did not change. The underlying implementation changed.
If you reviewed this before the changes are in HookCollectorPass, ModuleHandler and the other classes in core/lib/Drupal/Core/Hook
There are many more tests too.
Highlights
Comment #196
nicxvan commentedComment #199
donquixote commentedI am proposing this change to avoid relying on two synced arrays in ModuleHandler:
https://git.drupalcode.org/project/drupal/-/merge_requests/11676/diffs?c...
Comment #200
quietone commentedI started to review the comments here and have not finished. The main conclusion I have reached is that documentation for this should be in module.api.php. Then, the many references to the change record(s) should direct to the relevant section in module.api.php.
Comment #201
nicxvan commentedThank you for your review @quietone!
Where in module.api.php would we put this documentation? This is related to hooks, but it's more about ordering than defining a hook so I'm not sure where it would go.
I think most of your other comments make sense, we've replied to a few.
Regarding #199, we chatted in slack and agreed this issue is now stable. We'll manage any review feedback, but hold off on further refactors beyond that.
I think this is ready for full review and we will work on integrating the feedback @quietone provided so far.
Comment #202
quietone commentedThanks for the explanations and the prompt updates. Also kudos to @nicxvan for understanding my intent despite my typos.
I focused on just resolving comments and ignoring minor problems that don't effect meaning. Next, for me, is to think about adding this to module.api.php.
Comment #205
donquixote commentedI created a branch 3485896-parts and MR https://git.drupalcode.org/project/drupal/-/merge_requests/11783
This has a cleaned-up incremental history, which should make it easier to review step by step.
Ideally, if I did things correctly, each commit should have passing tests. But I have not really tried this, and I don't want to spam the pipelines.
We do lose original authorship.
In the end this is meant to help with reviews, the other branch is still the main review target.
Comment #208
nicxvan commentedComment #209
quietone commentedThe 'Defining and 'Invoking' sections have moved to the bottom. I think the section order in core.api.php should be this so the more 'advances' uses are last.
Comment #210
nicxvan commentedThat makes more sense, I've pushed that up!
Comment #211
nicxvan commentedI think this issue might finally be ready!
My recent commits have all been documentation and test organization related based on direct feedback.
So while I've been heavily involved in this isue I think I can RTBC for the following reasons.
The functional pieces I have written have long been reviewed and approved.
This issue has had significant feedback and it has all been addressed: (210 comments on the issue and 849! on the various MRs)
The developer facing pieces such as api, scope and deprecations have been reviewed and approved between
@larowlanand@catch.The sticking point for those reviews and many others were the extra types bit in the order before and after.
The underlying implementation in modulehandler and hookcollectorpass was rewritten by
@donquixoteto remove the extra types concern entirely and close the gaps introduced by extra types.@ghostofdrupalpastand I have both provided deep reviews of those rewrites.@donquixotealso provided additional testing beyond the tests reviewed in the initial approach.@quietoneprovided a full review of comments and api docs.@godotislateand@berdircontributed to feedback on early implementations.@amateescureviewed the conversion for workspaces and identified a gap in the implementation that was fixed.All in all, I think all feedback and concerns have been resolved.
The pieces I wrote have been reviewed by several others, the pieces I have not written have been reviewed by myself along with others.
I think this is ready, if I'm incorrect in my assessment, please let me know.
Comment #212
quietone commentedI asked @nicxvan for some clarification about the CR, Procedural hooks are gathered before object oriented hooks for a given module, in Slack. They said that this was for an edge case, so the CR may not be needed. I lean to not publishing it as well. We want to keep the CRs relevant and not create too many that they are ignored.
I added an item to the remaining tasks to consider not publishing that one.
Comment #213
donquixote commentedWhether this is an edge case is in the eye of the beholder.
It applies when a module implements the same hook twice, one time as a function and another time as a method, and without using the #[LegacyHook] attribute. So it would already be doing something deprecated.
Comment #214
nicxvan commentedNot quite, procedural hooks are not deprecated yet.
But I struggle to see the use case where one would have both an oop and procedural for the same hook in the same module.
Comment #215
donquixote commentedOh, you are right.
Quite simple, you have the procedural implementation which has not been migrated yet, then you add the OOP implementation which may be needed to cover a different problem.
E.g. you might have a mymodule_node_update() and then you add a \Drupal\mymodule\Hook\MyHook::nodeUpdate() to do something unrelated. You don't want to touch mymodule_node_update() because you were asked (and paid) to work on a very specific problem, and changing existing code is out of scope.
In most cases the order of these two won't matter, if they are for unrelated problems. But you could have funky side effects that depend on the order.
Comment #216
nicxvan commentedGood point! That makes me lean towards publishing it.
Comment #217
catchI read through the CRs, which all look great.
I then read through all of the code changes except the test coverage. It is pretty dense, so I am not confident that I would have spotted many issues, but I didn't spot any big ones, just a few comment nits and one or two small questions. I'm still behind on issue comments, semi-deliberately to read the MR with less context.
Everything looks really well organised and well documented, there's just quite a lot of it. However, even though it's adding quite a lot, it hugely simplifies the actual ordering implementations for modules and removes (well, soft deprecates) one of Drupal's biggest hacks.
Given the amount of review/iteration this has had, I'm fairly happy that I'm not spotting issues because they aren't there, rather than failing to spot issues that are, but it's going to take another read through to feel even a bit more on top of it. This shouldn't stop any other committers from having a look and/or committing if they're happy.
Comment #218
larowlanLeft comments on the MR.
The visibility of a couple of the new methods are commit blocking but many others are personal preferences
Very close here.
Comment #219
nicxvan commentedHad a chat with @larowlan in slack.
I pushed the two scope changes he mentioned were commit blocking and created a follow up for adding a test for using both modules and identifies here: #3521450: [pp-1] Add tests for passing both identifiers and modules to order against.
Not quite sure if I can self rtbc even if it was mentioned nothing else is blocking.
Comment #220
godotislate@nicxvan @larowlan Can you confirm that the only "commit-blocking" changes are for the two methods becoming
protectedinstead ofpublic?Oh, never mind, saw #218.
That being said, the validatable config job is showing a warning. Not sure if anything needs to be looked at there.
Comment #221
nicxvan commentedThe validatable config job can be ignored, it fails on pretty much every issue and we're not changing config here.
I think it only matters on issues changing config validate which this is not.
Comment #222
godotislateTried rerunning the job, got the same warning.
I also tried rebasing, but got this: "Rebase failed: Rebase locally, resolve all conflicts, then push the branch. Try again."
Comment #223
nicxvan commentedI will rebase now.
Comment #224
nicxvan commentedOk I rebased, I also added a test case for #3521450: [pp-1] Add tests for passing both identifiers and modules to order against.
Comment #225
godotislatePHPCS errors now.
Comment #226
nicxvan commentedI already got them :D, the weird thing is I didn't change those files in this push, not sure why PHPCS missed those comments previously.
Comment #227
godotislate#3521450: [pp-1] Add tests for passing both identifiers and modules to order against. says tests are needed for both OrderAfter and OrderBefore, but I see only a test for OrderAfter added here. Is the OrderBefore test still needed?
Comment #228
nicxvan commentedThe underlying functionality is the same, we're really testing core/lib/Drupal/Core/Hook/OrderOperation/BeforeOrAfter.php
The test I added tests the relevant pathway, we don't need to duplicate the test for both.
Comment #229
godotislatelgtm
Comment #231
larowlanCommitted to 11.x and published change records.
Added a release note snippet.
Comment #233
mondrakeThere's a small problem with mixing test annotations and attributes on the same level, here. Impacting #3497431: Deprecate TestDiscovery test file scanning, use PHPUnit API instead. Opened #3521815: Fix test annotations for tests introduced by #3485896 for a fix.
Comment #234
mondrakefrom https://docs.phpunit.de/en/10.5/attributes.html:
Comment #235
larowlanThanks @mondrake
Comment #237
mxr576opened this follow up today