zetta/menu-bundle
Zetta Menu Bundle is a PHP package for building and managing hierarchical navigation menus. It offers menu and menu-item models, tree operations, and integration points for Symfony applications, helping developers define, organize, and render multi-level menus with less boilerplate.
Install and pin the Symfony version first. This package's last tagged release is from October 2015, so it targets Symfony 2.x (~2.3–~2.8). Add it alongside the versions of knplabs/knp-menu-bundle and Doctrine it was built against, or Composer's solver will pull modern versions that break the service definitions:
{
"require": {
"zetta/menu-bundle": "dev-master",
"symfony/symfony": "~2.8",
"knplabs/knp-menu-bundle": "~2.0"
}
}
There is no Flex recipe, so register the bundle manually in app/AppKernel.php:
new Zetta\MenuBundle\ZettaMenuBundle(),
Where to look first — in this order:
DependencyInjection/Configuration.php — the config tree tells you what is actually configurable (table/entity names, template, role field, cache settings).Resources/config/services.xml — the real API surface. Service IDs here are what you will inject into your controllers and layouts; the README is sparse, this file is the documentation.Entity/ (if the menu is Doctrine-backed) — look at the fields the bundle expects on a menu item: label, route/URI, parent, position, and the security field (role or voter attribute).Resources/views/ and any Twig extension — rendering is almost certainly done through a Twig function/filter that wraps KnpMenu's knp_menu_render.Security/ or Voter/ — this is where "Secure" happens: the check that removes items the current token cannot access.Configure the minimum. Register the bundle's routing file if it ships controllers/admin screens, and import its config in config.yml:
zetta_menu:
# keys per Configuration.php — typically entity class, template, and
# an option controlling whether security filtering is on
Then run doctrine:schema:update --dump-sql and inspect the output before applying — if the bundle ships entities, this is your first real look at the data model.
First use case: a role-aware main navigation. Create a few menu items (route + required role) in the database or in a fixture, put a single call in your base layout:
{# app/Resources/views/base.html.twig #}
<nav>{{ zetta_menu_render('main') }}</nav>
Log in as a user with a reduced role set and confirm the items disappear. That round trip — seed item → render → log in as a restricted user → item hidden — is the whole value proposition of the bundle and the fastest way to validate your configuration.
Treat the bundle as a thin, security-aware wrapper around KnpMenu. The healthy way to use it day-to-day is to keep menu structure in the bundle and menu presentation in your own templates. Override the bundle's view rather than fighting it:
{# app/Resources/ZettaMenuBundle/views/menu.html.twig #}
{% for item in item.children %}
<li class="{{ item.current ? 'active' }}">
<a href="{{ item.uri }}">{{ item.label|trans({}, 'menu') }}</a>
{{ block('children') }}
</li>
{% endfor %}
Build menus in code when they are structural, in the database when they are editorial. A controller or MenuBuilder service gives you type safety, IDE navigation, and free diffs; the Doctrine-backed path is only worth it when non-developers maintain the tree. If you use both, namespace them (main, footer, admin) and don't mix sources in a single menu.
Filter with the security system, not with in_array($role, $user->getRoles()). The bundle's added value is delegating to Symfony's AuthorizationCheckerInterface, which means role hierarchy, remember-me, and switched users all behave correctly. If you add your own items, attach the security metadata through the same mechanism the bundle uses (check Configuration.php for whether it is a role string or a voter attribute/subject pair) so that filtering stays in one place.
Cache the tree, key it by the permission set. Rendering a menu on every request is cheap until the tree is large and role-filtered per user. Cache the unfiltered tree (it is identical for everyone) and apply security filtering per request; if you cache the filtered result, the cache key must include the user's effective roles, and you must invalidate on role changes. Caching the filtered tree under a generic key is the single fastest way to leak an admin-only link to an anonymous visitor.
Active-state and route parameters. Let the bundle compute current from the request rather than hardcoding class="active". If items link to parameterised routes, pass route params in the item definition, not by string-concatenating URIs — otherwise the current-item detection silently stops matching.
Integration tips:
main, sidebar, breadcrumb) and render each independently; breadcrumbs derived from a filtered menu are a good way to reuse the same security rules.Testing pattern. Write one kernel/functional test per security boundary:
$client = static::createClient();
$client->request('GET', '/', [], [], [
'PHP_AUTH_USER' => 'editor',
'PHP_AUTH_PW' => 'editor',
]);
$this->assertNotContains('Admin', $client->getResponse()->getContent());
This catches the two failure modes that actually matter: items leaking to users who shouldn't see them, and items vanishing for users who should.
security.context is gone. Any 2015-era bundle that called $this->get('security.context')->isGranted() breaks on Symfony ≥2.6 (deprecated) and ≥3.0 (removed). The fix is mechanical — inject security.authorization_checker and security.token_storage — but you will be doing it in vendor code, so do it in a fork and record it in your composer repositories block.services.xml. Every guess about service IDs and constructor arguments costs a ServiceNotFoundException cycle. php app/console container:debug, debug:container <id> --show-arguments, and the profiler's service panel are your fastest debugging tools here.role_hierarchy depends entirely on whether it calls isGranted(). If it compares raw strings, ROLE_ADMIN will not see items gated on ROLE_USER unless you also list that role on the item. Verify this explicitly before relying on hierarchy.Resources/config/doctrine. Don't patch the vendor mapping directly — your next composer update wipes it.<span> wrappers). Decide once whether labels are |raw and make sure editors cannot inject markup through the label field if they are.zetta_menu config block and any code-built menus in the repo; treat database-stored menu trees as content that needs a fixture or migration path for new environments, or staging will come up with an empty nav.How can I help you explore Laravel packages today?