The CMS Framework exposes a small set of WordPress‑style hooks and events to allow packages and applications to extend behavior without tightly coupling code.
This guide documents the available hooks grouped by module.
Namespace normalized in 2.5.0 (#193, #194, #195) — most infrastructure, lifecycle, and ability hooks moved onto the
ap.cmsFramework.*(orap.rbac.*) namespace. Every pre-2.5.0 name below is registered as a WordPress-style alias, so existing subscribers keep firing; the old names emit a one-per-request deprecation log entry so downstreams can migrate at their own pace.
Core: Assets
Filters for modifying enqueued assets before retrieval:
- ap.cmsFramework.admin.enqueuedAssets (was
ap.admin.enqueuedAssets) - ap.cmsFramework.public.enqueuedAssets (was
ap.public.enqueuedAssets) - ap.cmsFramework.auth.enqueuedAssets (was
ap.auth.enqueuedAssets)
Each filter receives the current associative array of assets and should return the modified array.
addFilter('ap.cmsFramework.admin.enqueuedAssets', function (array $assets) {
$assets['custom-admin'] = [
'path' => mix('js/custom-admin.js'),
'inFooter' => true,
];
return $assets;
});
Users: Settings UI
- ap.users.settings.sections (filter)
Add or modify sections (tabs) for your user settings UI.
apRegisterUserSettingsSection('profile', 'Profile', 10);
// Internally this uses the following filter:
addFilter('ap.users.settings.sections', function (array $sections) {
$sections['profile'] = ['label' => 'Profile', 'order' => 10];
return $sections;
});
Users: Model Events
Actions fired when roles or permissions are registered via managers:
- ap.rbac.roleRegistered (action, was
ap.roleRegistered) - ap.rbac.permissionRegistered (action, was
ap.permissionRegistered)
addAction('ap.rbac.roleRegistered', function ($role) {
// React to role creation/registration
});
addAction('ap.rbac.permissionRegistered', function ($permission) {
// React to permission creation/registration
});
Content: Lifecycle
Added in 2.5.0 (#196).
Every Post and Page model emits lifecycle actions via the FiresLifecycleHooks concern under src/Modules/ContentTypes/Models/Concerns/:
- ap.cmsFramework.{post,page}.saving
- ap.cmsFramework.{post,page}.saved
- ap.cmsFramework.{post,page}.published — fires only on a transition into
ContentStatus::Published: first-save-as-published, draft→published, and scheduled→published all count; subsequent saves of an already-published record do not. - ap.cmsFramework.{post,page}.trashed — soft delete only; force-delete is skipped.
- ap.cmsFramework.{post,page}.restored
Admin: Dashboard Widgets
Added in 2.5.0 (#196).
AdminWidgetManager::getAvailableWidgetsForUser() runs its output through ap.cmsFramework.admin.dashboardWidgets, passing the resolved user (or null) so subscribers can make per-user injections without re-resolving auth.
addFilter('ap.cmsFramework.admin.dashboardWidgets', function (array $widgets, ?User $user) {
if ($user?->hasRole('editor')) {
$widgets['editorial-queue'] = new EditorialQueueWidget();
}
return $widgets;
}, 10, 2);
Admin: Page Middleware
Added in 2.13.0 (#357).
AdminPageManager::registerRoutes() passes the cms.admin.middleware config through ap.cmsFramework.admin.middleware before applying it to every admin page route. Each page's can:{capability} gate is appended after the filtered stack. The config is normalized before the filter runs (non-string and empty entries dropped, an empty or null config replaced with ['web', 'auth']), so callbacks always receive an array. The filtered result is normalized the same way; only an empty result falls back to ['web', 'auth'].
addFilter('ap.cmsFramework.admin.middleware', function (array $middleware) {
$middleware[] = 'verified';
return $middleware;
});
Plugins: Hook Registration
Added in 2.5.0 (#196).
PluginManager::activate() fires ap.cmsFramework.plugin.hookRegistered immediately after the plugin's service provider registers, carrying (string $pluginSlug, array $hooks). The hooks array is the optional hooks field from the plugin's plugin.json (empty array when absent) so observers still get a per-plugin signal.
Plugins: Skipped Version Checks
Added in 2.14.0 (#372).
PluginManager::activate() fires ap.cmsFramework.plugin.versionCheckSkipped when a plugin declares min_framework_version (or the deprecated min_host_version) or min_app_version, but the version to compare against is unknown or not a comparable semver. Activation goes ahead. The payload is (string $pluginSlug, array $details), where $details holds constraint (framework or app), required_version, host_version (null when unresolved) and reason (unresolved or not_semver). A warning is logged as well; listen to the action to surface the skip in an admin UI. See Versioning & host compatibility.
Plugins: Deletion and Uninstall
PluginManager::delete() fires ap.cmsFramework.plugin.deleting with (string $slug) before teardown and ap.cmsFramework.plugin.deleted with (string $slug) once the row and files are gone. A deleting listener only runs if the plugin's service provider booted in the same request, which is usually not the case for a plugin deactivated earlier. For cleanup a plugin owns, declare an uninstall class in plugin.json instead (2.13.0, #358); see Uninstalling.
Search: Query
Added in 2.5.0 (#196).
HasContentFilters::applySearchFilter() — used by both BlogManager::getArchiveQuery() and PageManager::getPageQuery() — runs the assembled search query through ap.cmsFramework.search.query with (Builder $q, string $term, array $context) where $context carries the calling manager class, the queried model class, and the full filter array. Enough for a subscriber to swap in a full-text index or route to an external search service.
Site Editor: Reverts
Added in 2.16.0 (#423).
Reverting a template, template part or global-styles override, or unassigning a menu location, fires ap.cmsFramework.siteEditor.reverted with (string $type, ?string $slug, string $theme) after the row is deleted. The row is deleted through its model, so eloquent.deleting / eloquent.deleted fire too. See Site Editor: Hooks and Events.
Conventions
- Filters should return the modified value; actions return void.
- Hook names are namespaced with the
ap.prefix. - Prefer kebab‑case segments for readability.
Utilities
The examples above assume the presence of the following global helpers (provided by the artisanpack-ui/hooks package):
- addFilter(string $hook, callable $callback)
- applyFilters(string $hook, mixed $value): mixed
- addAction(string $hook, callable $callback)
- doAction(string $hook, ...$args): void
Settings: Registered Settings
- ap.settings.registeredSettings (filter)
Provide settings definitions from anywhere. Each item should include:
addFilter('ap.settings.registeredSettings', function (array $settings) {
$settings['site.title'] = [
'default' => 'My Site',
'type' => 'string',
'callback' => fn ($value) => trim((string) $value),
];
return $settings;
});
See the Settings module docs for details: Settings Hooks and Events.

