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
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:
- Is the module active? Items of an inactive module are not shown.
- Is there permission? If
capabilityis empty, being signed in is enough. If you writea|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. - 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.
Hiding from the menu does not close access. Always put #[IsGranted(...)] on every admin action; the menu is only a guide.
Submenu example
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.