
Working with plugins
Guide to creating MiniShop3 event plugins: getting parameters, returning data, aborting operations, passing data between plugins.
Component plugin (MODX events)
The package ships a system plugin minishop3.php on core MODX events (not to be confused with msOn*):
| MODX event | Purpose |
|---|---|
OnMODXInit | Service registration, namespace |
OnLoadWebDocument | Frontend assets, registerFrontend() |
OnDocFormSave | Resource-to-product conversion via ProductService::handleConversion() |
OnBeforeUserFormSave / OnUserSave | Sync msCustomer ↔ modUser when ms3_customer_sync_enabled |
OnUserRemove | Cleanup of related customer data |
For cart and order business logic, subscribe to events from the events hub.
Basic plugin structure
<?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
switch ($modx->event->name) {
case 'msOnBeforeAddToCart':
$product = $scriptProperties['msProduct'];
$count = $scriptProperties['count'];
$options = $scriptProperties['options'];
$controller = $scriptProperties['controller'];
break;
}Method 2: extract()
<?php
switch ($modx->event->name) {
case 'msOnBeforeAddToCart':
extract($scriptProperties);
// Now available: $msProduct, $count, $options, $controller
break;
}Debug: list all parameters
<?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
Important
In all MiniShop3 controller events the controller parameter is passed automatically — the current controller instance (Cart, Order, Customer).
Through the controller you can access:
<?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
For events with the Before prefix you can stop the operation by returning an error message:
$modx->event->output()
<?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
- MiniShop3's
invokeEvent()collects all plugin outputs - If any output is non-empty, the operation is aborted
- The message is returned to the user (frontend or API response)
Events that support aborting
msOnBeforeGetCartmsOnBeforeAddToCartmsOnBeforeChangeInCartmsOnBeforeChangeOptionsInCartmsOnBeforeRemoveFromCartmsOnBeforeEmptyCartmsOnBeforeAddToOrdermsOnBeforeRemoveFromOrdermsOnSubmitOrdermsOnBeforeCreateOrdermsOnBeforeChangeOrderStatus- Other events with the
Beforeprefix
Modifying data
returnedValues
To change parameters used later:
<?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;
}Important: use reference (&)
// 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 field | Meaning |
|---|---|
success | true when no plugin returned non-empty output |
message | Plugin output strings joined with <br/> |
data | Original $scriptProperties shallow-merged with returnedValues |
values | Raw 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
- By reference in
$scriptProperties(legacy MS2, still works where params are passed with&). returnedValues— preferred contract since 1.11+.
<?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
| Signal | Where | Effect |
|---|---|---|
$modx->event->output('text') | Utils::invokeEvent | success = false, text in message |
return false from plugin | import, notifications, some raw calls | EventGate::isCancelled() → operation aborted |
return 'cancel' | same | import counts the row as skipped |
<?php
case 'msOnImportRow':
if (empty($scriptProperties['data']['price'])) {
return 'cancel';
}
break;After hooks in Vue order manager
msOnCreateOrderProduct, msOnUpdateOrderProduct, msOnRemoveOrderProduct run after save/remove. An after-plugin error is logged only; the HTTP response stays success. Veto belongs in msOnBefore*. See Order product events.
Named returnedValues channels (import, msProducts, notifications)
Some events apply EventGate::applyReturnedArray() per key instead of a global merge:
| Event | returnedValues key | Behavior |
|---|---|---|
msOnBeforeImport | params | list → replace; assoc → array_replace |
msOnImportRow | data, tvData, optionData, gallery | same |
msOnProductsLoad | rows | same |
msOnProductPrepare | row | same |
msOnBeforeSendNotification | recipient, channels | same; 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
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
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
$modx->eventData = [
'msOnGetProductPrice' => [
'price' => 1500.00,
],
'msOnGetProductFields' => [
'data' => [...],
],
'myPlugin' => [
'custom_key' => 'custom_value',
],
];Important
$modx->eventData is cleared after the request. Use $_SESSION or the database to persist data between requests.
Showing messages in the manager
For manager events you can show messages to the user:
JavaScript alert via addHtml
<?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
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
- Price modification — plugins apply discounts in sequence
- Validation — one plugin may depend on another's result
- Data passing — source plugin must run before consumer
Debugging plugins
Logging
<?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
);Common mistakes
1. Forgetting return after output()
// Wrong
if ($error) {
$modx->event->output('Error');
}
doSomething();
// Correct
if ($error) {
$modx->event->output('Error');
return;
}2. Not using reference for returnedValues
// Wrong
$values = $modx->event->returnedValues;
$values['count'] = 10;
// Correct
$values = &$modx->event->returnedValues;
$values['count'] = 10;3. Wrong event for aborting
// msOnAddToCart — AFTER add; output() here won't stop the add
case 'msOnAddToCart':
$modx->event->output('Error'); // Useless
break;
// msOnBeforeAddToCart — BEFORE add
case 'msOnBeforeAddToCart':
$modx->event->output('Error'); // Stops the add
break;4. Forgetting break in switch
switch ($modx->event->name) {
case 'msOnBeforeAddToCart':
// code
// Forgot break — next case runs too!
case 'msOnAddToCart':
break;
}Common mistakes
1. Forgot return after output()
// 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
// Wrong
$values = $modx->event->returnedValues;
$values['count'] = 10; // Changes are lost
// Correct
$values = &$modx->event->returnedValues;
$values['count'] = 10;3. Wrong event type for aborting
// msOnAddToCart — event AFTER add
// Aborting here does not stop the add (product already added)
case 'msOnAddToCart':
$modx->event->output('Error'); // Useless
break;
// msOnBeforeAddToCart — event BEFORE add
case 'msOnBeforeAddToCart':
$modx->event->output('Error'); // Stops the add
break;4. Forgot break in switch
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
/**
* 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;
}