Skip to content

Menu and permissions

Adding a menu item

Every admin screen that should appear in the menu carries #[CpAdminMenu] together with a #[Route]:

#[Route('/admin/example', name: 'admin_example_index', methods: ['GET'])]
#[CpAdminMenu(label: 'Examples', icon: 'heroicons:squares-2x2', panel: 'studio', priority: 40, capability: 'example.view', group: 'studio.group.content')]
public function index(): Response { /* … */ }
Parameter Meaning
panel studio (content) or aacp (system)
priority smaller number first
capability required permission; a|b = one of them is enough
parent the route name of the parent item (two-level submenu)
group sidebar heading

The menu has two levels: a parent item and its children. A folder (hub) with no children is not shown.

The Tools rule in AACP

Rule

A module cannot add a top-level menu item to AACP. Any item registered with panel: 'aacp' that has no parent (or is attached to another module's item) is automatically moved to the Tools folder at build time.

If a module explicitly names one of the core's folders (e.g. aacp_hub_system) as its parent, that placement is kept. The rule is applied in AdminMenuRegistrationPass.

How the menu is built

The menu is collected from the #[CpAdminMenu] attributes on controllers at build time; it is not rescanned on every request. At render time (AdminMenuRuntime) three filters are applied:

  1. Is the module active? Items of an inactive module are not shown.
  2. Is there permission? If capability is empty, being signed in is enough. If you write a|b, having one of the two permissions is enough. A user without permission does not see the item; even if they type the address by hand, the #[IsGranted] on the controller stops them.
  3. Is the parent visible? A child item is visible only if its parent is visible; an "orphan" item whose parent was filtered out is skipped.
The menu is not a security layer

Hiding from the menu does not close access. Always put #[IsGranted(...)] on every admin action; the menu is only a guide.

The parent item and its children can come from the same module. The child gives the parent item's route name as parent:

#[Route('/admin/example', name: 'admin_example_index', methods: ['GET'])]
#[CpAdminMenu(label: 'Examples', icon: 'heroicons:squares-2x2', priority: 40, capability: 'example.view')]
public function index(): Response { /* … */ }

#[Route('/admin/example/tags', name: 'admin_example_tags', methods: ['GET'])]
#[CpAdminMenu(label: 'Tags', icon: 'heroicons:tag', priority: 41, capability: 'example.manage', parent: 'admin_example_index')]
public function tags(): Response { /* … */ }

Tags is shown only to a user who has both Examples and the example.manage permission.

Icons

The icon value is a symfony/ux-icons name; the core uses the heroicons: set (heroicons:home, heroicons:cog-6-tooth…). The default is heroicons:squares-2x2. If the icon name is wrong the menu does not break but the icon appears empty; before adding a new icon, open it once in the panel and look.

Order and groups

The smaller priority comes first (default 100). group gathers items under the same sidebar heading; if you give a heading the core uses (like studio.group.content), your item appears in that section. If the same module has several screens, give consecutive priorities (40, 41, 42) so that other modules' items do not slip in between.

Permissions

Resources/config/capabilities.yaml:

capabilities:
    - example.view
    - example.edit
    - example.manage

Use #[IsGranted('example.edit')] in the controller and is_granted('example.edit') in the template. Permissions are assigned to roles from AACP → Users.

Next: Forms and the Field API.

Last updated: 13.10.2026