nowo-tech/dashboard-menu-bundle
A PHP bundle for adding a configurable dashboard menu to your application. It helps organize admin navigation and expose menu items consistently, with simple configuration for integrating dashboard links into Symfony-based projects.
Install and register
composer require nowo-tech/dashboard-menu-bundle
There is no Flex recipe, so register the bundle manually in config/bundles.php:
NowoTech\DashboardMenuBundle\DashboardMenuBundle::class => ['all' => true],
Then import the routes (only if you want the JSON API):
# config/routes/nowo_dashboard_menu.yaml
nowo_dashboard_menu:
resource: '@DashboardMenuBundle/config/routes.php' # or .../routes.yaml
prefix: /admin/api
Where to look first, in order
src/DependencyInjection/Configuration.php — the authoritative list of config keys. This is what you will edit 90% of the time; the README examples drift, the config tree does not.src/DependencyInjection/DashboardMenuExtension.php — shows how items are merged and which services/tags the bundle exposes.src/Resources/views/ — the reference Twig template and its blocks (menu, item, item_link, …) plus the exact Twig function name and argument signature.src/Controller/ — the JSON endpoint's route name, serialization shape, and whether permission filtering happens there or in the provider.README.md — for the canonical config snippet to copy.First use case: a two-level admin sidebar in 15 minutes
Define three items in config/packages/nowo_tech_dashboard_menu.yaml, one of them with a parent, one with a role, one with a JSON translation key. Render it once with the bundle's Twig function in your layout, then hit the JSON route with curl -H 'Accept: application/json' as two different users to confirm the permission filter works on both paths. If those two outputs disagree, you have found the single most common integration bug before writing any UI.
Do not start by reading the whole repository. The bundle is small, but the config tree + extension + one template answer every day-one question.
Treat the config file as the source of truth, not the database. There is no Gedmo/Stof tree here, so there is no lft/rgt/level column and no automatic reordering. Menu structure lives in configuration (or whatever provider you write). Consequences you should design around:
parent/position contract exactly as the bundle expects, so rendering and the API keep working unchanged.Tree wiring: parent + position
nowo_tech_dashboard_menu:
menus:
main:
items:
dashboard: { label: menu.dashboard, route: admin_dashboard, position: 0 }
content: { label: menu.content, position: 10 }
content_news:{ label: menu.news, parent: content, route: admin_news, position: 10 }
Use sparse positions (0, 10, 20, …), never 0, 1, 2. Inserting a sibling later without renumbering everything is the whole point of a position field, and with dense numbering you lose that on the second iteration. Every item should get an explicit position; items without one fall back to declaration order, which silently changes when someone reorders YAML keys.
i18n via JSON translations
Labels are translation keys resolved at render time, so keep the JSON catalogs in the standard translations/ directory with the standard domain the bundle reads (check Configuration.php for the default translation_domain). One file per locale, flat keys:
{ "menu.dashboard": "Dashboard", "menu.news": "News" }
Reuse the same JSON catalogs for the API payload — the API returns translated labels, so the frontend never needs to know your translation keys. That is the main reason to use this bundle over hand-rolling a menu array: one label source for Twig and JSON.
Permission filtering
Filter on the server: the provider/controller must drop items the current token cannot see, and drop the descendants of any dropped parent (a child whose parent vanished must not be re-parented to the root — that produces the classic "orphan menu item appears at top level" bug). In Twig, use is_granted() only as a second line of defense around the render call, never as the primary gate.
Template override, not template fork
Copy the bundle template once into templates/bundles/DashboardMenuBundle/ and change only the blocks you need. The directory name must match the bundle's short name exactly, including casing, or the override is silently ignored and you debug the wrong file.
Integration tips
parent/position shape in your state — do not flatten it, or a future nested menu breaks your layout.WebTestCase) that asserts both the rendered HTML and the JSON payload for two roles. Given the package's 0 dependents and single-maintainer surface, that test is your upgrade insurance for Symfony 8.Config edits do nothing in prod. The container is compiled; after changing YAML you need bin/console cache:clear. In dev the container rebuilds, which makes this bite only at deploy time.
Numeric-key merging appends. Symfony's config merge for list-like item maps does not overwrite by key the way you expect — redefining an item in a second config file can append a duplicate rather than replace it. Keep all menu definitions for a given menu name in one file, or verify with debug:config nowo_tech_dashboard_menu.
Orphaned children. Deleting or permission-hiding a parent without handling descendants produces top-level garbage. Validate at boot: a compiler pass (or a test over the fixture config) that asserts every parent reference exists and is not itself, plus every route exists in the router. Two lines of validation save an hour of prod debugging.
Cycles hang the request. a.parent = b, b.parent = a gives you infinite recursion / a stack overflow, not an exception. Guard it in the same compiler pass.
Position collisions are undefined. Two siblings with position: 10 order by whatever PHP's sort does that day. Always unique per level; add a test that asserts the full ordered list of labels for your real config, which catches collisions and accidental reorderings at once.
Permissions ≠ visibility. Hiding a link in Twig hides nothing. If the JSON endpoint returns an item, the user will find it, and if the underlying route is not #[IsGranted]-protected they will open it. Filter in the provider and secure the controllers.
Shared caches leak menus. The API response depends on the user's roles. Do not put a CDN or a shared HTTP cache in front of it without Vary on the session/roles and Cache-Control: private. Symptom: an admin sees a normal-user menu, or worse, the reverse.
Missing translation domain. If translations/messages.en.json exists but the bundle's configured domain is different, you get raw keys like menu.dashboard in the UI. Confirm with bin/console debug:translation en — it lists which domain each key resolved in.
No security token in CLI/mail context. Rendering the menu from a console command or an email template means is_granted() has no token. Either filter explicitly by passing roles to the renderer, or wrap the call; otherwise you get an exception or, worse, an empty menu in a place you cannot see.
Template override path is silent. Wrong directory name or wrong bundle casing and Symfony falls back to the vendor template with no warning. When a template edit "does nothing", check the override path before you check the block names.
Extension points worth knowing: the menu provider/registry service (for dynamic menus), the Twig function's argument for choosing a menu name (for rendering the same config twice with different scopes), the reference template's blocks (for markup changes), and the bundle config tree (for adding custom item attributes if the item definition passes them through). Everything else — the tree, sorting, filtering, JSON shape — is bundle internals you should not fork.
Upgrade posture. Symfony 8 support is recent and the package has no downstream dependents, so treat each minor as potentially BC-breaking despite semver. Pin with a caret constraint, keep the HTML+JSON functional test, and read CHANGELOG.md/UPGRADING.md on every bump. If the bundle ever stalls, the escape hatch is small: your provider + a Twig loop reproducing the same tree contract, which is why the test matters more than the abstraction.
How can I help you explore Laravel packages today?