Skip to main content
The MenuTreeLoader service is responsible for turning a flat database table into the nested structure used by the Twig template and the JSON API.

Two-query loading

Loading a menu tree requires exactly two SQL queries — one for the Menu row and one for all of its MenuItem rows. There is no N+1 problem regardless of how deep the tree is.
Query 1: SELECT * FROM dashboard_menu WHERE code = :code AND attributes_key = :key
Query 2: SELECT * FROM dashboard_menu_item WHERE menu_id = :id ORDER BY position ASC
The two-query path is the fast path. A legacy path using the Doctrine ORM collections exists as a fallback but is not used in normal operation.
Items are fetched ordered by position ASC within each parent. The PHP assembly step re-sorts children by position after building the map, so the final tree is always correctly ordered.

Tree assembly

Once the flat list of items is loaded, the tree is built in PHP in two passes:
1

Permission filtering

Each item is checked against the menu’s MenuPermissionCheckerInterface. Items that fail canView() are discarded before the tree is wired up.
2

First pass — create node map

Every visible item is inserted into an in-memory map keyed by its database ID:
$map[$item->getId()] = ['item' => $item, 'children' => []];
3

Second pass — wire parent/child references

Each item’s parent is checked. If the parent is in the map (visible), the node is appended to the parent’s children array using PHP references so updates are shared. Items whose parent was filtered out are promoted to root level.
4

Sort

The resulting roots and all child arrays are sorted by position ASC recursively.
5

Prune empty parents

Parents that originally had children (before filtering) but now have none visible are removed. See Permissions — pruning empty parents.

Node structure

The return value of MenuTreeLoader::loadTree() is a list<array> of root nodes. Each node has this shape:
[
    'item'     => MenuItem,        // the entity
    'children' => list<array>,     // same structure, recursively
]
The Twig template receives this list as menuTree.

Item types

Each MenuItem has an itemType that controls how it is rendered:
TypeRendered asCan have children
link<a href="...">Yes
section<span> label (no link)Yes
divider<hr>No
A link item with children renders a clickable link and (optionally) an expand/collapse chevron when nestedCollapsible is enabled on the menu. A section item renders only a label; the chevron is its only interactive element.

Depth limit

The depthLimit field on Menu restricts how many levels the Twig template renders.
ValueEffect
nullUnlimited depth (render everything)
1Root items only — children are not rendered
2Root + one level of children
nRoot + n - 1 levels of children
depthLimit is enforced at render time by the Twig template, not during tree loading. The full tree is always loaded into memory; the template simply stops recursing past the limit.

Current route detection

The CurrentRouteTreeDecorator service adds two boolean keys to each tree node after the tree is assembled:
Node keyWhen trueCSS class applied (from Menu entity)
isCurrentThe item’s path and its query params are a subset of the current requestclassCurrent on the <a>
hasCurrentInBranchThis node or any of its descendants has isCurrent = trueclassBranchExpanded on the <li>
hasCurrentInBranch propagates up the tree so the full path from root to the active item can be highlighted and kept expanded.
isCurrent uses path matching with query subset checking: the link’s path must equal the request path, and every query parameter stored on the link must be present in the request with the same value. The request may have additional query parameters without affecting the match.

PSR-6 cache

If a PSR-6 CacheItemPoolInterface is wired to the MenuTreeLoader, the raw query results are serialized and cached. The cache key is derived from:
$cacheKey = 'nowo_dashboard_menu.tree.' . md5($menuCode . '.' . $locale . '.' . serialize($contextSets));
This means a separate cache entry is stored for each unique combination of menu code, locale, and context sets.
  1. Deserialize the raw SQL rows from cache.
  2. Hydrate Menu and MenuItem objects from the rows (no DB queries).
  3. Resolve config and permission checker.
  4. Build and prune the tree.
  5. Return result.
The cache stores the raw database rows (pre-permission-filtering). Permission checks and tree assembly always happen at request time, so cached menus are still filtered per-user correctly.
See Cache configuration to learn how to configure the cache pool and TTL.

Summary

Menu entities

The fields on Menu and MenuItem that drive tree loading.

Permissions

How items are filtered and empty parents pruned.

Context resolution

How the right Menu row is selected before loading begins.

Twig functions

How to render the tree in your templates.