Every menu link on your site — Structure, Content, your own module's admin page — starts life as a plain YAML entry in some module's .links.menu.yml file. Drupal collects every one of those entries from every enabled module into a single flat registry before the menu system ever renders anything. hook_menu_links_discovered_alter() is your one chance to reach into that combined registry and change, add to, or remove from it — after discovery, before it's used.
What you'll learn in this lesson
- How menu links move from individual
.links.menu.ymlfiles into one combined array - The array's structure, keyed by menu link plugin ID
- How to conditionally hide a menu link without deleting its YAML definition
The hook
/**
* Implements hook_menu_links_discovered_alter().
*/
function example_module_menu_links_discovered_alter(&$links) {
// Every key in $links is a menu link plugin ID, almost always in the form
// "module_name.link_machine_name" — matching how it was declared in that
// module's .links.menu.yml file.
if (isset($links['node.add_page'])) {
// Change the title shown in the admin menu, without touching node
// module's own .links.menu.yml file.
$links['node.add_page']['title'] = t('Create new content');
}
// Conditionally remove a link entirely — for example, hiding a
// development-only tool's menu entry outside of local environments.
if (isset($links['example_module.dev_tools']) && !\Drupal::service('kernel')->getContainer()->getParameter('some_dev_flag')) {
unset($links['example_module.dev_tools']);
}
}
The array is keyed exactly the way each link was declared — module_name.link_id — so finding the specific link you want to touch is a simple isset() check, not a search through nested structures.
Why "discovered," and why it matters
The word "discovered" in the hook's name is a hint about when it runs: after Drupal has already scanned every module's .links.menu.yml file and merged the results, but before any of it is cached for use. That makes this hook the right layer for changes that depend on runtime conditions — a feature flag, the current user's role, an environment check — since a static YAML file can't express "only show this link if X is true," but a PHP function absolutely can.
.links.menu.yml file — that's simpler, declarative, and doesn't require a hook at all. Reserve this hook for changing or conditionally removing links that another module already declared, which you have no other way to influence.Quick check: your module needs to add a brand-new "Reports" link under Structure that always appears for administrators. Should that go in this hook, or in your own
.links.menu.yml? (Answer: your own.links.menu.yml— it's your link, unconditionally present, which is exactly what the declarative YAML approach is for. Save this hook for links you don't own or links whose visibility depends on runtime state.)
Key takeaways
hook_menu_links_discovered_alter(&$links)receives every module's declared menu links merged into one array, keyed bymodule_name.link_id.- Use it to change or conditionally remove links you don't own — not as a substitute for declaring your own links in
.links.menu.yml. - Because it runs after discovery but before caching, it's the right place for runtime-conditional menu logic that plain YAML can't express.
Coming up next
You've now seen the alter-hook pattern applied to forms, entity types, and menu links — three completely different data structures, one consistent mechanism. Next: Creating Custom Hooks, where you'll flip the pattern around and define a hook of your own that other modules can implement.