Every menu link you've built so far has been static — written once in YAML, the same for every visitor, every time. But sometimes a link needs to change based on something YAML can't know in advance: the current user's role, a config setting, the number of items in a queue. hook_menu_links_discovered_alter() is how you reach in and modify links — your own, or anyone else's — after they've been discovered but before they're rendered.
What you'll learn in this lesson
- When
hook_menu_links_discovered_alter()runs in the menu-building process, and why that timing matters - How to find the exact array key for the link you want to change
- Real, common uses: dynamic titles, conditionally hiding a link, changing its target route
The shape of the hook
/**
* Implements hook_menu_links_discovered_alter().
*/
function my_module_menu_links_discovered_alter(array &$links) {
// Change an existing link's title dynamically.
if (isset($links['my_module.dashboard'])) {
$pending_count = \Drupal::database()
->select('my_module_queue', 'q')
->countQuery()
->execute()
->fetchField();
$links['my_module.dashboard']['title'] = t('Dashboard (@count pending)', [
'@count' => $pending_count,
]);
}
// Remove a link entirely for users who lack a specific permission.
if (isset($links['my_module.admin_tools']) && !\Drupal::currentUser()->hasPermission('access my module tools')) {
unset($links['my_module.admin_tools']);
}
}
How it works
The single &$links parameter, and why it's passed by reference
This is an alter hook — the same family as hook_form_alter() from an earlier lesson. It receives one parameter, the entire array of every menu link discovered from every enabled module's *.links.menu.yml file, passed by reference (that's what the & means). You're not given a copy to inspect — you're handed the real, live array, and any change you make to it directly changes what actually gets rendered. There's no return value because there's nothing to return; the mutation itself is the whole effect.
Finding the right array key
Every link is keyed by its own machine name — the exact same string that's the top-level key in its .links.menu.yml definition. So a link declared as my_module.dashboard: in YAML shows up here as $links['my_module.dashboard'], containing every property that link was defined with (title, route_name, parent, and so on) as a plain associative array. You can alter any module's link this way, not just your own — which is exactly why this hook exists, rather than every module just editing its own YAML conditionally.
Two genuinely common uses
Overwriting title with a dynamically computed string (a live count, a username, anything YAML can't express statically) is the most common use by far — Drupal core itself does this for things like the "My account" link showing the current user's name. The second common use is conditional removal with unset(), exactly as shown above: hiding a link entirely based on a runtime check that a static _permission requirement on the route can't express, like a feature flag or a business-hours check.
See it for yourself
There's no single page to visit for this one — the effect is the menu link tree itself. After implementing this hook, run drush cr (menu link data is cached, just like the plain YAML-defined links from earlier lessons), then check the target menu and confirm your title change or removal took effect.
Quick check: you implement this hook, but forget to check
isset($links['my_module.dashboard'])before modifying it — you just write straight to that key. What breaks, and when? Nothing breaks immediately if the link genuinely exists — but if your module is ever installed without the module that defines that link (or the link's machine name changes), you'll get an undefined-array-key warning, or worse, silently create a malformed new link entry with that key instead of altering the one you meant. Always guard withisset()first, exactly like the example above.
Key takeaways
hook_menu_links_discovered_alter(array &$links)runs after every module's static.links.menu.ymllinks are discovered, letting you modify any of them — not just your own.- Links are keyed by their own machine name, matching the top-level key from their YAML definition exactly.
- The two most common uses are dynamic titles (a live count, a username) and conditional removal with
unset()based on logic a static permission can't express. - Always guard with
isset()before touching a specific link key — it may not exist if the module defining it isn't installed. - This runs on every request needing menu data — keep the logic cheap, or cache anything expensive yourself.
Coming up next
You've now covered every way Drupal offers to get a visitor from "somewhere on the site" to "your module's functionality" — static menu links, tabs, action buttons, and now runtime alterations to any of them. Next, the course turns to a different kind of asset your module can declare: CSS and JavaScript libraries, starting with how to define one.