Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Menu Builder Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation

    composer require danilovl/menu-builder-bundle
    

    Add to config/bundles.php:

    return [
        // ...
        Danilovl\MenuBuilderBundle\MenuBuilderBundle::class => ['all' => true],
    ];
    
  2. Database Migration Run migrations (Doctrine driver):

    php bin/console doctrine:migrations:diff
    php bin/console doctrine:migrations:migrate
    
  3. First Menu Creation

    php bin/console menu-builder:create-menu main
    

    This generates a default menu with a single root item.

  4. Basic Twig Usage

    {{ render_menu('main') }}
    

    Outputs the menu structure in HTML.


First Use Case: Adding a Navigation Menu

  1. Access Admin Panel The bundle includes a Vue 3 SPA at /admin/menu-builder. Authenticate via Symfony security (configure in security.yaml).

  2. Create Items

    • Drag-and-drop to build hierarchy.
    • Use item types (link, external, heading, etc.).
    • Set visibility (roles, audiences) and publish dates.
  3. Render in Frontend

    {{ render_menu('main', {
        'currentRoute': app.request.get('_route'),
        'currentRouteParams': app.request.get('_route_params')
    }) }}
    

    Highlights active items based on current route.


Implementation Patterns

Storage Backend Selection

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:

  1. Doctrine (Recommended for most projects)

    • Full CRUD, soft deletes, and relationships.
    • Useful for complex queries (e.g., filtering by role).
    • Example query:
      $menu = $menuManager->findMenu('main');
      $items = $menu->getItems()->where(['publishedAt <= ?' => new \DateTime()]);
      
  2. Cache/Redis (For read-heavy, simple menus)

    • Faster reads but limited to basic operations.
    • Cache invalidation required after writes:
      $menuManager->invalidateCache('main');
      

Integration with Symfony Security

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 }

API Usage

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:

  1. Use API for dynamic menus (e.g., SPAs).
  2. Cache responses with stamp ETag for invalidation:
    $response = $this->json($menuManager->getMenu('main'));
    $response->setEtag(md5($menuManager->getMenuStamp('main')));
    

Twig Extensions

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()
}) }}

Gotchas and Tips

Pitfalls

  1. Cycle Protection

    • Moving items in a circular fashion (e.g., A → B → C → A) triggers a MenuCycleException.
    • Fix: Use the admin panel’s drag-and-drop (handles cycles automatically) or validate manually:
      try {
          $menuManager->moveItem($menu, $itemId, $newPosition);
      } catch (MenuCycleException $e) {
          $this->addFlash('error', 'Cannot create cycles in the menu.');
      }
      
  2. Locale Handling

    • Translations are stored per-locale but not synced across locales.
    • Tip: Use previewLocale in the admin to test translations before publishing:
      menu_builder:
          admin:
              preview_locale: en
      
  3. Soft Deletes

    • Doctrine driver supports soft deletes, but cache/Redis backends ignore them.
    • Workaround: Use isDeleted() filter in queries:
      $items = $menu->getItems()->where(['deletedAt' => null]);
      
  4. Vue Admin Quirks

    • The Vue SPA requires Symfony’s Webpack Encore for assets.
    • Debugging: Check browser console for 404 on /build/admin-menu-builder.js.
    • Fix: Ensure menu_builder/admin is in webpack.config.js:
      Encore.enableSingleRuntimeChunk()
          .addEntry('admin-menu-builder', './vendor/danilovl/menu-builder-bundle/resources/assets/admin.js')
      

Debugging

  1. Menu Not Updating?

    • Clear cache:
      php bin/console cache:clear
      
    • For Redis: Flush the menu key prefix:
      redis-cli FLUSHDB  # Use cautiously!
      
  2. API Returns Empty Data

    • Check publishedAt/unpublishedAt dates in the admin panel.
    • Verify security voter permissions:
      php bin/console debug:security
      
  3. Twig render_menu Fails

    • Ensure the menu exists:
      if (!$menuManager->menuExists('main')) {
          throw new \RuntimeException('Menu "main" not found.');
      }
      

Extension Points

  1. 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
    
  2. Override Admin Template Copy vendor/danilovl/menu-builder-bundle/resources/templates/admin/ to templates/admin/ and customize.

  3. 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.
    
  4. Bulk Operations Use the MenuManager directly:

    $menu = $menuManager->findMenu('main');
    $menu->getItems()->where(['type' => 'link'])->update(['uri' => '/new-uri']);
    $menuManager->saveMenu($menu);
    

Performance Tips

  1. Cache Menus Use Symfony’s cache system for rendered menus:

    {% cache app.request.get('_route') %}
        {{ render_menu('main') }}
    {% endcache %}
    
  2. 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`);
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky