Core Hooks & Filters Reference: Complete Developer Guide
Core Hooks & Filters Reference
Audience: Developer — Requires PHP / Laravel knowledge. Category: Technical Reference — Technical documentation for integrated programmers.
What you will learn
- Understand the Action vs Filter mechanism in PolyCMS
- Look up hook name, parameters, dispatch location
- Know how to register hooks from theme
functions.phpand moduleServiceProvider - Use priority to control execution order
- Apply actual use-cases to each hook group
1. Introducing the Hook system
PolyCMS uses a Hook/Filter system similar to WordPress, built natively on Laravel:
- Action (
Hook::doAction): Run side-effects (logging, sending emails, synchronizing data). No need to return. - Filter (
Hook::applyFilters): Get a value, edit, and force return the adjusted value.
Basic syntax
use App\Facades\Hook;
// Sign up for Action
Hook::addAction('order_completed', function ($order) {
Mail::to($order->user)->send(new OrderCompletedMail($order));
}, priority: 10);
// Sign up for Filter
Hook::addFilter('post.default_image', function (?string $imageUrl, $post) {
if ($post?->categories->contains('slug', 'news')) {
return '/images/news-default.jpg';
}
return $imageUrl;
}, priority: 10);
2. Content & Posts
Filters
| Hook | Parameters | Description |
|---|---|---|
post.default_image |
$imageUrl, $context |
Default image when the article does not have a featured image |
post.frontend_url |
$url, $post |
Customize the post's public URL |
post.content.render |
$html, $post |
Filter HTML content before returning API |
post.query.builder |
$query, $request |
Edit Eloquent query for list of posts |
category.frontend_url |
$url, $category |
Customize your category's public URL |
content.render.blocks |
$blocks |
Filter the block array before rendering |
content.render.html |
$html, $blocks |
Filter final output HTML |
content.render.block.{type} |
$html, $block |
Render or override a specific block type |
Actions
| Hook | Parameters | Description |
|---|---|---|
tag.saved |
$tag, $context |
After the tag is created/updated |
tag.deleted |
$tag, $context |
After the tag is deleted |
category.saved |
$category, $context |
After the category is created/updated |
category.deleted |
$category, $context |
After the category is deleted |
Practical use-cases
UC1 — Representative image by category: The news site wants each category to have its own default image when the article has not set a featured image:
Hook::addFilter('post.default_image', function (?string $url, $post) {
if ($post?->categories->contains('slug', 'tech')) return '/images/defaults/tech.jpg';
if ($post?->categories->contains('slug', 'lifestyle')) return '/images/defaults/lifestyle.jpg';
return $url; // fallback admin setting
});
UC2 — Wiki URL: Document theme wants to move post URL from /posts/slug to /docs/slug:
Hook::addFilter('post.frontend_url', function ($url, $post) {
return ($post->type === 'wiki') ? '/docs/' . $post->slug : $url;
});
UC3 — Automatically insert ads: The advertising module wants to insert a banner after the 3rd paragraph in the article content:
Hook::addFilter('content.render.html', function (string $html) {
$adBanner = '<div class=\"ad-inline\">Advertisement</div>';
$parts = explode('</p>', $html, 4);
if (count($parts) > 3) {
$parts[2] .= '</p>' . $adBanner;
return implode('</p>', $parts);
}
return $html;
});
3. Media Library
Filters
| Hook | Parameters | Description |
|---|---|---|
media.upload.file |
$file, $data |
Edit files before processing |
media.upload.data |
$data, $file |
Edit upload metadata |
media.create.data |
$mediaData, $file |
Adjust record data before saving DB |
media.delete.should |
$shouldDelete, $media |
Gate: return false to prevent deletion |
media.url |
$url, $media |
Rewrite URL (e.g. CDN) |
Actions
| Hook | Parameters | Description |
|---|---|---|
media.uploaded |
$media, $file, $data |
After successful upload |
media.deleting |
$media |
Before deleting |
media.deleted |
$media |
After deleting |
Practical use-cases
UC1 — CDN rewrite: Integrates Cloudflare R2/S3, automatically converts media URLs to CDN domain:
Hook::addFilter('media.url', function (string $url, $media) {
return str_replace('/storage/', 'https://cdn.mysite.com/', $url);
});
UC2 — Protect important files: Prevent deletion of images being used as site logos:
Hook::addFilter('media.delete.should', function (bool $allow, $media) {
$logoUrl = get_option('site_logo', null, 'general');
return ($media->url === $logoUrl) ? false : $allow;
});
UC3 — Auto-optimize when uploading: Automatically create WebP from uploaded images:
Hook::addAction('media.uploaded', function ($media, $file) {
if (str_starts_with($media->mime_type, 'image/')) {
dispatch(new ConvertToWebpJob($media));
}
});
4. E-Commerce — Orders & Refunds
Actions
| Hook | Parameters | Description |
|---|---|---|
order_status_updated |
$order, $oldStatus, $newStatus |
When order status changes |
order_completed |
$order |
When the order is completed |
order.refund.processing |
$order, $validated |
Before making a refund |
order.refund.completed |
$order, $result |
After successful refund |
order.refund.succeeded |
$order, $result, $validated, $userId |
API refund successful |
order.refund.failed |
$order, $validated, $exception, $userId |
API refund failed |
Filters
| Hook | Parameters | Description |
|---|---|---|
order.refund.preview.result |
$preview, $order, $input |
Adjust refund preview |
Practical use-cases
UC1 — Telegram notification when there is a new order:
Hook::addAction('order_status_updated', function ($order, $old, $new) {
if ($old === 'pending' && $new === 'processing') {
TelegramBot::send("Order #{$order->code} has been confirmed!");
}
});
UC2 — Earn loyalty points when completing an application:
Hook::addAction('order_completed', function ($order) {
if ($order->user_id) {
$points = (int) floor($order->total / 10000); // 1 point per 10k
LoyaltyService::addPoints($order->user_id, $points, "Order #{$order->code}");
}
});
UC3 — Record audit log when refund fails:
Hook::addAction('order.refund.failed', function ($order, $data, $exception, $userId) {
AuditLog::create([
'action' => 'refund_failed',
'order_id' => $order->id,
'user_id' => $userId,
'reason' => $exception->getMessage(),
]);
});
5. E-Commerce — Cart, Shipping, Tax, Inventory
Filters
| Hook | Parameters | Description |
|---|---|---|
cart.totals |
$totals, $cart |
Adjust cart total |
shipping.calculate_cost |
$cost, $method, $cart |
Override shipping fees |
shipping.available_methods |
$methods, $address, $cart |
Filter shipping methods |
tax.calculated |
$result, $subtotal, $address |
Override tax calculation results |
inventory.is_stockable_product |
$default, $product, $context |
Identify products with inventory management |
review.can_submit |
$allowed, $user, $product |
Gate: does it allow reviews? |
Actions
| Hook | Parameters | Description |
|---|---|---|
cart.item.added |
$item, $cart |
After adding items to cart |
cart.item.updated |
$item, $oldQty, $newQty |
After updating the quantity |
cart.item.removed |
$item, $cart |
After removing items from cart |
cart.cleared |
$cart |
After deleting the entire cart |
cart.merged |
$userCart, $guestCart |
When merging the guest cart into the user cart |
review.submitted |
$review |
After submitting a review |
review.approved |
$review |
After reviewing reviews |
review.rejected |
$review |
After rejecting the review |
Practical use-cases
UC1 — Free shipping for orders over 500k:
Hook::addFilter('shipping.calculate_cost', function ($cost, $method, $cart) {
$subtotal = collect($cart->items)->sum(fn($i) => $i->price * $i->quantity);
return ($subtotal >= 500000) ? 0 : $cost;
});
UC2 — Block reviews if you haven't purchased yet:
Hook::addFilter('review.can_submit', function (bool $allowed, $user, $product) {
$hasPurchased = Order::where('user_id', $user->id)
->where('status', 'completed')
->whereHas('items', fn($q) => $q->where('product_id', $product->id))
->exists();
return $hasPurchased;
});
UC3 — Facebook Pixel tracking when adding cart:
Hook::addAction('cart.item.added', function ($item, $cart) {
session()->push('fb_pixel_events', [
'event' => 'AddToCart',
'product_id' => $item->product_id,
'value' => $item->price,
]);
});
6. E-Commerce — Payment Gateways
Filters
| Hook | Parameters | Description |
|---|---|---|
payment.gateway.config_schema |
$schema, $gateway |
Expand the gateway configuration schema |
Use-case: Add "Branch Code" field for Bank Transfer gateway:
Hook::addFilter('payment.gateway.config_schema', function ($schema, $gateway) {
if ($gateway->code === 'bank_transfer') {
$schema['branch_code'] = [
'type' => 'text', 'label' => 'Branch code', 'required' => false,
];
}
return $schema;
});
7. Theme & Appearance
Filters
| Hook | Parameters | Description |
|---|---|---|
theme.view.data |
$data, $viewName |
Inject data into any view |
theme.template.resolve |
$viewName, $templateTheme, $entityType, $entity |
Override Blade view is rendered |
theme.template.registry |
$templates, $viewType |
Register new page template |
theme.options.values |
$options |
Filter the theme options |
theme.options.css_vars |
$cssVars, $themeOptionValues |
Adjust CSS custom properties |
theme.breadcrumbs.post |
$breadcrumbs, $post |
Edit article breadcrumb |
theme.breadcrumbs.product |
$breadcrumbs, $product |
Edit product breadcrumbs |
theme.show_page_header |
$show, $page |
Hide/show page header |
frontend.topbar.banners |
$banners |
Inject advertising banners |
themes.list |
$themes |
Filter admin theme list |
Actions
| Hook | Parameters | Description |
|---|---|---|
theme.activated |
$theme |
When the main theme is loaded |
theme.main.changed |
$theme, $oldMainTheme |
When changing the main theme |
theme.installing |
$file |
Before processing ZIP theme |
theme.activating |
$slug, $type, $mode |
Before activating the theme |
theme.deactivating |
$slug |
Before deactivating theme |
theme.deleting |
$theme |
Before deleting the theme |
cms_head |
(none) | Output hook in <head> |
Practical use-cases
UC1 — Site-wide promotional banner from module:
Hook::addFilter('frontend.topbar.banners', function (array $banners) {
$banners[] = [
'text' => ' Flash Sale — 30% off today!',
'url' => '/sale',
'bg_color' => '#ff4444',
];
return $banners;
});
UC2 — Inject Google Analytics into <head>:
Hook::addAction('cms_head', function () {
$ga = get_option('google_analytics_id', null, 'seo');
if ($ga) {
echo "<script async src='https://www.googletagmanager.com/gtag/js?id={$ga}'></script>";
}
});
UC3 — Inject sidebar data for blog page:
Hook::addFilter('theme.view.data', function ($data, $viewName) {
if ($viewName === 'posts.index') {
$data['popular_posts'] = Post::orderBy('views', 'desc')->limit(5)->get();
}
return $data;
});
8. Settings & Configuration
Filters
| Hook | Parameters | Description |
|---|---|---|
settings.defaults |
$defaults, $settingsService |
Extend/override setting definitions |
settings.media.drivers |
$drivers |
Register new storage driver |
settings.permalinks.structure |
$structure, $settingsService |
Adjust permalink structure |
Actions
| Hook | Parameters | Description |
|---|---|---|
setting.updating |
$key, $value, $group, $type |
Before saving settings |
settings.saved |
$payload |
After saving settings |
Practical use-cases
UC1 — Module registers individual settings on Settings page:
Hook::addFilter('settings.defaults', function ($defaults) {
$defaults['mymodule'] = [
'mymodule_api_key' => [
'key' => 'mymodule_api_key', 'value' => '', 'type' => 'text',
'label' => 'API Key', 'description' => 'Enter your API key',
],
];
return $defaults;
});
UC2 — Clear cache when updating permalink:
Hook::addAction('settings.saved', function ($payload) {
if (($payload['group'] ?? '') === 'permalinks') {
Artisan::call('route:clear');
Cache::tags('routes')->flush();
}
});
9. Users, Roles & Auth
Filters
| Hook | Parameters | Description |
|---|---|---|
user.resource.to_array |
$data, $user, $request |
Expand user API response |
auth.login.pre_token |
$response, $user, $request |
Intercept login (for 2FA) |
Actions
| Hook | Parameters | Description |
|---|---|---|
user.creating / user.updating / user.deleting |
$user|$data |
CRUD lifecycle |
role.creating / role.updating / role.deleting / role.cloning |
$role|$data |
CRUD lifecycle |
Practical use-cases
UC1 — Required 2FA for admin:
Hook::addFilter('auth.login.pre_token', function ($response, $user, $request) {
if ($user->hasRole('admin') && !$request->filled('otp_code')) {
return response()->json(['requires_2fa' => true, 'user_id' => $user->id], 403);
}
return $response; // null = continue creating tokens
});
UC2 — Send welcome email when creating user:
Hook::addAction('user.creating', function ($data) {
// Queued email will be sent after the user is saved
dispatch(new SendWelcomeEmail($data['email'], $data['name']));
});
10. SEO
Filters
| Hook | Parameters | Description |
|---|---|---|
seo.canonical_url |
$url |
Override canonical URL |
seo.site_favicon |
$iconUrl |
Override favicon URL |
Use-case — Canonical URL for multi-language:
Hook::addFilter('seo.canonical_url', function (string $url) {
$locale = app()->getLocale();
if ($locale !== 'en') {
return url("/{$locale}" . parse_url($url, PHP_URL_PATH));
}
return $url;
});
11. Widgets
Filters
| Hook | Parameters | Description |
|---|---|---|
widget.render.instance |
$instance |
Adjust widget instance before rendering |
widget.render.{widget_type} |
$widget |
Edit specific widget data |
widget.area.render.instances |
$instances, $area |
Filter instances in area |
widget.render.output |
$html, $instance |
Filter HTML output widget |
widget.area.render.output |
$html, $area |
Filter HTML output area |
widgets.types |
$widgets |
Filter widget types admin |
Actions
| Hook | Parameters | Description |
|---|---|---|
widgets.register_types |
$widgetManager |
Register new type widget |
widgets.register_areas |
$widgetManager |
Register new widget area |
Use-case — Register widget type "Store Map":
Hook::addAction('widgets.register_types', function ($manager) {
$manager->registerType('store_map', [
'label' => 'Store Map',
'description' => 'Google Maps showing store locations',
'fields' => [
'api_key' => ['type' => 'text', 'label' => 'Google Maps API Key'],
'lat' => ['type' => 'text', 'label' => 'Latitude'],
'lng' => ['type' => 'text', 'label' => 'Longitude'],
],
'view' => 'widgets.store-map',
]);
});
12. Admin Navigation & Editor
Filters
| Hook | Parameters | Description |
|---|---|---|
topbar.menu.items |
$items, $request, $user |
Add/remove frontend topbar item |
topbar.menu.should_show |
$show, $user |
Toggle display topbar |
admin.editor.panels |
$panels, $type, $user |
Add custom panel to block editor |
Actions
| Hook | Parameters | Description |
|---|---|---|
admin.menu.build |
(none) | When the sidebar admin menu is built |
topbar.menu.context |
$request, $user |
When the topbar context is initialized |
Use-case — Module adds menu item "SEO Score" to the topbar:
Hook::addFilter('topbar.menu.items', function (array $items, $request, $user) {
$items[] = [
'key' => 'seo_score',
'label' => 'SEO Score',
'icon' => 'chart-bar',
'url' => '/admin/seo/score',
'position' => 50,
];
return $items;
});
13. Modules
Filters
| Hook | Parameters | Description |
|---|---|---|
module.resource.meta |
$meta, $moduleData |
Expand the metadata module |
modules.list |
$modulesArray |
Filter the admin module list |
product.query.builder |
$query, $request |
Adjust Eloquent query for products |
Actions
| Hook | Parameters | Description |
|---|---|---|
module.activating |
$moduleKey, $module |
Before activating module |
module.deactivating |
$moduleKey, $module |
Before deactivating module |
module.deleting |
$moduleKey, $module |
Before deleting module |
module.installing |
$uploadedFile |
When installing the ZIP |
Use-case — Run migration when activating module:
Hook::addAction('module.activating', function ($moduleKey, $module) {
Artisan::call('migrate', [
'--path' => "modules/Polyx/{$moduleKey}/database/migrations",
'--force' => true,
]);
});
14. System Bootstrap
Actions
| Hook | Parameters | Description |
|---|---|---|
roles.register_permissions |
$permissionRegistry |
Register custom permissions |
register_email_templates |
$emailTemplateManager |
Sign up for email templates |
layout.register_assets |
$layoutAssetManager |
Register layout assets |
routes.frontend.register |
(none) | Register additional frontend routes |
Use-case — Private permission registration module:
Hook::addAction('roles.register_permissions', function ($registry) {
$registry->register('manage analytics', 'Analytics', 'View and manage analytics dashboard');
$registry->register('export reports', 'Analytics', 'Export analytics reports to CSV');
});
Related articles
- Hooks & Filters Overview — Event system overview
- Theme Development — Build theme
- Module Development — Module development
- REST API Authentication — API Authentication
PolyCMS is an open-source content management system for modern web applications, inspired by the WordPress plugin and theme ecosystem but built on top of the Laravel framework. It is designed to provide a complete foundation for content publishing, e-commerce, multi-language support, and extensible module architecture — powered by a Vue 3 admin panel with data served entirely through RESTful APIs.
Whether you're building a blog, a documentation site, an online store, or a multi-tenant SaaS platform, PolyCMS aims to give you a comprehensive starting scaffold so you can ship quickly and extend easily through integrated modules and themes. In particular, themes in PolyCMS follow a multi-theme architecture — one Main theme and an unlimited number of Sub themes can run side by side on the same installation.
We hope this ready-made foundation proves useful for building your next website, blog, or web app, saving you from having to start completely from scratch.