kunstmaan/menu-bundle
A menu bundle for Kunstmaan CMS that provides flexible, tree-based navigation menus you can configure and render in your templates. It lets developers define menu structures and integrate them into pages for consistent site navigation.
This is a Kunstmaan CMS stack bundle, not a standalone menu library. It assumes kunstmaan/node-bundle, the Kunstmaan admin (Sonata-based), and Doctrine extensions are already installed. Pulling it into a plain Symfony app is not a realistic path.
Minimal install:
composer require kunstmaan/menu-bundle
Register Kunstmaan\MenuBundle\KunstmaanMenuBundle in app/AppKernel.php, then import routing:
# app/config/routing.yml
_kunstmaan_menu:
resource: "@KunstmaanMenuBundle/Resources/config/routing.yml"
prefix: /admin
Declare the menus once in config — this is the contract between admin UI and your templates:
# app/config/config.yml
kunstmaan_menu:
menus:
main: ~
footer: ~
Override the entities (standard Kunstmaan pattern) before you ever run the schema tool:
# app/config/config.yml
parameters:
kunstmaan_menu.menu.class: AppBundle\Entity\Menu
kunstmaan_menu.menu_item.class: AppBundle\Entity\MenuItem
The subclasses just extend Kunstmaan\MenuBundle\Entity\Menu / MenuItem and re-map the table via annotations. Then create tables (kunstmaan_menu_menus, kunstmaan_menu_menu_items), clear cache, and open the admin — a "Menu" tab appears where editors build the tree by drag-and-drop.
Where to look first, in this order:
Entity/Menu.php and Entity/MenuItem.php — this is the data model you actually work with.Twig/MenuTwigExtension.php — the only API most developers touch (the get_menu() Twig function).Resources/views/ — the item rendering template you will override.Admin/ — controllers for add/edit/reorder and how the tree is persisted.First use case: build the main navigation from existing node pages. In the admin, add items pointing at node translations (or raw URLs), then in your base layout:
{% set menu = get_menu('main', app.request.locale) %}
{% if menu is not null %}
{{ include('@KunstmaanMenu/Menu/menu.html.twig', { menu: menu }) }}
{% endif %}
Config menu keys are the shared identifier. The key in kunstmaan_menu.menus is what get_menu('main') resolves to, and it is what the admin uses to group items. Treat renaming a key as a data migration: existing Menu rows and their items are tied to that name, so renames silently orphan items.
Always render per-locale, and guard for null. The lookup is (name, locale) scoped; a menu configured for nl but requested with fr returns null rather than falling back. Put a single helper in your base template instead of repeating the guard:
{% macro nav(name, locale, depth) %}
{% set menu = get_menu(name, locale) %}
...recursive include with item.parent / item.children...
{% endmacro %}
Recursion is done manually against the tree — the bundle gives you the hierarchy, not a renderer. The stock item template uses the item's title, nodeTranslation, url, and newWindow; override it at app/Resources/KunstmaanMenuBundle/views/Menu/menu.html.twig (or in your own templates/ and include it) to attach Bootstrap/Tailwind classes and aria-current.
Link resolution is "either node or URL". An item is normally bound to a NodeTranslation, which gives you locale-aware URLs and works with the CMS's online/offline state; a url field is the escape hatch for external links and anchors. Your template must branch:
{% if item.nodeTranslation is not null %}
<a href="{{ path('_slug', { url: item.nodeTranslation.urlSlug }) }}"
{% if item.newWindow %}target="_blank" rel="noopener"{% endif %}>
{% elseif item.url %}
<a href="{{ item.url }}">
{% endif %}
Active state is best derived from the current node, not from string comparison: compare item.nodeTranslation against the _nodeTranslation request attribute where available, and fall back to app.request.pathInfo for URL items.
Seeding menus programmatically (fixtures, deploy scripts, site imports) is done through the entity manager, but you must respect the Doctrine tree extension: use the repository's persistAsFirstChild / persistAsLastChildOf (or set parent and let the listener fix lft/rgt/lvl). Never INSERT rows by hand.
Cache the rendered nav, not the entity graph. Menu output is expensive (recursive node translation joins) and changes rarely. Wrap the rendered string in your own cache keyed by name + locale + a version counter, and bump the counter from an admin save/reorder listener rather than trying to invalidate per-item.
Admin-side extensions are the usual Sonata routes: add form fields to MenuItemAdminType, add roles (ROLE_ADMIN-style checks) in the admin controllers, and override the admin templates. If editors need "title only in this locale", that is an entity override plus a form extension — not a config toggle.
kunstmaan_menu.*.class parameters before creating tables. Changing them after the fact leaves you with two tables and mapping drift.lft/rgt/lvl/root are Gedmo-maintained. Any migration, importer, or manual UPDATE that touches parent_id must go through the tree repository, or the admin drag-and-drop renders nonsense.null from get_menu(). Never assume menu.items exists.knp-menu with your own entity; on an existing Kunstmaan install, lock the bundle and the tree extension version together and avoid upgrading one without the other.doctrine:schema:validate (mapping vs tables), debug:twig (confirm get_menu is registered), debug:container --parameter=kunstmaan_menu.menu.class (override actually applied), then the admin reorder AJAX response. Nine out of ten issues are one of those four.Menu row is a container.How can I help you explore Laravel packages today?