
Cart events
Adding, changing and removing products in the cart.
Besides the parameters listed below, every event on this page also receives controller — the call wrapper adds it.
Which events can abort the operation
$modx->event->output(...) does not stop the work everywhere: the core reads the plugin response only for some of the events.
| Event | Reaction to output() |
|---|---|
Every msOnBefore* | The operation is aborted, the client gets an error |
msOnAddToCart | The product is already saved, but the request still returns an error |
msOnChangeInCart, msOnChangeOptionInCart, msOnRemoveFromCart, msOnEmptyCart | Ignored — the operation has already completed |
msOnGetCart, msOnGetStatusCart | Used only to substitute data, not to abort. But a non-empty output() loses the substitution as well: the core keeps the original value |
Put checks in the paired msOnBefore* events. Where the response is ignored, a plugin error disappears and the customer never sees it.
msOnBeforeGetCart
Fired before getting cart contents.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
draft | msOrder | Order draft (cart) |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeGetCart':
/** @var \MiniShop3\Controllers\Cart\Cart $controller */
$controller = $scriptProperties['controller'];
$draft = $scriptProperties['draft'];
// E.g. deny cart access for certain users
if ($modx->user->get('id') == 123) {
$modx->event->output('Access denied');
return;
}
break;
}msOnGetCart
Fired after getting cart contents. Lets you modify data before it is returned.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
draft | msOrder | Order draft (cart) |
data | array | Cart products array |
Modifying data
<?php
switch ($modx->event->name) {
case 'msOnGetCart':
$controller = $scriptProperties['controller'];
$data = $scriptProperties['data'];
// Add extra data to each product
foreach ($data as $key => &$item) {
$product = $modx->getObject(\MiniShop3\Model\msProduct::class, $item['product_id']);
if ($product) {
$item['sku'] = $product->get('article');
$item['thumb'] = $product->get('thumb');
}
}
// Return modified data
$values = &$modx->event->returnedValues;
$values['data'] = $data;
break;
}msOnBeforeAddToCart
Fired before adding a product to the cart. Lets you validate or modify add parameters.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
msProduct | msProduct | Product object |
count | int | Quantity |
options | array | Product options |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeAddToCart':
/** @var \MiniShop3\Model\msProduct $product */
$product = $scriptProperties['msProduct'];
$count = $scriptProperties['count'];
// Disallow adding products with zero price
if ($product->get('price') <= 0) {
$modx->event->output('Product is not available for order');
return;
}
// Disallow adding more than 10 units
if ($count > 10) {
$modx->event->output('Maximum 10 units per product');
return;
}
break;
}Modifying data
<?php
switch ($modx->event->name) {
case 'msOnBeforeAddToCart':
$product = $scriptProperties['msProduct'];
$count = $scriptProperties['count'];
$options = $scriptProperties['options'] ?? [];
$values = &$modx->event->returnedValues;
// Minimum quantity — 2
if ($count < 2) {
$values['count'] = 2;
}
// Add source tag to options
$options['source'] = 'promo_landing';
$values['options'] = $options;
break;
}msOnAddToCart
Fired after a product has been successfully added. The product is already saved and the draft is recalculated.
Can fail an already completed request
The product is saved by this point, yet the plugin can still call $modx->event->output(...) — add() then returns an error to the client although the cart has already changed.
Not fired when the same product is added again
If the product+options key is already in the cart, msOnBeforeAddToCart still fires. output() on it also blocks the quantity increase. Then add() hands the work to change(), msOnBeforeChangeInCart and msOnChangeInCart fire, and msOnAddToCart does not. Quantities are summed: 2 units added to the existing 3 give count = 5.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
msProduct | msProduct | Product object |
count | int | Quantity added |
options | array | Product options |
product_key | string | Unique product key in cart |
Example
<?php
switch ($modx->event->name) {
case 'msOnAddToCart':
$product = $scriptProperties['msProduct'];
$count = $scriptProperties['count'];
$productKey = $scriptProperties['product_key'];
// Log add
$modx->log(modX::LOG_LEVEL_INFO, sprintf(
'[Cart] Product added: %s (ID: %d), qty: %d, key: %s',
$product->get('pagetitle'),
$product->get('id'),
$count,
$productKey
));
// Send event to analytics
// analytics()->track('add_to_cart', [...]);
break;
}msOnBeforeChangeInCart
Fired before changing product quantity in the cart.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
product_key | string | Unique product key in cart |
count | int | New quantity |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeChangeInCart':
$productKey = $scriptProperties['product_key'];
$count = $scriptProperties['count'];
// Disallow changing certain products
if (strpos($productKey, 'promo') !== false) {
$modx->event->output('Promo product quantity cannot be changed');
return;
}
break;
}Modifying data
<?php
switch ($modx->event->name) {
case 'msOnBeforeChangeInCart':
$count = $scriptProperties['count'];
// Cap maximum
$values = &$modx->event->returnedValues;
if ($count > 50) {
$values['count'] = 50;
}
break;
}msOnChangeInCart
Fired after changing product quantity in the cart.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
product_key | string | Unique product key |
count | int | New quantity |
Example
<?php
switch ($modx->event->name) {
case 'msOnChangeInCart':
$productKey = $scriptProperties['product_key'];
$count = $scriptProperties['count'];
$modx->log(modX::LOG_LEVEL_INFO,
"Quantity changed: {$productKey} => {$count}"
);
break;
}msOnBeforeChangeOptionsInCart
Fired before changing product options in the cart.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
product_key | string | Unique product key |
options | array | New options |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeChangeOptionsInCart':
$options = $scriptProperties['options'];
// Disallow changing color to "gold" (exclusive)
if (isset($options['color']) && $options['color'] === 'gold') {
$modx->event->output('Gold color is not available');
return;
}
break;
}Modifying data
The options can be substituted — the core reads them back from the response if the value is an array:
<?php
switch ($modx->event->name) {
case 'msOnBeforeChangeOptionsInCart':
$options = $scriptProperties['options'];
// Normalise the case of the values
$options = array_map('mb_strtolower', $options);
$values = &$modx->event->returnedValues;
$values['options'] = $options;
break;
}The substituted options take part in computing product_key, so the cart row key changes along with them.
msOnChangeOptionInCart
Fired after changing product options in the cart.
Not fired when keys collide
If the new combination of options matches an existing cart row, the core removes the current row and hands the work over to change() — msOnBeforeChangeInCart/msOnChangeInCart fire instead of this event. The quantities of the two rows are added together.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
old_product_key | string | Old product key |
product_key | string | New product key (may change) |
options | array | New options |
Example
<?php
switch ($modx->event->name) {
case 'msOnChangeOptionInCart':
$oldKey = $scriptProperties['old_product_key'];
$newKey = $scriptProperties['product_key'];
$options = $scriptProperties['options'];
if ($oldKey !== $newKey) {
$modx->log(modX::LOG_LEVEL_INFO,
"Product key changed: {$oldKey} => {$newKey}"
);
}
break;
}msOnBeforeRemoveFromCart
Fired before removing a product from the cart.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
product_key | string | Unique product key |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeRemoveFromCart':
$productKey = $scriptProperties['product_key'];
// Disallow removing required product
// (e.g. delivery service added automatically)
if ($productKey === 'ms_delivery_service') {
$modx->event->output('This product cannot be removed');
return;
}
break;
}msOnRemoveFromCart
Fired after removing a product from the cart.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
product_key | string | Removed product key |
Example
<?php
switch ($modx->event->name) {
case 'msOnRemoveFromCart':
$productKey = $scriptProperties['product_key'];
$modx->log(modX::LOG_LEVEL_INFO,
"Product removed from cart: {$productKey}"
);
// Analytics notification
// analytics()->track('remove_from_cart', [...]);
break;
}msOnBeforeEmptyCart
Fired before fully clearing the cart.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeEmptyCart':
// Disallow clearing cart at certain times
$hour = (int)date('G');
if ($hour >= 23 || $hour < 6) {
$modx->event->output('Cart cannot be cleared at night');
return;
}
break;
}msOnEmptyCart
Fired after fully clearing the cart.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
Example
<?php
switch ($modx->event->name) {
case 'msOnEmptyCart':
$modx->log(modX::LOG_LEVEL_INFO, 'Cart cleared');
// A good place to drop your own cart-bound state. MiniShop3 core keeps
// no such keys of its own — things like a promo code come from add-ons,
// for example ms3PromoCode.
unset($_SESSION['myshop']['promo']);
break;
}msOnGetStatusCart
Fired when calculating cart status (totals) — both on cart mutations and on every Cart::getStatus(), that is on cart/get, on cart/status and when the msCart snippet renders. Lets you modify the totals.
output() discards the status patch too
Both call sites read returnedValues['status'] only on a successful response. If a plugin writes its own status and calls output() at the same time, the patch is not applied: the original calculateStatus() result comes back and the message lands in the response as an error.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Cart\Cart | Cart controller |
status | array | Cart totals array |
status structure
$status = [
'total_count' => 5, // Total units across all rows
'total_cost' => 15000, // Total cost
'total_weight' => 12.5, // Total weight in ms3_weight_unit (kg by default)
'total_discount' => 1500, // Total discount
'total_positions' => 3, // Number of positions (cart rows)
];There are no other keys in status. CartItemManager::calculateStatus() builds the whole array.
Modifying data
<?php
switch ($modx->event->name) {
case 'msOnGetStatusCart':
$status = $scriptProperties['status'];
// Add bonus points
$status['bonus_points'] = floor($status['total_cost'] / 100);
// Add free delivery info
$status['free_delivery'] = $status['total_cost'] >= 5000;
$status['free_delivery_diff'] = max(0, 5000 - $status['total_cost']);
$values = &$modx->event->returnedValues;
$values['status'] = $status;
break;
}Outputting extra info
Once the status is modified, the data is available on the frontend:
{* In cart chunk *}
{if $status.free_delivery}
<div class="free-delivery">Free delivery!</div>
{else}
<div class="delivery-info">
Until free delivery: {$status.free_delivery_diff | number_format : 0 : '' : ' '} ₽
</div>
{/if}
{if $status.bonus_points > 0}
<div class="bonus">You will get {$status.bonus_points} bonus points</div>
{/if}