danilovl/menu-builder-bundle
Symfony bundle for managing site menus with pluggable storage (Doctrine/Cache/Redis), REST API, Twig rendering, and a Vue 3 admin SPA. Supports deep trees, item types (links, mega menus), roles/audience rules, scheduling, i18n, soft delete, and CLI sync from attributes.
Installation
composer require danilovl/menu-builder-bundle
Add to config/bundles.php:
return [
// ...
Danilovl\MenuBuilderBundle\MenuBuilderBundle::class => ['all' => true],
];
Database Migration Run migrations (Doctrine driver):
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
First Menu Creation
php bin/console menu-builder:create-menu main
This generates a default menu with a single root item.
Basic Twig Usage
{{ render_menu('main') }}
Outputs the menu structure in HTML.
Access Admin Panel
The bundle includes a Vue 3 SPA at /admin/menu-builder.
Authenticate via Symfony security (configure in security.yaml).
Create Items
link, external, heading, etc.).Render in Frontend
{{ render_menu('main', {
'currentRoute': app.request.get('_route'),
'currentRouteParams': app.request.get('_route_params')
}) }}
Highlights active items based on current route.
Choose a backend in config/packages/menu_builder.yaml:
menu_builder:
storage:
driver: doctrine # or 'cache', 'redis'
# Doctrine-specific:
entity: App\Entity\MenuItem
# Redis-specific:
# prefix: 'menu_builder_'
Workflow:
Doctrine (Recommended for most projects)
$menu = $menuManager->findMenu('main');
$items = $menu->getItems()->where(['publishedAt <= ?' => new \DateTime()]);
Cache/Redis (For read-heavy, simple menus)
$menuManager->invalidateCache('main');
Role-Based Visibility Configure visibility in the admin panel or via API:
$item->setVisibleToRoles(['ROLE_USER', 'ROLE_ADMIN']);
Custom Voter Extend the default voter for granular permissions:
use Danilovl\MenuBuilderBundle\Security\MenuItemVoter;
class CustomMenuItemVoter extends MenuItemVoter
{
protected function supports(string $attribute, $subject): bool
{
return $attribute === 'EDIT_MENU_ITEM';
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
// Custom logic here
return $token->getUser()->hasRole('ROLE_SUPER_ADMIN');
}
}
Register in security.yaml:
security:
access_control:
- { path: ^/admin/menu-builder, roles: ROLE_EDIT_MENU }
The bundle exposes a REST API at /api/menu-builder.
Example: Fetch a Menu
curl -X GET http://localhost/api/menu-builder/menus/main -H "Authorization: Bearer token"
Example: Create an Item
curl -X POST http://localhost/api/menu-builder/menus/main/items \
-H "Content-Type: application/json" \
-d '{
"type": "link",
"label": "Home",
"uri": "/",
"position": 1
}'
Workflow:
stamp ETag for invalidation:
$response = $this->json($menuManager->getMenu('main'));
$response->setEtag(md5($menuManager->getMenuStamp('main')));
Custom Rendering
Extend the default render_menu function:
{% macro render_custom_menu(menu, options) %}
<nav class="custom-menu">
<ul>
{% for item in menu.getItems() %}
<li class="{{ item.getType() }}">
{% if item.isVisible(options) %}
{% if item.getType() == 'link' %}
<a href="{{ item.getUri() }}">{{ item.getLabel() }}</a>
{% elseif item.getType() == 'mega' %}
{{ render_mega_menu(item, options) }}
{% endif %}
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
{% endmacro %}
Passing Options
{{ _self.render_custom_menu(menu('main'), {
'currentRoute': app.request.get('_route'),
'locale': app.request.getLocale()
}) }}
Cycle Protection
MenuCycleException.try {
$menuManager->moveItem($menu, $itemId, $newPosition);
} catch (MenuCycleException $e) {
$this->addFlash('error', 'Cannot create cycles in the menu.');
}
Locale Handling
previewLocale in the admin to test translations before publishing:
menu_builder:
admin:
preview_locale: en
Soft Deletes
isDeleted() filter in queries:
$items = $menu->getItems()->where(['deletedAt' => null]);
Vue Admin Quirks
404 on /build/admin-menu-builder.js.menu_builder/admin is in webpack.config.js:
Encore.enableSingleRuntimeChunk()
.addEntry('admin-menu-builder', './vendor/danilovl/menu-builder-bundle/resources/assets/admin.js')
Menu Not Updating?
php bin/console cache:clear
redis-cli FLUSHDB # Use cautiously!
API Returns Empty Data
publishedAt/unpublishedAt dates in the admin panel.php bin/console debug:security
Twig render_menu Fails
if (!$menuManager->menuExists('main')) {
throw new \RuntimeException('Menu "main" not found.');
}
Custom Item Types
Extend the MenuItem entity or use events:
// config/services.yaml
Danilovl\MenuBuilderBundle\EventListener\MenuItemEvents::ITEM_TYPE_REGISTER:
tag: kernel.event_listener
class: App\EventListener\CustomMenuItemTypeListener
Override Admin Template
Copy vendor/danilovl/menu-builder-bundle/resources/templates/admin/ to templates/admin/ and customize.
Add Fields to Items Extend the Doctrine entity:
// src/Entity/MenuItem.php
#[ORM\Column(nullable: true)]
private ?string $customField = null;
// Add getter/setter and update migrations.
Bulk Operations
Use the MenuManager directly:
$menu = $menuManager->findMenu('main');
$menu->getItems()->where(['type' => 'link'])->update(['uri' => '/new-uri']);
$menuManager->saveMenu($menu);
Cache Menus Use Symfony’s cache system for rendered menus:
{% cache app.request.get('_route') %}
{{ render_menu('main') }}
{% endcache %}
Lazy-Load Mega Menus For large mega menus, fetch children on-demand via API:
// Vue 3 example
async fetchChildren(item) {
const response = await fetch(`/api/menu-builder/items/${item.id}/children`);
How can I help you explore Laravel packages today?