Skip to content
  1. Extras
  2. Reactions
  3. MODX events

MODX events ​

Reactions fires system events when reactions change. Use plugins to log, block selected objects, clear cache, or send notifications.

Outbound HTTP callbacks after success are separate: Webhooks. Built-in author messages use the reactions_notify_authors setting.

Event list ​

EventWhenCancellable
OnBeforeReactionAfter request validation, before writing to the DByes (cancel)
OnAfterReactionAfter a successful transaction (any action)no
OnReactionRemovedExtra when action = removedno
OnReactionChangedExtra when action = changedno

OnAfterReaction always runs on success. For remove and type change, a specialized event with the same parameters follows.

Registration: Elements → Plugins → System Events tab. Events are created on package install.


Server execution order ​

Typical path for POST/DELETE action=react:

text
1. Origin + CSRF + nonce (API layer)
2. IdentityResolver (fingerprint / user_id)
3. BotDetector + ReactionBan + RateLimiter
4. Object exists (short class_key → FQCN/STI + object_id)
5. Type active. Type ∈ set. For set=full — reactions_full_types
6. OnBeforeReaction  ← can cancel
7. DB transaction (insert / update / delete)
8. OnAfterReaction
9. OnReactionRemoved  or  OnReactionChanged  (by action)
10. AggregateService::recount
11. WebhookDispatcher (if enabled)
12. NotificationService (only for react/POST branch, not for the pure unreact notification path — see code)

Notes:

  • At steps 8–9 the aggregate is not recalculated yet. Do not read ReactionAggregate from an OnAfter* plugin for “fresh” numbers — recount yourself, use request / action fields, or subscribe to the webhook (it leaves after recount in the current service flow — the webhook gets a ReactionResult with updated counts).
  • OnBeforeReaction runs after rate limit and object checks. Cancel does not return the rate-limit “slot” for this request (the limit is already consumed).

Parameters ​

Parameters arrive in the plugin $scriptProperties and as local variables in plugin scope (MODX invokeEvent + compact).

DTO objects ​

Reactions\Dto\ReactionRequest (readonly):

PropertyTypeDescription
classKeystringe.g. modResource
objectIdintObject ID
typeNamestringType name (like, fire…)
contextstringMODX context
setKeystringSet key. May be empty until default resolve
allowMultipleboolFlag from the setting at request time

Reactions\Dto\VisitorIdentity (readonly):

PropertyTypeDescription
fingerprintstringStable fingerprint for reaction uniqueness
userId?intUser ID when authenticated
ipHash?stringIP hash (depends on strategy)
sessionId?stringSession ID (session strategy)

xPDO models:

  • Reactions\Model\ReactionType — type (name, emoji, active…)
  • Reactions\Model\ReactionSet — set (key, exclusive, active…)

OnBeforeReaction ​

KeyTypeDescription
requestReactionRequestRequest
identityVisitorIdentityWho reacts
typeReactionTypeType from DB
setReactionSetSet from DB

Cancel:

php
$modx->event->returnedValues['cancel'] = true;

The service throws ReactionNotAllowed → API responds 403 / code: forbidden with lexicon reactions_err_forbidden.

OnAfterReaction, OnReactionRemoved, OnReactionChanged ​

KeyTypeDescription
requestReactionRequestRequest
identityVisitorIdentityVisitor
typeReactionTypeType
actionstringadded | removed | changed

action values match the data.action field in the API response and webhook events (reaction.added, etc.).


Action values ​

actionTypical scenario
addedFirst reaction of this type (or a new one after removal)
removedToggle cleared the reaction, or an explicit DELETE
changedExclusive: was dislike, became like without a separate remove in the client response

For changed, OnAfterReaction + OnReactionChanged fire (not OnReactionRemoved).


Plugin examples ​

Logging ​

Event: OnAfterReaction.

php
<?php
/** @var \MODX\Revolution\modX $modx */
/** @var \Reactions\Dto\ReactionRequest $request */
/** @var \Reactions\Dto\VisitorIdentity $identity */
/** @var string $action */

$modx->log(
    \MODX\Revolution\modX::LOG_LEVEL_INFO,
    sprintf(
        '[Reactions] %s %s on %s#%d ctx=%s fp=%s user=%s',
        $action,
        $request->typeName,
        $request->classKey,
        $request->objectId,
        $request->context,
        $identity->fingerprint,
        $identity->userId ?? '-'
    )
);

Block reactions on unpublished resources ​

Event: OnBeforeReaction.

php
<?php
/** @var \MODX\Revolution\modX $modx */
/** @var \Reactions\Dto\ReactionRequest $request */

if ($request->classKey !== 'modResource'
    && $request->classKey !== 'MODX\\Revolution\\modResource'
) {
    return;
}

$resource = $modx->getObject(\MODX\Revolution\modResource::class, $request->objectId);
if (!$resource) {
    return;
}

if (!(bool) $resource->get('published') || (bool) $resource->get('deleted')) {
    $modx->event->returnedValues['cancel'] = true;
}

Only selected types on comments ​

php
<?php
/** @var \Reactions\Dto\ReactionRequest $request */

if ($request->classKey !== 'TicketComment') {
    return;
}

$allowed = ['like', 'dislike', 'funny'];
if (!in_array($request->typeName, $allowed, true)) {
    $modx->event->returnedValues['cancel'] = true;
}

Invalidate resource cache (Fenom / pdoTools) ​

Event: OnAfterReaction.

php
<?php
/** @var \MODX\Revolution\modX $modx */
/** @var \Reactions\Dto\ReactionRequest $request */

if ($request->classKey === 'modResource' || str_contains($request->classKey, 'modResource')) {
    $modx->cacheManager->refresh([
        'resource' => ['contexts' => [$request->context]],
    ]);
    // Or pointwise if your keys look like resource/{id}:
    // $modx->cacheManager->delete('resource/' . $request->objectId);
}

Exact keys depend on the site cache config.

Telegram notification (quick prototype) ​

Event: OnAfterReaction. Only added:

php
<?php
/** @var \MODX\Revolution\modX $modx */
/** @var string $action */
/** @var \Reactions\Dto\ReactionRequest $request */

if ($action !== 'added') {
    return;
}

$token = $modx->getOption('telegram_bot_token');
$chatId = $modx->getOption('telegram_chat_id');
if (!$token || !$chatId) {
    return;
}

$text = sprintf(
    'Реакция %s на %s #%d (%s)',
    $request->typeName,
    $request->classKey,
    $request->objectId,
    $request->context
);

$url = sprintf(
    'https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s',
    rawurlencode($token),
    rawurlencode((string) $chatId),
    rawurlencode($text)
);

@file_get_contents($url);

In production prefer webhooks or a queue: the plugin blocks the API response for the HTTP call duration.

“Likes only” counter in a TV ​

php
<?php
/** @var \MODX\Revolution\modX $modx */
/** @var string $action */
/** @var \Reactions\Dto\ReactionRequest $request */

if ($request->classKey !== 'modResource' || $request->typeName !== 'like') {
    return;
}

$resource = $modx->getObject(\MODX\Revolution\modResource::class, $request->objectId);
if (!$resource) {
    return;
}

$tv = (int) $resource->getTVValue('likes_mirror');
if ($action === 'added') {
    $tv++;
} elseif ($action === 'removed') {
    $tv = max(0, $tv - 1);
} else {
    return;
}

$resource->setTVValue('likes_mirror', $tv);

Watch races under load — more reliable to read the aggregate after recount (deferred bus / cron).


Relation to API and webhooks ​

LayerWhat happens
Plugin OnBeforeReaction + cancelHTTP 403, reaction not written, webhook not sent
Successful reactEvents → recount → webhook reaction.* → optional author message
WidgetDoes not see events directly. Sees JSON data.action / counts

Webhook payload signature: see Webhooks. Event names there use the reaction. prefix (reaction.added). In MODX they have no prefix (OnAfterReaction + action=added).


Debugging ​

  1. Set the log level to INFO / ERROR in MODX system settings.

  2. Check core/cache/logs/error.log (prefixes [Reactions], your plugin).

  3. Confirm the plugin is enabled and the event is checked.

  4. Cancel test: unpublished resource + the plugin above → API response:

    json
    {
      "success": false,
      "error": "…",
      "code": "forbidden"
    }
  5. To debug order, attach a temporary log to all four events and one curl to POST react.


Plugin checklist ​

  • Do not throw unhandled exceptions from after-hooks: they can become 500 after a committed transaction.
  • Do not run long HTTP/email synchronously in after — use a webhook or a queue.
  • Do not rely on “multiselect” by set name: multi only when !exclusive and reactions_allow_multiple.
  • For UI filters (&types=): the API only cuts reactions_full_types for set=full. Block other types via OnBeforeReaction.