Skip to content
  1. Extras
  2. MiniShop3
  3. Development
  4. Events
  5. Plugin guide

Working with plugins ​

A plugin on MiniShop3 events: how to get the parameters, return data, abort an operation and pass data to another plugin.

Component plugin (MODX events) ​

The package ships a system plugin minishop3.php on core MODX events (not to be confused with msOn*):

MODX eventPurpose
OnMODXInitService registration, namespace
OnLoadWebDocumentFrontend assets, registerFrontend()
OnDocFormSaveResource-to-product conversion via ProductService::handleConversion()
OnBeforeUserFormSave / OnUserSaveSync msCustomer ↔ modUser when ms3_customer_sync_enabled
OnUserRemoveCleanup of related customer data

For cart and order business logic, subscribe to events from the events hub.

Basic plugin structure ​

php
<?php
/**
 * MiniShop3 plugin
 * Events: msOnBeforeAddToCart, msOnAddToCart
 */

switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        // Code for "before add" event
        break;

    case 'msOnAddToCart':
        // Code for "after add" event
        break;
}

Getting incoming parameters ​

Method 1: $scriptProperties array ​

All event parameters are in $scriptProperties:

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        $product = $scriptProperties['msProduct'];
        $count = $scriptProperties['count'];
        $options = $scriptProperties['options'];
        $controller = $scriptProperties['controller'];
        break;
}

Method 2: extract() ​

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        extract($scriptProperties);
        // Now available: $msProduct, $count, $options, $controller
        break;
}

Debug: list all parameters ​

php
<?php
$modx->log(modX::LOG_LEVEL_ERROR, '[' . $modx->event->name . '] Parameters: ' . print_r(array_keys($scriptProperties), true));
$modx->log(modX::LOG_LEVEL_ERROR, '[' . $modx->event->name . '] ' . print_r($scriptProperties, true));

controller parameter ​

Passed automatically

Every MiniShop3 controller event receives a controller parameter — the instance of the current controller (Cart, Order, Customer).

The controller gives access to MiniShop3 and MODX:

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        /** @var \MiniShop3\Controllers\Cart\Cart $controller */
        $controller = $scriptProperties['controller'];

        $ms3 = $controller->ms3;
        $modx = $controller->modx;
        // Use public controller methods for cart data
        break;
}

Aborting the operation ​

An event with the Before prefix is aborted by returning an error message:

$modx->event->output() ​

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        $product = $scriptProperties['msProduct'];

        if ($product->get('price') <= 0) {
            $modx->event->output('Product is not available for order');
            return;
        }

        if ($scriptProperties['count'] > 10) {
            $modx->event->output('Maximum 10 units per product');
            return;
        }
        break;
}

How it works ​

  1. MiniShop3's invokeEvent() collects all plugin outputs
  2. If any output is non-empty, the operation is aborted
  3. The message is returned to the user (frontend or API response)

Events that support aborting ​

  • msOnBeforeGetCart
  • msOnBeforeAddToCart
  • msOnBeforeChangeInCart
  • msOnBeforeChangeOptionsInCart
  • msOnBeforeRemoveFromCart
  • msOnBeforeEmptyCart
  • msOnBeforeAddToOrder
  • msOnAddToOrder
  • msOnBeforeRemoveFromOrder
  • msOnBeforeEmptyOrder
  • msOnEmptyOrder
  • msOnSubmitOrder
  • msOnBeforeCreateOrder
  • msOnBeforeChangeOrderStatus
  • Other events with the Before prefix

Modifying data ​

returnedValues ​

To change the parameters the core continues with:

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        $values = &$modx->event->returnedValues;

        if ($scriptProperties['count'] < 2) {
            $values['count'] = 2;
        }

        $options = $scriptProperties['options'] ?? [];
        $options['source'] = 'promo_landing';
        $values['options'] = $options;
        break;
}

Take returnedValues by reference (&) ​

php
// Correct — changes apply
$values = &$modx->event->returnedValues;
$values['count'] = 5;

// Wrong — changes are lost
$values = $modx->event->returnedValues;
$values['count'] = 5;

Utils::invokeEvent and EventGate ​

Most cart, order, cost and customer events go through MiniShop3\Utils\Utils::invokeEvent(), not raw $modx->invokeEvent(). Core clears returnedValues before the call and normalizes the response:

Response fieldMeaning
successtrue when no plugin returned non-empty output
messagePlugin output strings joined with <br/>
dataOriginal $scriptProperties shallow-merged with returnedValues
valuesRaw merged returnedValues from all plugins

Shallow merge: if two plugins set returnedValues['count'], the plugin with higher priority wins (MODX runs plugins in ascending priority order; later plugins overwrite keys).

Two ways to change data ​

  1. By reference in $scriptProperties (the older MS2 way; still works where params are passed with &).
  2. returnedValues — preferred contract since 1.11+.
php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        $values = &$modx->event->returnedValues;
        $values['count'] = min(99, (int) $scriptProperties['count']);
        break;
}

Callers read $response['data']['count'] after merge.

Abort: output, false and cancel ​

SignalWhereEffect
$modx->event->output('text')Utils::invokeEventsuccess = false, text in message
$modx->event->output(false) or output('cancel')import, notifications, some raw callsEventGate::isCancelled() → true

A plugin's return cancels nothing

Only what a plugin passed to output() reaches the event result: modX::invokeEvent() collects $this->event->_output, while the plugin's return value is used for something else — a string is written to error.log as an event error, false as "Plugin … failed!".

So return 'cancel' not only fails to cancel, it also pollutes the log. Cancel through output() and use return only to leave the switch.

For isCancelled(), false and the string 'cancel' are equivalent. What the cancellation means is decided by the calling code:

EventWhat the core does on cancellation
msOnBeforeImportThe whole import aborts with the ms3_utilities_import_cancelled error
msOnImportRowThe current row goes into the skipped counter, the import continues
msOnBeforeSendNotificationThe notification is not sent, no error is raised
php
<?php
case 'msOnImportRow':
    if (empty($scriptProperties['data']['price'])) {
        $modx->event->output('cancel');
        return;
    }
    break;

After-events in the order manager

msOnCreateOrderProduct, msOnUpdateOrderProduct, msOnRemoveOrderProduct run after save() / remove(). A plugin error is only logged, and the HTTP response to the client stays successful. The operation can be aborted only in the paired msOnBefore*. See Order product events.

Named returnedValues channels (import, msProducts, notifications) ​

Some events apply EventGate::applyReturnedArray() per key instead of a global merge:

EventreturnedValues keyBehavior
msOnBeforeImportparamslist → replace; assoc → array_replace
msOnImportRowdata, tvData, optionData, gallerysame
msOnProductsLoadrowssame
msOnProductPreparerowsame
msOnBeforeSendNotificationrecipient, channelssame; list channels replaces the channel list

Import / msProducts / notifications use EventGate::invokeRaw(), not Utils::invokeEvent().

Passing data between plugins ​

Use $modx->eventData to pass data between plugins of the same event or between events in one request.

Example: price modification ​

php
<?php
switch ($modx->event->name) {
    case 'msOnGetProductPrice':
        $price = $modx->eventData['msOnGetProductPrice']['price']
            ?? $scriptProperties['price'];

        $data = $scriptProperties['data'];

        if ($data['parent'] == 5) {
            $price = $price * 0.85;
        }

        $modx->eventData['msOnGetProductPrice']['price'] = $price;
        break;
}

Example: passing data between events ​

php
<?php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        $modx->eventData['myPlugin']['original_count'] = $scriptProperties['count'];
        $modx->eventData['myPlugin']['added_at'] = time();
        break;

    case 'msOnAddToCart':
        $originalCount = $modx->eventData['myPlugin']['original_count'] ?? null;
        $addedAt = $modx->eventData['myPlugin']['added_at'] ?? null;

        if ($originalCount) {
            $modx->log(modX::LOG_LEVEL_INFO,
                "Product added. Original quantity: {$originalCount}, time: {$addedAt}"
            );
        }
        break;
}

eventData structure ​

php
$modx->eventData = [
    'msOnGetProductPrice' => [
        'price' => 1500.00,
    ],
    'msOnGetProductFields' => [
        'data' => [...],
    ],
    'myPlugin' => [
        'custom_key' => 'custom_value',
    ],
];

Data lives for one request

$modx->eventData is cleared after the request. Use $_SESSION or the database to persist data between requests.

Showing messages in the manager ​

JavaScript alert via addHtml ​

php
<?php
switch ($modx->event->name) {
    case 'msOnManagerCustomCssJs':
        $page = $scriptProperties['page'];

        if ($page === 'product_update') {
            $controller = $scriptProperties['controller'];

            $controller->addHtml('<script>
                Ext.onReady(function() {
                    MODx.msg.alert("Notice", "Remember to check prices!");
                });
            </script>');
        }
        break;
}

Toast notifications ​

php
<?php
switch ($modx->event->name) {
    case 'msOnManagerCustomCssJs':
        $page = $scriptProperties['page'];
        $controller = $scriptProperties['controller'];

        if ($page === 'orders') {
            $controller->addHtml('<script>
                Ext.onReady(function() {
                    Ext.Msg.notify("MiniShop3", "You have new orders!");
                });
            </script>');
        }
        break;
}

Plugin priority ​

Execution order is determined by the plugin priority setting:

  • Lower value = runs first
  • Default: 0
  • Any integer

When priority matters ​

  1. Price modification — plugins apply discounts in sequence
  2. Validation — one plugin may depend on another's result
  3. Data passing — source plugin must run before consumer

Debugging plugins ​

Logging ​

php
<?php
$modx->log(modX::LOG_LEVEL_ERROR, 'Error');
$modx->log(modX::LOG_LEVEL_WARN, 'Warning');
$modx->log(modX::LOG_LEVEL_INFO, 'Info');
$modx->log(modX::LOG_LEVEL_DEBUG, 'Debug');

$modx->log(modX::LOG_LEVEL_ERROR,
    '[MyPlugin::' . $modx->event->name . '] ' . $message
);

Checking whether the plugin fires ​

php
<?php
// At the top of the plugin
$modx->log(modX::LOG_LEVEL_ERROR, '=== PLUGIN START: ' . $modx->event->name . ' ===');

switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        $modx->log(modX::LOG_LEVEL_ERROR, 'msOnBeforeAddToCart triggered');
        // ...
        break;
}

$modx->log(modX::LOG_LEVEL_ERROR, '=== PLUGIN END ===');

Listing the plugins subscribed to an event ​

php
<?php
// Fetch every plugin subscribed to the event
$plugins = $modx->getCollection('modPluginEvent', [
    'event' => 'msOnBeforeAddToCart'
]);

foreach ($plugins as $pluginEvent) {
    $plugin = $pluginEvent->getOne('Plugin');
    $modx->log(modX::LOG_LEVEL_INFO,
        'Plugin: ' . $plugin->get('name') . ', priority: ' . $pluginEvent->get('priority')
    );
}

Common mistakes ​

1. Forgot return after output() ​

php
// Wrong — execution continues
if ($error) {
    $modx->event->output('Error');
}
// This runs even on error
doSomething();

// Correct
if ($error) {
    $modx->event->output('Error');
    return;
}

2. Not using a reference for returnedValues ​

php
// Wrong
$values = $modx->event->returnedValues;
$values['count'] = 10; // Changes are lost

// Correct
$values = &$modx->event->returnedValues;
$values['count'] = 10;

3. Checking in an after-event instead of a before-event ​

php
// msOnAddToCart — event AFTER add.
// The product is already in the cart and output() will not take it back.
// The response still turns into an error, so the shopper sees a message
// while the cart has already changed
case 'msOnAddToCart':
    $modx->event->output('Error');
    break;

// msOnBeforeAddToCart — event BEFORE add
case 'msOnBeforeAddToCart':
    $modx->event->output('Error'); // Stops the add
    break;

A cart that disagrees with the response is worse than an honest refusal: the frontend shows an error while the item counter has already gone up. Put the checks in msOnBefore*.

4. Forgot break in switch ​

php
switch ($modx->event->name) {
    case 'msOnBeforeAddToCart':
        // code
        // Forgot break — next case runs too!

    case 'msOnAddToCart':
        // This also runs for msOnBeforeAddToCart
        break;
}

Full plugin example ​

php
<?php
/**
 * Plugin: Cart add control
 *
 * Events: msOnBeforeAddToCart, msOnAddToCart
 *
 * - Disallow adding products without price
 * - Cap maximum quantity
 * - Log adds
 */

switch ($modx->event->name) {

    case 'msOnBeforeAddToCart':
        /** @var \MiniShop3\Model\msProduct $product */
        $product = $scriptProperties['msProduct'];
        $count = $scriptProperties['count'];
        $options = $scriptProperties['options'] ?? [];

        if ($product->get('price') <= 0) {
            $modx->event->output($modx->lexicon('ms3_cart_product_not_available'));
            return;
        }

        $maxCount = 99;
        if ($count > $maxCount) {
            $values = &$modx->event->returnedValues;
            $values['count'] = $maxCount;

            $modx->eventData['cartControl']['count_limited'] = true;
            $modx->eventData['cartControl']['original_count'] = $count;
        }

        $values = &$modx->event->returnedValues;
        $options['added_timestamp'] = time();
        $values['options'] = $options;
        break;

    case 'msOnAddToCart':
        $product = $scriptProperties['msProduct'];
        $count = $scriptProperties['count'];
        $productKey = $scriptProperties['product_key'];

        $modx->log(modX::LOG_LEVEL_INFO, sprintf(
            '[CartControl] Product added: %s (ID: %d), qty: %d, key: %s',
            $product->get('pagetitle'),
            $product->get('id'),
            $count,
            $productKey
        ));

        if (!empty($modx->eventData['cartControl']['count_limited'])) {
            $original = $modx->eventData['cartControl']['original_count'];
            $modx->log(modX::LOG_LEVEL_WARN, sprintf(
                '[CartControl] Quantity capped: requested %d, added %d',
                $original,
                $count
            ));
        }
        break;
}