Skip to main content
Context sets let you store multiple menus under the same code but with different context (a JSON key-value map on the Menu entity), then resolve the most specific match at render time. A common use case is a multi-tenant SaaS where each partner — or even each operator within a partner — has a customised sidebar menu, with a generic fallback for everyone else.

How context resolution works

Each Menu entity has an optional context field (stored as a JSON object, e.g. {"partnerId": 1, "operatorId": 1}). When you pass a list of context objects (“context sets”), the bundle tries them in order and returns the first menu whose code and context both match.
  • Most specific first — put the tightest match at the start of the list.
  • Fallback last — use {} (or null) at the end to match menus with no context.
  • If nothing matches, the tree is empty.
contextSets = [
    { partnerId: 1, operatorId: 7 },   // 1. exact operator match
    { partnerId: 1 },                  // 2. partner-level match
    {}                                 // 3. global fallback
]

Rendering in Twig

Pass the contextSets array as the third argument to dashboard_menu_tree and as the second argument to dashboard_menu_config:
{% set contextSets = [
    { 'partnerId': app.user.partnerId, 'operatorId': app.user.operatorId },
    { 'partnerId': app.user.partnerId },
    {}
] %}

{% set tree = dashboard_menu_tree('sidebar', null, contextSets) %}
{% set menuConfig = dashboard_menu_config('sidebar', contextSets) %}

{% include '@NowoDashboardMenuBundle/menu.html.twig' with {
    menuTree: tree,
    menuCode: 'sidebar',
    menuConfig: menuConfig
} %}

Using the JSON API

When calling the API endpoint, pass the same list as the _context_sets query parameter, JSON-encoded:
GET /api/menu/sidebar?_context_sets=[{"partnerId":1,"operatorId":7},{"partnerId":1},{}]
The resolution logic is identical to Twig — most specific first, fallback {} last.

Real-world scenario: per-partner menus

Suppose you have three menus in the database, all with code sidebar:
Menu codeContextDescription
sidebar{"partnerId": 1, "operatorId": 7}Custom menu for operator 7 under partner 1
sidebar{"partnerId": 1}Default menu for all of partner 1
sidebar{} (none)Global fallback for all other users
For a request from operator 7 of partner 1 you would pass:
{% set contextSets = [
    { 'partnerId': 1, 'operatorId': 7 },
    { 'partnerId': 1 },
    {}
] %}
The bundle finds the first menu match — the operator-specific one — and returns its tree. If that menu did not exist, it would fall back to the partner-level one, then the global one.

Programmatic code resolution with MenuCodeResolverInterface

For more complex scenarios — such as deriving the menu code itself from request attributes — implement MenuCodeResolverInterface. The resolved code is then looked up with context sets as usual.
use Nowo\DashboardMenuBundle\Repository\MenuRepository;
use Nowo\DashboardMenuBundle\Service\MenuCodeResolverInterface;
use Symfony\Component\HttpFoundation\Request;

final class MyMenuCodeResolver implements MenuCodeResolverInterface
{
    public function __construct(
        private readonly MenuRepository $menuRepository,
    ) {}

    public function resolveMenuCode(Request $request, string $hint): string
    {
        $operatorId = $request->attributes->getInt('operatorId', 0);
        $partnerId  = $request->attributes->getInt('partnerId', 0);

        // 1. Try operator + partner + name
        if ($operatorId > 0 && $partnerId > 0) {
            $code = sprintf('op_%d_partner_%d_%s', $operatorId, $partnerId, $hint);
            if ($this->menuRepository->findOneByCode($code) !== null) {
                return $code;
            }
        }

        // 2. Try partner + name
        if ($partnerId > 0) {
            $code = sprintf('partner_%d_%s', $partnerId, $hint);
            if ($this->menuRepository->findOneByCode($code) !== null) {
                return $code;
            }
        }

        // 3. Fall back to the hint as-is
        return $hint;
    }
}
Register your resolver by aliasing MenuCodeResolverInterface in config/services.yaml:
config/services.yaml
services:
    Nowo\DashboardMenuBundle\Service\MenuCodeResolverInterface:
        alias: App\Service\MyMenuCodeResolver
The same resolver is called for both Twig (dashboard_menu_tree('sidebar')) and the API (GET /api/menu/sidebar). The hint is the string you pass to the function or the {code} segment in the URL.

Ordering rules summary

  • Pass context sets in most-specific → least-specific order.
  • Always end the list with {} if you want a global fallback.
  • Passing null or an empty array is equivalent to looking up a menu with no context.
  • The resolved menu determines which tree and which config are returned.