WordPress-style API (core/src/Core/Hooks.php + global helpers in core/src/Support/hooks.php):
| Helper | Behavior |
|---|---|
add_action($hook, $cb, $priority = 10, $accepted_args = 1) |
Register side effect |
do_action($hook, ...$args) |
Fire actions |
add_filter($hook, $cb, $priority = 10, $accepted_args = 1) |
Register transform |
apply_filters($hook, $value, ...$args) |
Run filters; return value |
remove_action / remove_filter |
Unregister |
Lower priority runs first. Filters must return the value.
| Hook | Args | Call site | Purpose |
|---|---|---|---|
init |
Container $container, Config $config |
core/bootstrap.php |
App booted; extensions already loaded |
routes |
Router $router, Container $container |
core/bootstrap.php |
Register extra routes |
api_routes |
Router $router, Container $container |
core/bootstrap.php |
Register JSON API routes under /api (router already grouped) |
auth.login |
?array $member |
AuthController |
After successful login (post-2FA when required) |
auth.logout |
?array $member |
AuthController |
After logout |
install.complete |
(none) | InstallController |
After successful install |
cron |
Container $container |
bin/cron.php |
Scheduled jobs (also drains queue + prunes rate limits) |
migrate.complete |
Container $container, list<string> $versions |
bin/migrate.php |
After CLI migrate applies one or more versions (empty list not fired) |
queue.{type} |
array $payload, array $job |
Queue |
Handle a job pushed with $queue->push($type, $payload) |
| Hook | Value in → out | Extra args | Call site | Purpose |
|---|---|---|---|---|
admin_menu |
array $definitions |
?Role $role |
AdminController |
Add/reorder/remove sidebar items |
admin_capabilities |
list<string> $caps |
?Role $role |
AdminController |
Extend capability list for templates |
admin_page_data |
array $data |
string $template, string $activePath |
AdminController |
Inject admin shell template vars |
view.render |
array{name:string,data:array} |
— | View |
Alter template name or data before render |
mail.before_send |
array $payload |
— | SmtpMailer |
Alter to/subject/template/data/headers |
extension_enabled |
bool $enabled |
string $slug, Extension $extension |
ExtensionLoader |
Force enable/disable before boot (register before load()) |
request |
Request |
— | Kernel |
Inspect/replace request |
response |
Response |
Request |
Kernel |
Inspect/replace response |
api.version |
array{name,version,php} |
— | VersionController |
Alter /api/version payload |
Standalone link:
['link' => [
'href' => '/admin/x',
'label' => 'X',
'cap' => 'view_dashboard',
'icon' => 'layout-dashboard',
'order' => 15, // optional; lower = earlier within the section (default 50)
'badge' => 3, // optional; number or short string, right-aligned
'target' => '_blank', // optional; opens in a new tab
]]Order: items are sorted by order (ascending) within their section. Ties keep registration order. Core defaults leave gaps so you can insert without splicing:
| Item | Section | order |
|---|---|---|
| Dashboard | Menu | 10 |
| Extensions | Admin | 10 |
| Team Settings | Admin | 20 |
| Settings | System | 10 |
| (unspecified) | — | 50 |
Example — Visit Site above Dashboard:
add_filter('admin_menu', static function (array $nav): array {
$nav[] = [
'link' => [
'href' => '/',
'label' => 'Visit Site',
'cap' => 'view_dashboard',
'icon' => 'external-link',
'target' => '_blank',
'order' => 5, // before Dashboard (10)
],
];
return $nav;
});Optional section places the link under a labeled sidebar heading (default Menu). Example: 'section' => 'Admin'. Sections render as Menu → Admin → System, then any custom sections A–Z.
Optional target (e.g. '_blank') opens the link in a new tab; rel="noopener noreferrer" is added automatically.
Optional badge (or legacy badge_count) shows a pill on the right of the label. Falsy / 0 hides it.
icon may be a Lucide icon name (resolved to SVG) or raw <svg> markup. Browse names at https://lucide.dev/icons — helper: icon('name').
Group:
[
'group' => 'Tools',
'icon' => 'wrench',
'section' => 'Menu', // optional; default Menu
'order' => 30, // optional; group position within the section
'items' => [
['href' => '/admin/x', 'label' => 'X', 'cap' => 'manage_settings', 'badge' => 2, 'order' => 10],
],
]Group child order sorts within the group (default 50). Items without cap are shown to any authenticated admin who can see the group (use sparingly).
- Call
do_actionorapply_filtersat the seam in core. - Document it in this file (name, args, call site path).
- Prefer a filter when callers must transform a value; an action for side effects only.