Problem/Motivation
When triaging extension system issues I realized 11.2 fixed this without explicit test coverage.
Original Problem
The module handler does not respect the configured weight for extra hooks. I bumped into this issue when I tried to increase the weight of MY_MODULE in MY_MODULE_module_implements_alter() and run a hook_form_user_register_form_alter() and hook_form_user_form_alter() implementation after the ZZZ module's implementations. MY_MODULE's hooks were always run before the ZZZ module's hook implementations, although they should have been the last ones. (Your first guess would be that it caused by the alphabetical order, and you would not be wrong...)
This problem caused by the current impementation in \Drupal\Core\Extension\ModuleHandler::alter() which I beleive is overly complicated. In case of the described problem, it collects all base hook (hook_form_alter()) and all extra hook (hook_form_FORM_ID_alter() ) implementations, properly ordered by weights set by hook_module_implements_alter() implementations, then threw those ordered lists away and re-order the list alphabethically by doing this: $modules = array_intersect(array_keys($this->moduleList), array_merge($modules, $extra_modules));, then it calls hook_module_implements_alter()-s again only for the base hook (form) and re-orders the list. If your module's hook_module_implements_alter() does not change the weight of form_alter 's too - and for that the MY_MODULE_form_alter() function must exists, otherwise you get a RuntimeException - then your module's form id alters are not going to be called in the correct order.
Steps to reproduce
N/A
Proposed resolution
Add test ordering alters with no base hook.
Remaining tasks
Review
User interface changes
N/A
Introduced terminology
N/A
API changes
N/A
Data model changes
N/A
Release notes snippet
N/A
| Comment | File | Size | Author |
|---|
Issue fork drupal-3120298
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mxr576Comment #3
mxr576Comment #4
mxr576My colleague spotted this comment in the API documentation:
Based on the original comments in the
alter()method I do not see why this limitation would exist, I rather think that this note added to the API documentation because someone bumped into the same issue as I did and rather documented it as a limitation.Also, if this limitation exists, how can I achieve FORM_ID_A alter hook should always run first and FORM_ID_B alter hook should always run last defined in MY MODULE?
Comment #6
mxr576Fixed failed tests and extended test coverage.
Comment #7
mxr576Comment #8
mxr576Fixing a code error.
Comment #9
mxr576And the a final polish... removed the outdated docblock.
Comment #10
jungleSubscribing. Would be great to upload a test-only patch.
Comment #11
jungleExtracted a test only patch from #9
Comment #13
mxr576Back the needs review as the test only patch should have failed.
Comment #14
andyf commentedPossible duplicate of #2917192: hook_form_FORM_ID_alter , hook_form_alter : unexpected execution order of form alters hooks? I totally get that you might want to reorder form alters individually and I could be reading it wrong, but is this introducing a BC-breaking change? If a module uses
hook_module_implements_alter()to reorder itsform_alterhook, and the module implements both ahook_form_alter()and ahook_form_FORM_ID_alter(), then with HEAD that would reorder both alter hooks, whereas after the patch it would only reorder theform_alter? I guess the need to updateThemeSuggestionsAlterTestmight illustrate that we're changing existing behaviour.Comment #15
mxr576I am not sure about the theme suggestions alter part, I was surprised that had to be changed too, but I guess this could be only a problem for form alters if someone did not see this note in the API documentation:
and implemented both
hook_form_alter()andhook_form_FORM_ID_alter()alter hooks in a module and then changed the weights of both form_alter and FORM_ID_ALTER hooks inhook_module_implements_alter()alter. The original code always ignored weights set for FORM_ID_alter-s inhook_module_implements_alter(), BUT if the weight for form_alter was set it could make that false assumption that the weight was also changed for FORM_ID_alter-s too, but it did not. After this patch, FORM_ID_alter weight's are actually gets used by the system. But I guess (again) nobody changed weights of FORM_ID_alter hooks before inhook_module_implements_alter()because that never worked, only the module's name mattered in the order of FORM_ID_alter function calls.It is not an easy topic to decide if it is going to be a breaking change or not.
Comment #16
mxr576Comment #17
andyf commentedThanks @mxr576! I could be misunderstanding the patch (or the current behaviour!) but
Doesn't it also impact the scenario in 14?
ie as I understand it, currently the correct way to set the order of
hook_form_MYFORM_alter()(or any other "extra" hook) is by setting the order of the base hook, iehook_form_alter(). But after this patch would that still work without modifications? Thanks!Comment #18
mxr576Yes, it is still going to work, first the FORM_ID specific alter gets called, then the BASE_FORM alter and the last one is the generic FORM_ALTER but the order of these can still changed in
hook_module_implements_alter(), this is what the enhanced test also demonstrates.The same rule applies to other hooks alters, always the more specific alter implementation is the first in the call chain.
Comment #19
andyf commentedThanks for bearing with me (: I've tried testing locally and I do seem to be seeing the behaviour I described. With
test1.moduleandtest2.moduleboth altering the node form with all three types of form alter, iftest1.modulehas the following:Then without the patch I get:
Note that current behaviour is that we can make
test1_form_node_form_alter()andtest1_form_node_article_form_alter()run after thetest2form alters just by altering the weight on the base hooktest1_form_alter().And with the patch I get:
In this case
test1_form_node_form_alter()andtest1_form_node_article_form_alter()are run before thetest2equivalents - I think this makes it a BC-breaking change?Separately from that, I think the patch also introduces the desired order described in #2917192-11: hook_form_FORM_ID_alter , hook_form_alter : unexpected execution order of form alters hooks - I'm not sure if that's considered breaking BC or not, but certainly worth highlighting it imho.
Thanks!
Comment #20
mxr576:thinking-face: thanks AndyF, I will look into this
Comment #24
dwwThis is currently one of the random major bug issue targets for the Bug Smash Initiative.
Hope to have a chance to dive into this sometime soon and see if I can help move it along...
Thanks,
-Derek
Comment #26
zengenuity commentedPatch needs a re-roll.
This version works on 9.4.5.
Comment #27
swentel commentedWow, it seems like I've been bitten by this one while getting back field group support in DS in a new branch where I'm refactoring (see https://www.drupal.org/project/ds/issues/3308854#comment-14716099)
When manually testing, moving 'ds' to the end (to end up after field_group), it all works out fine.
However, in the test (FieldGroupTest), the order of the 'entity_view_alter' hook remains ds first, then field_group.
Even when I mimic standard install (with history. comment + layout_builder and no test modules) in the test, ds still remains before field_group.
The only 'clean' workaround I can think off for the test is to swap out EntityViewBuilder and only call alter on entity_view and not ['node_view', 'entity_view']. However, swapping that class in DS is a no go as that would break so many sites out there. I currently solved it by calling field_group_entity_view_alter in DS so the field groups are attached. It's wrapped in a Settings::get call which returns FALSE by default, which the test overrides.
I'm very tempted to mark this critical from a DX perspective, but I'll leave it for now because I'm not even 100% sure it this is the bug I'm facing.
Comment #28
andyf commentedThanks @swentel, I think that's an important example that's actually more involved than the form altering case. With forms, the more general hook (
form_alter) is passed first in the list of hooks to the call to alter, which meanshook_module_implements_alter()to ensure your generalform_alteris called later/after another.Between those two, it's always possible to have final say on any form. The problems with form alters (IIUC) are
hook_module_implements_alter()to change the weight of an "extra" hook (or "subtype", not sure of the terminology) such ashook_form_FORM_ID_alter()directly, you have to have implementedhook_form_alter()and change the weight of that.hook_form_alter()and ahook_form_FORM_ID_alter()run with different weights.However,
entity_view_alteris dispatched with the hooks in the reverse order: the more specific comes first, and the more generic comes second. This meansnode_view_alterwill always come afterentity_view_alterand furthermore, the only way to affect the order it's run in is to usehook_module_implements_alter()onnode_view_alterwhich IIUC only works if you've actually implemented ahook_node_view_alter, so it's completely unworkable for an entity-agnostic solution.I'm not sure the patch as it stands will help though. It will let you control when both your
entity_view_alterandnode_view_alterhooks will run relative to others, but the overall order ofnode_view_alterbeing called afterentity_view_alterwill remain the same. Looking at other examples of multiple hooks being passed to an alter, it looks like the majority put the more general hook first:['block_build', "block_build_" . $plugin->getBaseId()]['block_view', "block_view_$base_id"]['query', 'query_' . $tag]["plugin_filter_{$type}", "plugin_filter_{$type}__{$consumer}"]['form', "form_$base_form_id", "form_$form_id"]['theme_suggestions', 'theme_suggestions_' . $base_theme_hook]['field_widget_complete_form', 'field_widget_complete_' . $this->getPluginId() . '_form']['field_widget_single_element_form', 'field_widget_single_element_' . $this->getPluginId() . '_form']and the others:
["language_fallback_candidates_$operation", 'language_fallback_candidates'][$entityType . '_build_defaults', 'entity_build_defaults']["{$entity_type_id}_view", 'entity_view']I wonder if those last three should have the order switched? (Tho again sadly a BC break IIUC.)
Comment #31
donquixote commentedLet's add related tests here, #3369958: Improve unit test coverage for ModuleHandler
Comment #32
donquixote commentedI believe changing this as proposed would make some happy and others unhappy.
The impact will become more clear with the new order tests added in #3369958: Improve unit test coverage for ModuleHandler.
I suggest to keep the functionality as it is today, and wait for #3368812: Hux-style hooks, proof of concept which will allow to specify weights for individual implementations.
----
For people who still need this, there _is_ a workaround!
You can invent a fake module, and use hook_module_implements_alter() to insert an implementation on behalf of that fake module.
E.g. Display Suite (ds) can invent a fake module '_ds_late', for hooks that should run late.
Then do this:
I am not sure if we will or should support this workaround in #3366083: [META] Hooks via attributes on service methods (hux style), though.
Comment #33
donquixote commentedSorry, links in my previous posts were wrong.
Correct link is #3369958: Improve unit test coverage for ModuleHandler
(I already corrected, but this is for people who only read the emails)
Comment #34
donquixote commentedComment #35
nicxvan commentedIs this resolved in #3442009: OOP hooks using attributes and event dispatcher ?
Comment #37
nicxvan commentedI'm pretty sure this is now resolved now that #3485896: Hook ordering across OOP, procedural and with extra types i.e replace hook_module_implements_alter is in.
Maybe we just add a test here.
Comment #40
nicxvan commentedNeeds a rebase, but I have a clean test that shows this is still an issue.
Comment #41
nicxvan commentedScratch that this was fixed in 11.2 but it will be good to have explicit test coverage.
I'll clean up the issue summary etc later.
Comment #42
nicxvan commentedTook a pass at credit and updated the issue summary.
Comment #43
nicxvan commentedComment #44
nicxvan commentedComment #45
nicxvan commentedComment #46
smustgrave commentedAppears to have an open thread
Comment #47
nicxvan commentedReplied thanks!
Comment #48
nicxvan commentedActually I want to make it less likely that a random test hook will collide with this and some more comments
Comment #49
nicxvan commentedOk I made the hook more rare and added some comments, this is ready for review again.
Comment #51
nicxvan commentedNot quite sure why that new MR is here.
Comment #52
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 #53
nicxvan commentedComment #54
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 #56
nicxvan commentedThis needs a rebase on main.
Also I'm not sure what to do with the extra mr since it is failing.