Problem/Motivation
Currently, across core and contrib there are a number of modules and issues that attempt to override the menu link tree. However lacking a core way to alter this behavior these overrides conflict with each other. The issue summary of #2854013 outlines some of these existing conflicts.
A quick look through the issue queue found these additional use cases:
- Add classes and attributes
- Add conditional query parameters to a link (ex. ?destination=<current_route>)
- Show links regardless of user access.
- Hide links to unpublished entities regardless of user access.
- Hide links without a translation.
Core can't cover all of these possible combinations but we should facilitate contrib and custom modules to cover them.
Proposed resolution
Dispatch an event within \Drupal\Core\Menu\MenuLinkTree::transform() that modules can subscribe to and alter menu link manipulators.
This solution is similar to #2854013 except that it applies to any menu link tree regardless of how it is built (ex. via a block or theme preprocess). This decouples the building of a menu link tree from how it is rendered.
API changes
- Dispatch an event to alter the array of manipulators in MenuLinkTree::transform()
- Allow adding cache metadata to menu links by implementing
\Drupal\Core\Cache\RefinableCacheableDependencyTrait.
| Comment | File | Size | Author |
|---|---|---|---|
| #43 | failed_javascript.png | 63.66 KB | eduardo morales alberti |
| #42 | regression_manipulators.png | 167.17 KB | eduardo morales alberti |
| #41 | menu_link_tree_fail.png | 19.72 KB | eduardo morales alberti |
Issue fork drupal-3091246
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
malcolm_p commentedComment #4
malcolm_p commentedComment #5
malcolm_p commentedUpdated with a new patch that also allows adding cache metadata to menu links.
Comment #6
gonssalFirst of all, thanks for your solution. I tested it and it works as expected.
I have some suggestions on your code though:
@varcomments should be 3 lines, with the opening and closing in their own line.__constructfunction must declare visibliitypublicin MenuLinkTreeManipulatorsAlterEvent.php.abstractclass similar toRouteSubscriberBasewould be nice.Comment #7
joachim commentedLooks like a potentially useful addition (and the patch comes with tests, which is great!), but needs work because of code style at least. I also spotted this:
This looks like an unrelated change.
Comment #8
johnwebdev commentedI am wondering if there would be use cases where, i.e.
Hide links without a translation.
What if you want a certain menu block that does not do that?
Comment #10
s.abbott commentedThis seems like a better approach in theory, but when I went to use it I almost immediately ran into an issue where I want to apply a manipulator only to some trees and not others, but the alter event does not expose any identifiable information I could find. This means that unless you filter based on the content of the tree, any manipulators added via this method are an 'all or nothing' type deal.
Comment #11
gonssal@s.abbott your comment is incorrect. I'm for example using the
getMenuName()function of every tree element to make changes to a specific menu only.Comment #12
tuutti commentedFixed some coding standard issues and rerolled for 9.2.x.
Comment #14
dhirendra.mishra commentedRe-rolled patches against 9.3.x as patch was not getting applied for MenuLinkTreeTest.php file so re-rolled it manually.
Comment #18
j.cowher commentedRe-rolling #14 for 9.5.3
Comment #19
j.cowher commentedLast patch was generated incorrectly and was missing files.
Comment #21
gwvoigtIs there a patch that works with D10?
Comment #22
gwvoigtAdding a patch that can be applied to D10.
Comment #23
gwvoigtFixing my previous patch, to be used with D10
Comment #24
huhhuh commentedPatch for Drupal 10.2.
Comment #26
acbramley commentedComment #28
acbramley commentedI've rolled ##24 onto an MR and updated the event dispatcher based on https://www.drupal.org/node/3376090
This was set to NW all the way back in #7 so I think this may be ready for another review.
The main thing I'm interested in is allowing menu links to have their cacheable metadata refined (i.e adding RefinableCacheableDependencyTrait to MenuLinkBase).
Comment #29
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue.
While you are making the above changes, we recommend that you convert this patch to a merge request. Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)
Comment #30
acbramley commentedComment #31
smustgrave commentedAppears to have test failures
core/lib/Drupal/Core/Menu/MenuLinkBase.phpwill the removed functions have to be deprecated?Comment #32
acbramley commentedcore/lib/Drupal/Core/Menu/MenuLinkBase.php will the removed functions have to be deprecated?No, they are provided by the trait that's added instead.
Failures should be fixed with latest commit.
Comment #33
smustgrave commentedLeft 2 more small comments, will keep an eye out for this one.
Comment #34
acbramley commentedResolved the comments and also applied similar changes to the rest of the new files. Also removed the reference from getManipulators as we don't need that when we have a setter.
Comment #35
kriboogh commentedFailing tests due to https://www.drupal.org/project/drupal/issues/3041318
Comment #36
eduardo morales alberti+1 to this solution
Comment #37
eduardo morales albertiThe event does not alters the manipulators array, help!
My subscriber:
Maybe the array should be by reference?
Comment #38
tunicI would say
should be:
The $manipulators array is not changed during dispatch because arrays are copied when assigned, so a call to setManipulators inside a subscriber changes the array inside the $event but not the original $manipulators array used to create the instance of $event.
Comment #39
eduardo morales albertiWe are using 10.2.7, we are not sure, if Drupal 11 treats arrays in a different way, but the @tunic comment seems to be logical.
Comment #40
eduardo morales albertiThe change proposed by @tunic works for as on Drupal 10.2.7
Comment #41
eduardo morales albertiTests are failing https://git.drupalcode.org/issue/drupal-3091246/-/jobs/2307611

The manipulator is not applying
Comment #42
eduardo morales albertiHere came the regression, the references were removed on commit https://git.drupalcode.org/project/drupal/-/commit/552c484ad403a37e93056...
The options are to apply the solution proposed by @tunic or recover the property manipulators as reference, the last one could be a bit confusing to implement by the user on the Subscriber.
Comment #43
eduardo morales albertiFailed Javascript tests https://git.drupalcode.org/issue/drupal-3091246/-/pipelines/275582

Comment #44
eduardo morales albertiReady to review!!
Comment #45
amateescu commentedPosted a few comments in the MR.
Comment #46
tuutti commentedThe current implementation in #2466553: Untranslated menu items are displayed in menus is very similar to this
Comment #47
mstrelan commentedOpened #3475433: Make it easier to decorate menu.default_tree_manipulators which proposes another method of decorating manipulators.
Comment #48
jnicola commentedFaffing around with this now.
How does one determine exactly which menu they are adjusting the manipulators on?
I am using the code from the patch for 10.2, so perhaps this has been addressed later... but I'm looking to remove checkAccess from certain menus as we can simply rely on published... but I'll be darned if I can't figure out how to determine the menu based on what's available to me in the event.
Comment #49
eduardo morales albertiSolve MR conflict with Drupal 11
Comment #50
eduardo morales albertiComment #51
eduardo morales albertiDeprecation:
Comment #53
sleitner commentedRemoved constructor parameter comments , MenuLinkMock::create to MenuLinkMock::createMock, changed deprecation version to 11.3.0,
parse the eventDispatcher to parent constructor.
Please review
Comment #54
smustgrave commentedSorry but deprecations need to be bumped to 11.4 now.
Also think we probably need a CR for the new event, if the goal is to allow others to alter it.
Comment #55
sleitner commentedBumped to 11.4 and new CR for the new event
Comment #56
smustgrave commentedBelieve super close! 1 thread still open on MR.
Comment #57
sleitner commentedremoved RefinableCacheableDependencyTrait
Comment #58
smustgrave commentedbelieve feedback for this one has been addressed
Comment #59
longwaveTagging for subsystem and/or framework manager review. While allowing menu tree structures to be altered is a good idea, the implementation and idea of "manipulators" feels a bit like a Drupalism. This MR adds an event where callbacks can be set, which are then immediately called. Why doesn't
transform()just dispatch an event containing the tree, and the manipulators themselves can be event subscribers? Every case in core wheretransform()is called does a mostly copy-pasted setup with some hardcoded strings:etc.
This feels suboptimal, and I'm not sure adding this API on top of this is the right thing to do.
Comment #60
longwaveFor the case in Navigation (or elsewhere) where the caller wants something different to happen, perhaps
transform()could be passed a custom event class so downstream subscribers can tell the difference between who is calling.edit: not sure this will work actually with the way event subscribers are set up, but maybe there is some alternative solution to the variations.
Comment #61
jnicola commentedI like the idea of this as an event subscriber alone where you can adjust the menu tree manipulators.
For our use case with groups, having
menu.default_tree_manipulators:checkAccesson a larger menu made for a very large amount of additional db calls, when all we care about is published on this particular menu. Removing that manipulator made our life a lot easier.I still didn't get an answer on how you'd determine just which menu you're adjusting however, which I think would be a critical piece for this. I looked through all the values available to me in xdebug and wasn't seeing any identifying values to ensure which menu I was altering. Maybe I missed something obvious though, I'm capable of being wrong for sure.
Being able to identify just which menu you are altering would be critical though, as removing
menu.default_tree_manipulators:checkAccessis fine for our main menu for example, but it would be an issue if we did that on other menus... say admin menu!Comment #63
pwolanin commentedI agree with the what seems to be @longwave's sentiment that this approach might be making an ugly situation a little worse.
Every menu link has a menu, but also the transform might not be happening in the context of that specific menu in code like
\Drupal\system\SystemManager::getAdminBlock()where you can be rendering anything you want based on on incoming menu link.The fact that the manipulators are almost the same across core suggests the transform() method could just start with that standard list and add an alter hook? I'm not sure I understand why an event is preferred here?
It does seem like there needs to be more context provided (e.g. menu name or information about where the tree is being built).
Comment #64
nicxvan commented+1 for hooks. I have the same question, what benefit do event subscribers provide here?
Comment #65
catchI was also wondering why this couldn't be an alter hook, although it might need to be fired from the place adding the manipulators rather than centrally?
Don't know exactly how yet, but this would probably help #3529163: Default menu block plugin should only show enabled links - e.g. could workspaces remove the manipulator again?
Comment #66
eduardo morales albertiIn our case, the hook is too late for alteration; we need to alter the menu before the pre-render see issue https://www.drupal.org/project/translatable_menu_link_uri/issues/3472291
Comment #67
berdirComing here from #3559353: Find a way to bulk load entities and aliases in menus where this could be useful too as it might be too opinionated for core.
event vs hook is IMHO an implementation detail, they essentially achieve the same. For a while we moved things, especially lower-lever to events so we could use OOP/services, but now we have OOP hooks, so that changes things again.
I think the problem with replacing manipulators entirely is the necessary context as #60 said. navigation is a good example. That extra logic runs just because it is the navigation block. The same menus could be shown in a different place where that manipulator should not run. If we replace them completely then we need a way to pass in a context string or other identifier that allows the navigation alter to just run on itself.
navigation module seems to have invented two different alter mechanism, one is the passed in manipulator, but it _also_ subclasses MenuTree as a separate service, so it can run an alter hook in \Drupal\navigation\Menu\NavigationMenuLinkTree::transform(), which it then implements in \Drupal\navigation\Hook\NavigationHooks::navigationMenuLinkTreeAlter(). So we essentially already have a precedent for an alter hook.
BC is tricky as it's again a multi-directional BC thing. We need BC if the menu link tree service is customized, we also need BC for callers. We could completely deprecate the transform() method and add a new alter() method on a new optional interface but it also seems quite unusual to require an explicit call between load and build for some that's pretty fundamental like access checks. But how do we pass in the context if not through a new method? And how do we ensure correct behavior when MenuLinkTree is subclassed, which would then also have the new method? We had similar issues with the user authentication API changes.
Calls in contrib: https://search.tresbien.tech/search?q=menuTree-%3Etransform%20-r%3Adrupa...
subclasses: https://search.tresbien.tech/search?q=%22extends%20MenuLinkTree%22%20-r%... (doesn't catch some cases that alias the parent)
Comment #68
longwaveNot sure we need an event or a hook here, but instead we could use tagged services.
The common case is currently:
What if we changed this to just:
The two default manipulators would become tagged services. Anything else that wants to manipulate any menu item (the original aim of this issue) becomes another tagged service; it can add itself before, in between or after
checkAccessandgenerateIndexAndSort.For special cases where callers add their own manipulator, they just call their helper directly. For example
ModuleAdminLinksHelper::getModuleAdminLinks()becomes:or
MenuParentFormSelector::getParentSelectOptions()becomes:The only case in core where this becomes tricky is NavigationMenuBlock:
However I'm pretty sure we can refactor this so the second level links can be either before or after.
We also have some awkwardness in that both Navigation and Toolbar provide their own subclass of
MenuLinkTree- however I'm also pretty sure that that is not strictly necessary either, although that's almost certainly out of scope here.Comment #69
longwaveNote that because we're removing the second argument to
transform()here we can both do this with backward compatibility and issue a deprecation notice to downstream users.Comment #71
longwaveHad a further idea here which is a slight BC break, but means the API becomes cleaner.
transform()entirely.load()becomes responsible for also providing the default sort order for the results, ie. foldgenerateIndexAndSort()intoload(). The BC break here is that the keys returned fromload()will no longer be mlids. Or maybe we can use a custom sort method that doesn't change the keys?checkAccess()method. MenuForm (or other callers that don't care about access) don't call this. Callers that want to do different access checks optionally call this if they need to, and can provide their own additional checks.This means the generic calling case becomes:
This splits the API much more cleanly into load > access check > build phases. Maybe we add hooks at the load and access check phases for contrib to easily make changes at both these points?
Comment #72
jnicola commentedI 100,000% support splitting out access checking.
I'm on several projects where permissions checking bogs down menu performance and is irrelevant since all that matters is published.
Comment #74
longwaveMR!15727 is a rough implementation of #71, partially assisted by Claude Code.