Hooks Reference
This is the complete catalogue of extension points MultiSaas fires. Plugins and themes both extend the platform through this single contract. For the API itself and worked examples, see Plugin Development and Theme Development.
Removing or renaming an emitter tag silently breaks every listener — there is no error at edit time. The tags listed with a CRITICAL badge are protected by php artisan hook:audit and by the rule that you never remove a hook call while editing.
How the system works
All extensibility is on the WordPress-like hook bus:
add_action('tag', $cb, $priority) / do_action('tag', ...$args) / render_action('tag', ...$args)
add_filter('tag', $cb, $priority) / apply_filters('tag', $value, ...$args)
menu_insert_before / menu_insert_after / menu_insert_before_many / menu_insert
hook() (HookService) stores tag ⇒ priority ⇒ [callable]. Unknown tags are no-ops (do_action returns void, render_action returns '', apply_filters returns $value). Register callbacks from a service provider's boot(); the registry is per-request.
Naming: snake_case with a scope prefix. Generic frontend_* / admin_* tags fire on both landlord and tenant; scoped landlord_* / tenant_* tags fire only in that context.
Frontend hooks
| Tag | Type | Scope | Fired in |
|---|---|---|---|
frontend_head | action | both | landlord/frontend/partials/header.blade.php and tenant/frontend/partials/header.blade.php, before </head> |
landlord_frontend_head | action | landlord | landlord header, before </head> |
tenant_frontend_head | action | tenant | tenant header, before </head> (theme headerHook CSS/JS is emitted independently) |
landlord_frontend_header | action | landlord | after the navbar |
tenant_frontend_header | action | tenant | after the navbar |
frontend_footer | action | both | both footers, before </body> |
landlord_frontend_footer | action | landlord | landlord footer |
tenant_frontend_footer | action | tenant | tenant footer — Blog / Service / Product register here |
landlord_frontend_before_content / _after_content | action | landlord | frontend-page-master, around @yield('content') |
tenant_frontend_before_content / _after_content | action | tenant | same, tenant master |
tenant_frontend_navbar | action | tenant | tenant/frontend/partials/navbar.blade.php after the theme navbar fallback |
tenant_frontend_widget_area | action | tenant | tenant/frontend/partials/widget-area.blade.php — theme footerWidgetArea |
add_action('tenant_frontend_head', function () {
echo '<link rel="stylesheet" href="'.asset('plugin-assets/yourplugin.css').'">';
}, 10);
Admin panel hooks
| Tag | Type | Scope | Fired in |
|---|---|---|---|
admin_head | action | both | both admin headers, before </head> |
admin_footer | action | both | both admin footers, before </body> |
landlord_admin_head | action | landlord admin | landlord/admin/partials/header.blade.php |
landlord_admin_footer | action | landlord admin | landlord/admin/partials/footer.blade.php |
landlord_admin_before_content | action | landlord admin | admin-master, inside the content wrapper, before @yield('content') — every landlord admin page |
landlord_admin_after_content | action | landlord admin | same position, after the page content |
landlord_admin_menu_items | filter | landlord admin sidebar | SidebarMenuHelper::render_sidebar_menus() — positionable item array; anchor keys are item ids |
landlord_admin_menu | action | landlord admin sidebar | raw-HTML append after the rendered items |
tenant_admin_head | action | tenant admin | tenant/admin/partials/header.blade.php |
tenant_admin_footer | action | tenant admin | tenant/admin/partials/footer.blade.php |
tenant_admin_before_content | action | tenant admin | tenant/admin/admin-master.blade.php, before @yield('content') |
tenant_admin_after_content | action | tenant admin | same position, after the page content |
tenant_admin_menu_items | filter | tenant admin sidebar | SidebarMenuHelper::render_tenant_sidebar_menus() — positionable item array; item shape route, label, parent, icon, permissions, badge |
tenant_admin_menu | action | tenant admin sidebar | raw-HTML append after the rendered items |
Sidebar menu item shape
landlord_admin_menu_items uses the same shape as tenant_admin_menu_items (MenuWithPermission items). Use the menu helpers with anchor keys equal to item ids (dashboard-menu, general-settings-menu-items, appearance-settings-menu-items, languages, external menu ids):
add_filter('tenant_admin_menu_items', function (array $items) {
return menu_insert_before_many($items, 'appearance-settings-menu-items', [
'seo-manager-menu' => [
'route' => 'tenant.admin.seo-manager.settings',
'label' => __('SEO Manager'),
'parent' => null,
'permissions' => ['seo-manager'],
'icon' => 'mdi mdi-search-web',
'class' => 'plugin-menu',
],
]);
}, 10);
User dashboard menu hooks
The landlord user dashboard sidebar (landlord/frontend/user/dashboard/user-master.blade.php, web guard — fires only for authenticated users) and the tenant user dashboard mirror each other.
landlord_user_dashboard_menu_items (filter, preferred)
Core items in render order, with their anchor keys:
| Key | Item | Conditional |
|---|---|---|
dashboard | Dashboard | always |
wallet | My Wallet (submenu: My Wallet, Settings) | Wallet module enabled + option on |
payment_logs | Payment Logs | always |
custom_domain | Custom Domain | user owns a tenant |
support_tickets | Support Tickets | always |
edit_profile | Edit Profile | always |
change_password | Change Password | always |
logout | Logout | always |
add_filter('landlord_user_dashboard_menu_items', function (array $items) {
if (! auth()->user() || count(auth()->user()->tenant_details ?? []) < 1) {
return $items; // condition decides visibility
}
return menu_insert_after($items, 'custom_domain', 'my_domains', [
'label' => __('My Domains'),
'url' => route('landlord.user.dashboard.domains.index'),
'icon' => 'la-globe',
'active' => request()->routeIs('landlord.user.dashboard.domains.*'),
]);
}, 10);
When an anchor is optional (may be absent for some users), use menu_insert with fallbacks so the item stays pinned above a safe anchor:
return menu_insert($items, 'tolt_affiliate', [
'label' => __('Affiliate Earnings'), 'url' => route('landlord.user.dashboard.tolt.index'),
'icon' => 'la-handshake', 'active' => request()->routeIs('landlord.user.dashboard.tolt.*'),
], ['after' => 'payment_logs', 'before' => 'logout']);
Item shape: label, url, icon (a las la-* / la-* icon name), optional active (bool), optional children (array of ['label','url','active'] for submenus).
Other dashboard hooks
| Tag | Type | Scope |
|---|---|---|
landlord_user_dashboard_menu | action | landlord user dashboard — legacy raw-HTML append after Logout |
landlord_user_dashboard_before_content / _after_content | action | landlord user dashboard — around the whole sidebar + content wrapper |
tenant_user_dashboard_menu_items | filter | tenant user dashboard — anchors: dashboard, hotel_bookings, restaurant_orders, product_logs, donation_logs, plan_logs, event_logs, applied_jobs, appointments, support_tickets, edit_profile, change_password, logout |
tenant_user_dashboard_menu | action | tenant user dashboard — legacy raw-HTML append |
tenant_user_dashboard_before_content / _after_content | action | tenant user dashboard — around the whole wrapper |
hotel_bookings and restaurant_orders are module-gated and may be absent as anchors. The menu_insert helper appends when no anchor exists, which keeps the item visible instead of silently dropping it.
Landlord tenant-list hooks
landlord/admin/tenant/index.blade.php is fully hookable end-to-end:
| Tag | Type | Position / payload |
|---|---|---|
landlord_admin_tenant_list_before | action | Before the card |
landlord_admin_tenant_list_toolbar | action | Inside the toolbar |
landlord_admin_tenant_list_th | action | Inside <thead> — echo a <th> |
landlord_admin_tenant_list_cell | filter | Per row: ($html, $user) → extra <td> HTML |
landlord_admin_tenant_row_actions_after | action | Inside the row dropdown: ($user) |
landlord_admin_tenant_list_rows_after | action | After all rows in <tbody> |
landlord_admin_tenant_list_after_table | action | After the table element |
landlord_admin_tenant_list_after | action | After the whole card |
landlord_admin_tenant_pagination | filter | ($html) → modified pagination markup |
landlord_admin_tenant_list_scripts | action | After the page scripts section |
Pricing, plan and order hooks
Extension points on the landlord frontend purchase funnel (/pricing is the PageBuilder price-plan addon; details is view-plan; checkout is order-page):
| Tag | Type | Fired in | Payload |
|---|---|---|---|
landlord_pricing_plan_buttons | filter | PageBuilder/views/landlord/addons/common/price-plan.blade.php — after each plan's default buttons | ($html, $price_plan_item) → HTML appended to the button wrapper |
landlord_plan_view_top | action | view-plan.blade.php — top of the pricing-details section | — |
landlord_plan_view_bottom | action | same file — bottom of the section | — |
landlord_plan_features | filter | same file — the features list | ($html, $order_details) — return non-empty HTML to replace the default list; '' for the default |
landlord_order_form_top | action | order-page.blade.php — inside the order form, after @csrf | ($order_details) |
landlord_order_form_bottom | action | same file — right after </form> | ($order_details) |
landlord_order_gateways | filter | same file — the payment-gateway block | ($html, $order_details) — wraps render_payment_gateway_for_form() output |
add_filter('landlord_pricing_plan_buttons', function ($html, $plan) {
return $plan->price == 0 ? $html.'<span class="badge">Free forever</span>' : $html;
}, 10, 2);
Fallback template filters
Fallback frontend templates (used when the active theme does not ship a view) expose filters so plugins can replace or decorate whole sections without touching core views. When the filter returns empty, the fallback renders its own default.
| Tag | Type | Scope | Controls |
|---|---|---|---|
shop_grid_products | filter | tenant | Product grid HTML in fallbacks/frontend/shop/* |
shop_sidebar_filters | filter | tenant | Shop sidebar filter block |
shop_before_details | action | tenant | Before the product details section |
blog_sidebar | filter | tenant | Blog sidebar block |
event_grid | filter | tenant | Event listing grid |
Never hard-reference a plugin/module view namespace in a fallback without a view_exists() guard. An unregistered namespace throws "No hint path defined"; the helper catches and returns false.
Payment gateway hooks
| Tag | Type | Fires when | Contract |
|---|---|---|---|
landlord_payment_gateways / tenant_payment_gateways | filter | The registry builds the gateway list (lazy, once per request) | (array $gateways) — add $gateways[$slug] = […definition…], return the array |
{scope}_gateway_credentials_{slug} | filter | Settings UI or your charge handler needs saved credentials | (array $credentials) → key/value array |
{scope}_gateway_settings_save_{slug} | action | Admin submits the generic settings form | (array $data) — persist the POST (field names follow {slug}_{field}) |
{scope}_gateway_ipn_{slug} | action | Provider calls the generic IPN endpoint | (Request $request) → return redirect()/Response via dispatchActionResponse |
{scope}_gateway_payment_complete | action | After gateway_mark_payment_complete() records the payment | ($order_id, $transaction_id) |
{scope}_gateway_checkout_options | filter | Checkout page builds the gateway selection list | (array $options) → modified options |
Critical tags (money movement): landlord_payment_gateways, tenant_payment_gateways, tenant_gateway_payment_complete, landlord_gateway_payment_complete. Plugins must never remove the filter or the IPN dispatch — payments silently stop.
Public helpers: gateway_registry(), gateway_currency(), gateway_success_url($scope,$id), gateway_cancel_url($scope), gateway_ipn_url($slug), gateway_mark_payment_complete($scope,$id,$txn), gateway_charge_context($params).
Response conventions:
| Callsite | Must return | If not |
|---|---|---|
| Charge handler | RedirectResponse / Response | The calling flow has nothing to redirect to |
| IPN closure | redirect() / Response (returned, not echoed) | No callback → 404; returns nothing → core falls back to the cancel URL |
| Settings save closure | Nothing (plain action) | — |
| Checkout options / credentials filters | The (modified) array | Forgetting return silently empties the list |
Theme-local hooks
Every theme ships these placeholders (empty string when no plugin listens). hook:audit verifies them.
| Location | Tag | Purpose |
|---|---|---|
headerNavbarArea/navbar.blade.php top | theme_before_navbar | Inject above the navbar. |
headerNavbarArea/navbar.blade.php bottom | theme_after_navbar | Inject below the navbar. |
footerWidgetArea/widget-area.blade.php top | theme_before_footer_widgets | Inject above the footer widgets. |
footerWidgetArea/widget-area.blade.php bottom | theme_after_footer_widgets | Inject below the footer widgets. |
These are generic — a plugin listening on theme_before_navbar fires for every theme. For theme-scoped hooks use ThemeHookService::themeTag($slug, 'before_navbar').
Hooks fired by shipped plugins
These are stable extension points you can consume today.
DomainReseller
| Tag | Type | Payload | Fires when |
|---|---|---|---|
domain_providers | filter | (array $providers) | The provider registry is built — add/replace a reseller provider |
domain_dns_provider | filter | (mixed $class, string $domain) | DNS provider resolved for a domain — return a class to override |
domain_purchase_body | filter | (array $body, array $domain_data) | Just before the purchase API call |
domain_dns_record_set | action | ($domain, $type, $name, $value, $result) | After a DNS record is created/updated |
domain_dns_record_deleted | action | ($domain, $recordId, $result) | After a DNS record is deleted |
domain_whois_privacy_toggled | action | ($domain, bool $enable, $result) | After WHOIS privacy is toggled |
domain_wallet_deducted | action | ($userId, $amount, $domainPaymentLogId) | After a successful wallet deduction |
ToltAffiliate
ToltAffiliate consumes several hooks and fires none of its own yet; it registers on landlord_admin_menu_items, landlord_user_dashboard_menu_items, landlord_gateway_payment_complete (to report completed orders to Tolt), landlord_frontend_head and landlord_frontend_footer (the tolt.js pixel and lead binding).
Firing your own hooks
- Prefix with your slug:
yourplugin_after_enrollment. - Fire filters wherever a value could plausibly be overridden:
$title = apply_filters('yourplugin_widget_title', $title, $widget);. - For template sections, use
render_actionin your Blade. - Document every hook you add (tag, type, payload, firing location) in the same format as this page.
do_action('yourplugin_after_enrollment', $enrollment, $user);
add_action('yourplugin_after_enrollment', function ($enrollment, $user) {
// react
}, 10);
Verifying and debugging
# Full audit: emitters × listeners × per-theme hooks
php artisan hook:audit
php artisan hook:audit --verbose # prints the file:line table
php artisan hook:audit --theme=agency # one theme
php artisan hook:audit --theme=my-theme # third-party theme in themes/my-theme/
# Raw grep used to generate the audit
grep -rn "do_action\|render_action\|apply_filters" core --include="*.php" --include="*.blade.php" | sort
grep -rn "add_action\|add_filter" core plugins --include="*.php" | grep -v "function add_" | sort
App\Services\Hook\MandatoryHooks::all() is the machine-readable source of truth consumed by hook:audit. If you add a new render_action or apply_filters in core, add its tag to MandatoryHooks and to this page in the same commit.
Hook registration is per-request, so after editing a provider run php artisan view:clear; opcache can otherwise serve stale provider code.

