MODX Custom Plugin Development
A MODX plugin is PHP code bound to system events. It intercepts lifecycle moments of the CMS: page loading, resource saving, authentication, errors, caching. Unlike a snippet, a plugin does not return content — it changes system behavior.
Event Binding
// AutoSEO Plugin — automatically fills SEO fields
// Events: OnBeforeDocFormSave (before saving resource in manager)
$eventName = $modx->event->name;
switch ($eventName) {
case 'OnBeforeDocFormSave':
handleBeforeSave($modx, $resource, $mode);
break;
case 'OnDocFormSave':
handleAfterSave($modx, $resource, $mode);
break;
}
function handleBeforeSave($modx, $resource, $mode): void {
// Auto-fill description from introtext
if (empty($resource->get('description')) && !empty($resource->get('introtext'))) {
$description = mb_substr(strip_tags($resource->get('introtext')), 0, 160);
$resource->set('description', $description);
}
// Auto-fill longtitle from pagetitle
if (empty($resource->get('longtitle'))) {
$resource->set('longtitle', $resource->get('pagetitle'));
}
}
Popular MODX Events
| Event | When | Usage |
|---|---|---|
| OnPageNotFound | 404 error | Custom 404, redirects |
| OnHandleRequest | Every request | Redirects, geolocation |
| OnLoadWebDocument | Resource loading | Content modification |
| OnBeforeDocFormSave | Before manager save | Validation, auto-fill |
| OnDocFormSave | After save | External system sync |
| OnUserLogin | User login | Logging, 2FA |
| OnCacheUpdate | Cache clear | CDN synchronization |
| OnSiteRefresh | Settings reset | Deploy hooks |
301 Redirect Plugin
<?php
// Redirects Plugin
// Event: OnPageNotFound
$uri = $_SERVER['REQUEST_URI'];
$redirects = $modx->getOption('custom_redirects', null, '');
// Redirects stored as JSON in system setting
$redirectMap = json_decode($redirects, true) ?? [];
foreach ($redirectMap as $from => $to) {
if (strpos($uri, $from) === 0) {
$target = str_replace($from, $to, $uri);
$modx->sendRedirect($target, ['responseCode' => 'HTTP/1.1 301 Moved Permanently']);
exit;
}
}
// Show custom 404 page
$errorPage = $modx->getOption('error_page', null, 404);
$modx->sendForward($errorPage, ['response_code' => 'HTTP/1.1 404 Not Found']);
Geolocation Plugin
<?php
// GeoRedirect Plugin
// Event: OnHandleRequest
if ($modx->context->key === 'web') {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
// Only on first visit (no cookie)
if (!isset($_COOKIE['geo_redirect_done'])) {
$geoData = getGeoByIp($ip);
$currentUri = $_SERVER['REQUEST_URI'];
$countryContextMap = ['BY' => 'by', 'UA' => 'ua', 'KZ' => 'kz'];
$targetContext = $countryContextMap[$geoData['country']] ?? null;
if ($targetContext && $modx->context->key !== $targetContext) {
setcookie('geo_redirect_done', '1', time() + 86400, '/');
$modx->sendRedirect('/' . $targetContext . $currentUri);
exit;
}
}
}
function getGeoByIp(string $ip): array {
// MaxMind GeoIP2 or ipapi.co
$response = file_get_contents("https://ipapi.co/{$ip}/json/");
return json_decode($response, true) ?? ['country' => 'RU'];
}
CRM Synchronization Plugin
<?php
// CRMSync Plugin
// Event: OnDocFormSave
if ($modx->event->params['mode'] === modSystemEvent::MODE_UPD
&& in_array($resource->get('template'), [5, 6])) { // only specific templates
$crmApiKey = $modx->getOption('crm_api_key');
$crmApiUrl = $modx->getOption('crm_api_url');
$data = [
'id' => $resource->id,
'title' => $resource->get('pagetitle'),
'url' => $modx->makeUrl($resource->id, '', '', 'full'),
'price' => $resource->getTVValue('price'),
'category' => $resource->get('parent'),
'updated' => time(),
];
$ch = curl_init($crmApiUrl . '/products/' . $resource->id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $crmApiKey,
],
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
$modx->log(modX::LOG_LEVEL_ERROR, "CRM sync failed for resource {$resource->id}: {$response}");
}
}
Plugin Storage in File
// In plugin editor: File Source = file system
// File: core/components/myplugin/myplugin.plugin.php
// This allows versioning the plugin in git
The StaticElements plugin synchronizes plugins between database and filesystem.
Timeline
Plugin development for one or two events — 0.5–1 day. Complex plugin with multiple events and external integrations — 2–4 days.







