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

Issue fork drupal-3120298

Command icon 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

mxr576 created an issue. See original summary.

mxr576’s picture

Status: Active » Needs review
StatusFileSize
new4.21 KB
mxr576’s picture

Issue summary: View changes
mxr576’s picture

My colleague spotted this comment in the API documentation:

Note that hooks invoked using \Drupal::moduleHandler->alter() can have multiple variations(such as hook_form_alter() and hook_form_FORM_ID_alter()). \Drupal::moduleHandler->alter() will call all such variants defined by a single module in turn. For the purposes of hook_module_implements_alter(), these variants are treated as a single hook. Thus, to ensure that your implementation of hook_form_FORM_ID_alter() is called at the right time, you will have to change the order of hook_form_alter() implementation in hook_module_implements_alter().

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?

Status: Needs review » Needs work
mxr576’s picture

Issue summary: View changes
StatusFileSize
new7.95 KB
new3.28 KB

Fixed failed tests and extended test coverage.

mxr576’s picture

Status: Needs work » Needs review
mxr576’s picture

StatusFileSize
new7.98 KB
new602 bytes

Fixing a code error.

mxr576’s picture

StatusFileSize
new9.16 KB
new1.07 KB

And the a final polish... removed the outdated docblock.

jungle’s picture

Version: 8.8.x-dev » 8.9.x-dev

Subscribing. Would be great to upload a test-only patch.

jungle’s picture

StatusFileSize
new3.78 KB

Extracted a test only patch from #9

Status: Needs review » Needs work

The last submitted patch, 11: 3120298-11-test-only.patch, failed testing. View results

mxr576’s picture

Version: 8.9.x-dev » 8.8.x-dev
Status: Needs work » Needs review

Back the needs review as the test only patch should have failed.

andyf’s picture

Possible 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 its form_alter hook, and the module implements both a hook_form_alter() and a hook_form_FORM_ID_alter(), then with HEAD that would reorder both alter hooks, whereas after the patch it would only reorder the form_alter? I guess the need to update ThemeSuggestionsAlterTest might illustrate that we're changing existing behaviour.

mxr576’s picture

I 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:

Note that hooks invoked using \Drupal::moduleHandler->alter() can have multiple variations(such as hook_form_alter() and hook_form_FORM_ID_alter()). \Drupal::moduleHandler->alter() will call all such variants defined by a single module in turn. For the purposes of hook_module_implements_alter(), these variants are treated as a single hook. Thus, to ensure that your implementation of hook_form_FORM_ID_alter() is called at the right time, you will have to change the order of hook_form_alter() implementation in hook_module_implements_alter().

and implemented both hook_form_alter() and hook_form_FORM_ID_alter() alter hooks in a module and then changed the weights of both form_alter and FORM_ID_ALTER hooks in hook_module_implements_alter() alter. The original code always ignored weights set for FORM_ID_alter-s in hook_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 in hook_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.

mxr576’s picture

andyf’s picture

Thanks @mxr576! I could be misunderstanding the patch (or the current behaviour!) but

I guess this could be only a problem for form alters if someone [snip] implemented both hook_form_alter() and hook_form_FORM_ID_alter() alter hooks in a module and then changed the weights of both form_alter and FORM_ID_ALTER hooks

Doesn't it also impact the scenario in 14?

If a module uses hook_module_implements_alter() to reorder its form_alter hook, and the module implements both a hook_form_alter() and a hook_form_FORM_ID_alter(), then with HEAD that would reorder both alter hooks, whereas after the patch it would only reorder the form_alter?

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, ie hook_form_alter(). But after this patch would that still work without modifications? Thanks!

mxr576’s picture

Yes, 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.

andyf’s picture

Status: Needs review » Needs work

Thanks for bearing with me (: I've tried testing locally and I do seem to be seeing the behaviour I described. With test1.module and test2.module both altering the node form with all three types of form alter, if test1.module has the following:

function test1_module_implements_alter(&$implementations, $hook) {
  if ($hook === 'form_alter') {
    $group = $implementations['test1'];
    unset($implementations['test1']);
    $implementations['test1'] = $group;
  }
}

Then without the patch I get:

Executed test2_form_alter()
Executed test2_form_node_form_alter()
Executed test2_form_node_article_form_alter()
Executed test1_form_alter()
Executed test1_form_node_form_alter()
Executed test1_form_node_article_form_alter()

Note that current behaviour is that we can make test1_form_node_form_alter() and test1_form_node_article_form_alter() run after the test2 form alters just by altering the weight on the base hook test1_form_alter().

And with the patch I get:

Executed test2_form_alter()
Executed test1_form_alter()
Executed test1_form_node_form_alter()
Executed test2_form_node_form_alter()
Executed test1_form_node_article_form_alter()
Executed test2_form_node_article_form_alter()

In this case test1_form_node_form_alter() and test1_form_node_article_form_alter() are run before the test2 equivalents - 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!

mxr576’s picture

:thinking-face: thanks AndyF, I will look into this

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev
dww’s picture

Issue tags: +Bug Smash Initiative

This 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

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

zengenuity’s picture

Patch needs a re-roll.

This version works on 9.4.5.

swentel’s picture

Version: 9.4.x-dev » 9.5.x-dev

Wow, 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.

andyf’s picture

Thanks @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 means

  • the general form alters are always called after the more specific;
  • you can use hook_module_implements_alter() to ensure your general form_alter is 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

  • You can't use hook_module_implements_alter() to change the weight of an "extra" hook (or "subtype", not sure of the terminology) such as hook_form_FORM_ID_alter() directly, you have to have implemented hook_form_alter() and change the weight of that.
  • You can't have a hook_form_alter() and a hook_form_FORM_ID_alter() run with different weights.

However, entity_view_alter is dispatched with the hooks in the reverse order: the more specific comes first, and the more generic comes second. This means node_view_alter will always come after entity_view_alter and furthermore, the only way to affect the order it's run in is to use hook_module_implements_alter() on node_view_alter which IIUC only works if you've actually implemented a hook_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_alter and node_view_alter hooks will run relative to others, but the overall order of node_view_alter being called after entity_view_alter will 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.)

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

donquixote’s picture

donquixote’s picture

I 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:

function ds_module_implements_alter(array &$implementations, string $hook): void {
  // Always target the the base hook, otherwise implementations will be removed.
  if ($hook === 'entity_view_alter') {
    $implementations['_ds_late'] = FALSE;
  }
}

// This will run last on ->alter(['entity_view', 'node_view']).
function _ds_late_node_view_alter(...) {...}

I am not sure if we will or should support this workaround in #3366083: [META] Hooks via attributes on service methods (hux style), though.

donquixote’s picture

Sorry, 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)

donquixote’s picture

nicxvan’s picture

dieterholvoet made their first commit to this issue’s fork.

nicxvan’s picture

I'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.

nicxvan changed the visibility of the branch 11.x to hidden.

nicxvan’s picture

Needs a rebase, but I have a clean test that shows this is still an issue.

nicxvan’s picture

Scratch 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.

nicxvan’s picture

Issue summary: View changes
Status: Needs work » Needs review

Took a pass at credit and updated the issue summary.

nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
smustgrave’s picture

Status: Needs review » Needs work

Appears to have an open thread

nicxvan’s picture

Status: Needs work » Needs review

Replied thanks!

nicxvan’s picture

Status: Needs review » Needs work

Actually I want to make it less likely that a random test hook will collide with this and some more comments

nicxvan’s picture

Status: Needs work » Needs review

Ok I made the hook more rare and added some comments, this is ready for review again.

nicxvan’s picture

Not quite sure why that new MR is here.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The 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.

nicxvan’s picture

Status: Needs work » Needs review
needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The 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.

solimanharkas made their first commit to this issue’s fork.

nicxvan’s picture

Version: 11.x-dev » main

This needs a rebase on main.

Also I'm not sure what to do with the extra mr since it is failing.