PHP Hooks (Action & Filters)
- Server-Side (PHP): For data logic, saving, and rendering HTML.
- Client-Side (JS): For the Admin UI, Visual Builder tools, and Media Manager.
Use the Hook enum class (Hook::) to reference these hooks safely.
Core & Admin System Hooks
Filter Hooks (Modify Data)
These hooks expect you to return a modified value.
| Hook Name | Constant (Hook::) | Description |
|---|---|---|
| ADMIN_ENQUEUE_JS | ADMIN_ENQUEUE_JS | Modify JS files loaded on Admin pages (e.g., adding a new Editor.js tool). |
| ADMIN_ENQUEUE_CSS | ADMIN_ENQUEUE_CSS | Modify CSS files loaded on Admin pages. |
| REGISTER_DASHBOARD_WIDGETS | REGISTER_DASHBOARD_WIDGETS | Add, remove, or reorder widgets on the admin dashboard. |
| ADMIN_STATISTICS_SECTIONS | ADMIN_STATISTICS_SECTIONS | Add custom sections (e.g., iFrames) to the Statistics page. |
| BODY_END | BODY_END | Inject HTML/Scripts just before the </body> tag on the front-end. |
Code Example: Registering a CSS file for single entry pages
<?php
/**
* Adds the theme's base styles for pages of type 'single'.
*
* @param array $cssFiles The array of already registered CSS files.
* @param object $page The current page object.
* @return array The updated array of CSS files.
*/
function portfolioTheme_addSingleEntryStyles(array $cssFiles, $page): array {
// Add our file to the existing array.
// Using [] ensures we ADD and do not OVERWRITE.
$cssFiles[] = [
"href" => getAssetsPath("vendors/fontawesome-7/css/all.min.css", isTheme: true)
];
// IMPORTANT: Always return the modified array.
return $cssFiles;
}
// Tell HooksManager: "When 'enqueue_single_entry_css' is triggered,
// call the function 'portfolioTheme_addSingleEntryStyles'."
// The '10' is the priority (standard).
Hooks::addFilter(Hook::ENQUEUE_SINGLE_ENTRY_CSS, 'portfolioTheme_addSingleEntryStyles', 10);
?>
Code Example: Dashboard Widgets
<?php
Hooks::addFilter(Hook::REGISTER_DASHBOARD_WIDGETS, function(array $widgets) {
ob_start(); ?>
<div id="widget-custom-stats" class="card-admin span-2">
<h2>🛒 Store</h2>
<div class="stats-grid">
<div class="stat-item highlight">
<span class="label">Today's Sales</span>
<span class="value">14</span>
</div>
</div>
</div>
<?php
$widgets['my-extension-stats'] = [
'html' => ob_get_clean()
];
return $widgets;
});
Action Hooks (Trigger Logic)
These hooks do not return data.
| Hook Name | Constant (Hook::) | Description |
|---|---|---|
| AFTER_ENTRY_SAVE | AFTER_ENTRY_SAVE | Triggered after a successful save. Use for cache clearing, webhooks, or logging. |
| REGISTER_SYSTEM_NOTIFICATIONS | REGISTER_SYSTEM_NOTIFICATIONS | Inject warnings or info messages into the admin dashboard (e.g. "Missing API Key"). |
| POST_SITE_UPDATE | POST_SITE_UPDATE | Fired after updating a site to a new CMS version. Useful for re-indexing, publishing assets, or migrations. |
| RENDER_HEAD | RENDER_HEAD | Inject scripts or styles into the <head> of the public site. |
Code Example: Enqueueing a CSS file globally
// Add flag CSS via a hook, only if the theme/extension is active
Hooks::addAction(Hook::ENQUEUE_CSS, function($page) {
// Add flag CSS only if the theme/extension is active
$page->cssFiles[] = [
"href" => getAssetsPath("icons/Flag-Icons/flag-icons.min.css", isTheme: true)
];
});
Code Example: System Notifications
Hooks::addAction(Hook::REGISTER_SYSTEM_NOTIFICATIONS, function (bool $isPlatform) {
if (defined('APP_ENV') && APP_ENV === 'production' && defined('DEBUG') && DEBUG) {
NotificationManager::add(
'warning',
getTranslation('Debug mode is enabled in production'),
$isPlatform ? getPlatformLink('settings') : null
);
}
});
System & Configuration
These hooks allow you to modify the behavior of the CMS core and the global administration environment.
| Hook (Enum) | Type | Description |
|---|---|---|
SETTINGS_SCHEMA | Filter | High Level: Define custom settings fields. Handles both UI rendering and automatic data processing (encryption/sanitization). |
SETTINGS_BEFORE_SAVE | Filter | Low Level: Escape hatch triggered before saving. Use this to validate API keys, format complex arrays to JSON, or trigger side-effects (e.g., clearing cache). |
REGISTER_SYSTEM_NOTIFICATIONS | Action | Inject warnings or info messages into the admin dashboard (e.g., "Missing API Key"). |
ADMIN_SIDEBAR_MENU | Filter | Add, modify, remove, or reorder custom items in the main admin sidebar using a structured menu array (labels, icons, permissions, routes, order, etc.). |
Code Example: Adding a Stripe Billing section
The SETTINGS_SCHEMA hook is the "Single Source of Truth" for settings. You don't need to write any HTML or SQL saving logic; the Core does it for you based on the types you define.
<?php
Hooks::addFilter(Hook::SETTINGS_SCHEMA, function(array $schema) {
// Add a new "Billing" group
$schema['billing'] = [
'title' => getTranslation('Billing & Payments'),
'fields' => [
'stripe_publishable_key' => [
'type' => 'text',
'label' => 'Stripe Publishable Key'
],
'stripe_secret_key' => [
'type' => 'password', // Automatically encrypted in DB and hidden in UI
'label' => 'Stripe Secret Key'
],
'currency' => [
'type' => 'select',
'label' => 'Default Currency',
'options' => ['EUR' => 'Euro', 'USD' => 'US Dollar']
]
]
];
return $schema;
});
Code Example: Validating data before saving
If you need to verify that the Stripe Secret Key provided by the user is actually valid before saving it to the database, use SETTINGS_BEFORE_SAVE.
Hooks::addFilter(Hook::SETTINGS_BEFORE_SAVE, function(array $updates, array $postData) {
// Check if the user is trying to save a new Stripe Key
if (!empty($postData['stripe_secret_key'])) {
$isValid = StripeApi::verifyKey($postData['stripe_secret_key']);
if (!$isValid) {
// Log the error to the UI and remove the key from the update array
logError("The provided Stripe Secret Key is invalid.", forUser: true);
unset($updates['stripe_secret_key']);
}
}
return $updates;
});
Content & Entries
| Hook (Enum) | Type | Description |
|---|---|---|
BEFORE_ENTRY_SAVE | Filter | Modify entry data before it is written to the database. Useful for validation or auto-formatting. |
AFTER_ENTRY_SAVE | Action | Triggered after a successful save. Use for cache clearing, webhooks, or logging. |
ON_OBJECT_DELETE | Action | Fired when an object (entry, media) is deleted. Clean up relations (e.g. taxonomy terms). |
GET_RECOMMENDED_CONTENT | Filter | Override or enrich the recommended content for a given entry. |
ON_OBJECT_UPDATE_RECOMMENDATIONS | Action | Fired when an object's related recommendations are updated. |
GET_COMMENTS_FOR_ENTRY | Filter | Retrieve or modify the comment list (useful for integrating external systems). |
RENDER_EDITOR_BLOCK | Filter | Critical: Transform Editor.js JSON block data into HTML. Used by the frontend renderer. |
Code Example: Rendering Editor Block
Hooks::addFilter(Hook::RENDER_EDITOR_BLOCK, function($html, $type, $data) {
if ($type === 'myCustomTool') {
return '<div class="my-custom-block">' . htmlspecialchars($data['someData'] ?? '') . '</div>';
}
return $html;
});
Taxonomies
Manage how terms are linked, saved, or rendered.
| Hook (Enum) | Type | Description |
|---|---|---|
AFTER_HYDRATE_TAXONOMIES | Filter | Called after taxonomies have been added to each post. Modify, filter or enrich before frontend use. |
TAXONOMY_TERM_PAGE_HANDLERS | Filter | Provide handlers (public name, pagination, callbacks) for taxonomy term pages. |
ON_OBJECT_UPDATE_TERMS | Action | Fired when an object's taxonomy terms are updated. |
ON_TAXONOMY_TERM_SAVE | Action | Fired when a taxonomy term is created or updated. Ideal for Sitemaps or Cache purging. |
ON_TAXONOMY_TERM_DELETE | Action | Fired when a taxonomy term is deleted. |
Code Example: Hydrate Taxonomies: after_hydrate_taxonomies
This hook is called after taxonomies have been added to each post. It allows you to modify, filter or enrich the taxonomies before they are used by the application.
$entries = Hooks::applyFilters(Hook::AFTER_HYDRATE_TAXONOMIES, $entries, $contentType, $langId);
Example of use:
Hooks::addFilter(Hook::AFTER_HYDRATE_TAXONOMIES, function($entries, $contentType, $langId) {
foreach ($entries as &$entry) {
// Add a dynamically calculated tag
$entry['taxonomies'][] = 'custom-tag';
}
return $entries;}, 10, 3);
Code Example: Taxonomy Page Handlers
Hooks::addFilter(Hook::TAXONOMY_TERM_PAGE_HANDLERS, function(array $handlers): array {
$handlers['article'] = [
'public_name' => 'Articles',
'items_per_page' => 5,
'count_callback' => 'get_articles_count_by_ids',
'fetch_callback' => 'get_articles_data_by_ids',
'render_card_callback' => 'render_article_card_for_taxonomy'
];
return $handlers;
});
Fields (Custom Fields Pipeline)
Pragma CMS allows developers to create custom fields. We use a Single Source of Truth approach: you define the UI, the Database Storage, and the Security rules in one single hook pipeline.
1. Definition and Storage (FIELD_REGISTRY)
This step is unique and allows you to declare your custom field to the CMS whilst defining its behaviour in the database.
This filter allows you to add your field to the list of types available in the builders and immediately define how it will be stored (SQL). This is the first step in ensuring that your field is recognised by the CMS.
Hooks::addFilter(Hook::FIELD_REGISTRY, function(array $registry) {
// Definition of identity and SQL storage
$registry['google_maps_address'] = [
'label' => 'Google Maps Address', // Name in the interface
'icon' => 'place', // Material Symbol icon
'group' => 'Special Fields', // Category in the palette
'sql' => 'JSON NULL', // SQL type for Collections/Structures
'storage' => 'json', // Target column for Singles (text, number, json, etc.)
'sanitize' => 'json' // Sanitization strategy (string, int, bool, html, json)
];
return $registry;
});
UI Properties
- label: Display name shown in the field picker interface.
- icon: Material Symbol identifier displayed for the field.
- group: Category used to organize the field in the builder palette.
Storage Properties
- sql: Raw SQL definition used for
CREATEorALTERtable commands. - storage: For Single content types, defines which column of the
entries_fieldstable stores the data. - sanitize: Defines the cleaning strategy before saving.
- Available sanitize strategies:
string(default),int,float,bool,date,hash,slug,html,json,strict_json,deep_json.
- Available sanitize strategies:
Optional Behaviour Properties
- is_subfield_compatible (optional): Defaults to
true. Set tofalseto prevent the field from being used as a nested subfield.
2. Contextual Configuration (FINALIZE_CONTENT_TYPE_FIELD)
After saving, you can fine-tune the configuration of a field differently depending on whether it is used in a Content Type or a Form.
- Configuration Hooks (Filters)
These filters are called just after submission of the create/edit form (for a Content Type or a Form), for each field. They allow you to validate, sanitize, or enrich the configuration of a field before it is saved.
Note: While Content Types have access to the full suite of fields, the Form Builder provides a curated subset of fields specifically tailored for public data collection.FINALIZE_CONTENT_TYPE_FIELD: For fields in a Content Type.FINALIZE_FORM_FIELD: For fields in the Form Builder.
Example: Let’s add a default option only when our field is used in a Content Type.
Hooks::addFilter(Hook::FINALIZE_CONTENT_TYPE_FIELD, function($processedField, $rawFieldData) {
// Only target our custom field in this context
if ($processedField['type'] === 'google_maps_address') {
// Add a default zoom option if it is not defined.
// This option will exist only for Content Type fields.
if (!isset($processedField['options']['default_zoom'])) {
$processedField['options']['default_zoom'] = 14;
}
}
// Always return the configuration array, modified or not.
return $processedField;
});
3. Admin Rendering (RENDER_CUSTOM_FIELD_HTML)
These hooks define how the field is displayed in the admin interface and how the data is handled before saving.
This filter allows you to provide custom HTML for displaying the field in admin forms (content creation/editing).
Hooks::addFilter(Hook::RENDER_CUSTOM_FIELD_HTML, function($html, $handle, $config, $currentValue, $namePrefix) {
// Check if it's our Google Maps custom field
if ($config['type'] === 'google_maps_address') {
$fieldName = "{$namePrefix}[{$handle}]";
$id = "field-" . htmlspecialchars(
str_replace(['[', ']'], ['-', ''], $fieldName)
);
ob_start(); ?>
<div class="form-container">
<fieldset class="fieldset-card field-type-google-maps" id="container-<?= $id ?>">
<legend class="form-label" for="<?= $id ?>">
<?= htmlspecialchars($config['label']) ?>
</legend>
<input
type="text"
id="<?= $id ?>"
name="<?= htmlspecialchars($fieldName) ?>"
value="<?= htmlspecialchars($currentValue ?? '') ?>"
class="form-control google-maps-autocomplete-input"
placeholder="Search an address..."
>
<small class="form-help">
Start typing to search for an address using Google Maps.
</small>
</fieldset>
</div>
<?php
$html = ob_get_clean();
}
return $html;
}, 10); // 10 = priority
4. Data Processing (PROCESS_CUSTOM_FIELD_VALUE)
Pragma CMS uses two distinct filters to process data depending on whether it comes from an administrative entry or a public form submission.
A. Content Types (Back-office)
This filter intercepts data for Custom Fields in Content Types before it is saved to the database. It is ideal for server-side formatting or API calls during content creation.
Hooks::addFilter(Hook::PROCESS_CUSTOM_FIELD_VALUE, function($value, $handle, $config) {
if ($config['type'] === 'google_maps_address' && is_string($value)) {
// 1. Call the Google Geocoding API with the address ($value)
$geoData = callGoogleApi($value); // Hypothetical function
// 2. Return a structured array that will later be encoded as JSON
if ($geoData) {
return [
'address' => $value,
'lat' => $geoData['lat'],
'lng' => $geoData['lng']
];
}
}
// If it's not our field, return the original value unchanged
return $value;
}, 10);
B. Public Form Submissions (Front-office)
When using the Form Builder, you can intercept the submitted data for any field (native or custom). This is useful for late-stage validation, encryption, or cleaning data specifically for leads and contact requests.
// Processing for Form Builder submissions
Hooks::addFilter(Hook::FORM_PROCESS_FIELD_VALUE, function(array $result, array $fieldConfig) {
// $result contains ['handle' => '...', 'value' => '...']
if ($fieldConfig['field_type'] === 'phone') {
// Example: Force international format for all phone submissions
$result['value'] = formatToInternational($result['value']);
}
return $result;
});
5. Data Hydration (HYDRATE_FIELD_VALUE)
This filter lets you intercept a value after it has been retrieved from the database and before it is used by the application (e.g., passed to a template). It’s the ideal place to enrich the data, make API calls, or modify its structure.
Example:
Our google_maps_address field is stored as JSON with the address, latitude, and longitude. We want to add the current weather for that location each time an entry is loaded.
Hooks::addFilter(Hook::HYDRATE_FIELD_VALUE, function($value, $config, $entry) {
// Check that it's our field and that the value is an array (already decoded from JSON by the system)
if ($config['type'] === 'google_maps_address' && is_array($value) && isset($value['lat'])) {
// 1. Call a weather API with the coordinates
// A caching system should be used here to avoid overloading the API
$weatherData = getWeatherForCoordinates($value['lat'], $value['lng']); // Hypothetical function
// 2. Enrich the value by adding weather data
if ($weatherData) {
$value['weather'] = [
'temperature' => $weatherData['temp_celsius'],
'description' => $weatherData['summary']
];
}
}
// Always return the value (modified or not)
return $value;
}, 10, 3);
Template Usage Result:
Thanks to this strict pipeline, front-end developers receive a fully hydrated, safe-to-use object directly in their views:
// In a PHP template (e.g., home.php)
// Access the 'entry' array from the $page object: $page->entry["fields"]['my_address_field'];
$adresse = $page->entry["fields"]['mon_champ_adresse'];
echo htmlspecialchars($adresse['address']); // Displays "1600 Amphitheatre Parkway, Mountain View, CA"
echo htmlspecialchars($adresse['map_url']);
echo htmlspecialchars($adresse['lat']); // Displays "37.422"
echo htmlspecialchars($adresse['weather']['temperature']); // Displays "15°C"
Specific Field Implementations
Icon
Adding a custom icon library (FontAwesome, Lucide, etc.)
You can register any third-party icon library, in your theme for example, by using the JS method window.CMS.icons.registerProvider.
Step 1: Enqueue resources (PHP)
In your theme’s functions.php file, use the CMS hooks to inject the CSS files (for styling) and JS files (for the selector logic) only in the admin area.
/**
* Loading FontAwesome resources in the admin panel
*/
Hooks::addFilter(Hook::ADMIN_ENQUEUE_CSS, function($page) {
// Load the CSS to view the icons in the gallery
$page->cssFiles[] = [
'href' => getAssetsPath("vendors/fontawesome/css/all.min.css", isTheme: true)
];
return $page;
});
Hooks::addFilter(Hook::ADMIN_ENQUEUE_JS, function($page) {
// Load the JS script for your new provider
$page->jsFiles[] = [
'src' => getAssetsPath("js/fontawesome-provider.js", isTheme: true),
'defer' => true
];
return $page;
});
Step 2: Register the Provider (JS)
Create the relevant JavaScript file in your theme (e.g. assets/js/fontawesome-provider.js) and use the window.CMS.icons.registerProvider method.
/**
* Enregistrement du provider FontAwesome
*/
window.CMS.icons.registerProvider('fontawesome', 'Font Awesome 7', {
// Chemin vers le catalogue JSON des icônes de la librairie
path: window.CMS.baseUrl + "assets/themes/mon-theme/assets/vendors/fontawesome/icons.json",
// Charge la liste des icônes
async load() {
const response = await fetch(this.path);
const jsonData = await response.json();
// Retourne un tableau d'objets { name, styles }
return Object.keys(jsonData).map(iconName => ({
name: iconName,
styles: jsonData[iconName].styles
}));
},
// --- Affiche l'icône dans la grille de sélection ---
render(iconData, callback) {
const item = document.createElement('div');
item.className = 'grid-icon-item';
// --- Détermination du style et des noms ---
// On choisit un style par défaut. 'solid' est le plus courant.
// Si 'solid' n'est pas dispo, on prend le premier style de la liste.
const style = iconData.styles.includes('solid') ? 'solid' : iconData.styles[0];
const libraryKey = `fa-${style}`; // ex: fa-solid
const iconName = iconData.name; // ex: house
const iconClassName = `fa-${style} fa-${iconName}`;
// --- Affichage de l'icône dans la grille ---
item.innerHTML = `
<i class="${iconClassName}"></i>
<div class="icon-name">${iconName}</div>
`;
// --- Action au clic : transmet les données au CMS ---
item.addEventListener('click', () => {
const iconDataForForm = {
html: `<i class="${iconClassName}"></i>`, // Pour l'aperçu immédiat
library: libraryKey, // Stocké en BDD (ex: fa-solid)
name: iconName // Stocké en BDD (ex: house)
};
callback(iconDataForForm);
});
return item;
}
});
Step 3: Front-end rendering (PHP)
To display the icon on your public website, use the renderIcon() helper provided by the CMS in your Twig or PHP templates.
Twig example:
{# Retrieves the icon for a menu or content field #}
{{ renderIcon(item.icon_library, item.icon_name) }}
Gallery
You can add extra fields directly inside Gallery items.
- Hook for adding custom fields to gallery items (
render_gallery_item_fields)
This hook allows you to add additional fields to each gallery item (for example: category, tag, caption, etc.).
Hooks::addAction('render_gallery_item_fields', function($index, $baseName, $item) {
$key = 'category';
$value = $item[$key] ?? '';
echo "
<div class='form-input-group'>
<span>" . getTranslation('Category') . "</span>
<input
type='text'
name='{$baseName}[{$index}][{$key}]'
value='" . htmlspecialchars($value) . "'
>
</div>
";
});
- Processing hook (
process_custom_field_value)
Use this hook to sanitize and validate your custom “category” field before saving.
Hooks::addFilter('process_custom_field_value', function($value, $handle, $config) {
if ($config['type'] === 'gallery' && is_array($value)) {
// $value contains all gallery items
foreach ($value as &$item) {
$key = 'category';
if (isset($item[$key])) {
// Sanitize the category value
$item[$key] = trim(strip_tags($item[$key]));
}
}
unset($item);
}
return $value;
}, 10);
- Hydration hook (
hydrate_field_value)
Use this hook to transform the “category” data (for example, an ID) into a complete object or enriched structure.
Hooks::addFilter('hydrate_field_value', function($value, $config, $entry) {
if ($config['type'] === 'gallery' && is_array($value)) {
// $value contains gallery items already decoded from JSON
foreach ($value as &$item) {
// Enrich the item here
// Example:
// $item['category_object'] = getCategoryData($item['category']);
}
unset($item);
}
return $value;
}, 10);
Pages
Creating a Custom Editor.js Tool (JavaScript)
Imagine you want to create a simple “Google Maps” plugin. You can add a script like this:
Example file: components/editorjs/editor-map.js
/**
* ============================================================================
* EXAMPLE: CREATING A CUSTOM TOOL (SIMPLE GOOGLE MAPS)
* ============================================================================
*
* This file demonstrates how to add a new tool to the content editor.
* It follows the standard Editor.js API and integrates with the CMS plugin system.
*/
(function() { // IIFE to isolate scope and avoid variable conflicts
/**
* MapTool class.
* Must implement render() and save() methods.
*/
class MapTool {
/**
* Toolbox configuration ("+" menu).
* @return {object} - Title and SVG icon.
*/
static get toolbox() {
return {
title: 'Google Map',
// SVG icon (location pin)
icon: '<svg width="20" height="20" viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>'
};
}
/**
* Constructor called when the block is initialized.
*/
constructor({data, config, api}) {
this.data = {
lat: data.lat || '',
lng: data.lng || '',
zoom: data.zoom || 12
};
this.apiKey = config.apiKey || '';
this.wrapper = undefined;
}
/**
* Main method: builds the block UI in the admin.
*/
render() {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('custom-map-tool');
this.wrapper.style.padding = '15px';
this.wrapper.style.border = '1px solid #eee';
this.wrapper.style.backgroundColor = '#f9f9f9';
this.wrapper.innerHTML = `
<div style="font-weight:bold; margin-bottom:10px; color:#555;">📍 Map Settings</div>
<div style="display:flex; gap:10px;">
<input type="text" class="cdx-input map-lat" placeholder="Latitude (e.g. 48.8566)" value="${this.data.lat}">
<input type="text" class="cdx-input map-lng" placeholder="Longitude (e.g. 2.3522)" value="${this.data.lng}">
</div>
<div style="margin-top:10px; font-size:0.8em; color:#888;">
API Key: ${this.apiKey ? 'Yes (Hidden)' : 'Not defined'}
</div>
`;
return this.wrapper;
}
/**
* Save method: extracts data from the DOM into JSON.
*/
save(blockContent) {
const latInput = blockContent.querySelector('.map-lat');
const lngInput = blockContent.querySelector('.map-lng');
return {
lat: latInput.value,
lng: lngInput.value,
zoom: this.data.zoom
};
}
}
// Register tool in CMS
window.CMS = window.CMS || {};
window.CMS.editorTools = window.CMS.editorTools || {};
window.CMS.editorTools.map = {
class: MapTool,
tunes: ['containerTune'],
inlineToolbar: true,
config: {
apiKey: 'AIzaSyD...'
}
};
})();
Registering the Tool in the CMS (PHP Hooks)
Add the JS script to the Pages CRUD:
Hooks::addFilter(Hook::ADMIN_ENQUEUE_JS, function($page) {
// Load only on create/edit pages
if ($page->currentRoute["handle"] === "admin.pages.create" || $page->currentRoute["handle"] === "admin.pages.edit") {
$page->jsFiles[] = [
"src" => getAssetsPath(
"js/components/editorjs/map-tool.js",
isTheme: false,
extensionName: "my-extension"
),
"defer" => true
];
}
return $page;
});
You can also add CSS using the same approach (ADMIN_ENQUEUE_CSS).
Rendering the saved block (frontend)
Hooks::addFilter(Hook::RENDER_EDITOR_BLOCK, function($html, $type, $data) {
if ($type === 'myCustomTool') {
return '<div class="my-custom-block">' . htmlspecialchars($data['someData']) . '</div>';
}
return $html;
});
Forms
1. Hooks PHP (Backend)
Use the Hooks class and the Hook enum to interact with the form lifecycle.
| Hook (Enum) | Type | Description |
|---|---|---|
FORM_BUILDER_PALETTE | Filter | Allows you to add new field types to the admin field palette (for example: a “Map” or “Rating” field). |
FORM_BEFORE_VALIDATION | Filter | Triggered before validation starts. Useful for sanitizing or formatting raw data (for example: normalizing a phone number). |
FORM_CUSTOM_VALIDATION | Filter | Allows you to add advanced business validation rules and return custom validation errors (for example: checking if an email already exists in the users database). |
FORM_PROCESS_FIELD_VALUE | Filter | Modifies the final field value right before saving (for example: encrypting sensitive data or transforming text to uppercase). |
FORM_AFTER_SUBMISSION | Action | The most important hook. Triggered after a successful submission save (and SQL transaction). Ideal for third-party integrations such as Zapier, Mailchimp, Slack, or CRM systems. |
Example: Mailchimp integration for a newsletter signup
Hooks::addAction(Hook::FORM_AFTER_SUBMISSION, function($submissionId, $data, $formConfig, $files) {
// Only trigger for the "newsletter" form (adjust as needed)
if ($formConfig['form_handle'] !== 'newsletter') {
return;
}
// Retrieve Mailchimp settings stored in your CMS configuration
global $site;
$apiKey = $site->settings['mailchimp_api_key'] ?? '';
$listId = $site->settings['mailchimp_list_id'] ?? '';
// Basic safety checks
if (!$apiKey || !$listId) {
logError("Mailchimp: Missing API key or list ID in settings.");
return;
}
if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
logError("Mailchimp: Invalid or missing email for submission $submissionId.");
return;
}
// Extract datacenter from API key (e.g. xxxx-us8 → us8)
$dc = substr($apiKey, strpos($apiKey, '-') + 1);
// Member hash used by Mailchimp to uniquely identify a subscriber
$memberHash = md5(strtolower($data['email']));
// Mailchimp "PUT" endpoint (Create or Update subscriber)
$url = "https://{$dc}.api.mailchimp.com/3.0/lists/{$listId}/members/{$memberHash}";
// Payload (use "status_if_new" with PUT)
$payload = json_encode([
'email_address' => $data['email'],
'status_if_new' => 'subscribed', // Only applied if the member does not exist
'merge_fields' => [
'FNAME' => $data['firstname'] ?? '',
'LNAME' => $data['lastname'] ?? ''
]
]);
// Prepare the request
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_USERPWD, "user:{$apiKey}"); // Basic Auth: username is irrelevant
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); // The key for upsert behavior
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
// Execute the call
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
// Handle cURL errors
if ($curlErr) {
logError("Mailchimp cURL error: $curlErr");
return;
}
// Handle API errors
if ($httpCode >= 400) {
logError("Mailchimp API Error ($httpCode): " . $response);
return;
}
// Success (optional: log quietly for debugging)
logError("Mailchimp: {$data['email']} successfully synced to list {$listId}.", false);
});
Example: CRM integration
Hooks::addAction(Hook::FORM_AFTER_SUBMISSION, function($submissionId, $data, $formConfig, $files) {
if ($formConfig['form_handle'] === 'quote_request') {
// Send data to Salesforce / HubSpot / custom CRM
MyCrmService::createLead([
'email' => $data['email'],
'name' => $data['name'],
'budget' => $data['budget']
]);
}
});
2. Hooks JavaScript (Admin UI)
The form builder exposes a global registry that allows plugins and extensions to render their own configuration interfaces for custom field types.
Entry point : window.CMS.fieldRenderers
Example: Adding a custom UI for a “Rating” field
// In your plugin JavaScript file
document.addEventListener('DOMContentLoaded', () => {
window.CMS.fieldRenderers['rating'] = function(fieldId, data) {
// Return the custom configuration UI HTML
return `
<div class="form-container">
<label>Maximum number of stars</label>
<input
type="number"
class="form-control val-max-input"
value="${data.validation_rules?.match(/max:(\d+)/)?.[1] || 5}"
>
<input
type="hidden"
name="fields[${fieldId}][validation_rules]"
class="validation-rules-final"
>
</div>
`;
};
});