
Customer events
Adding and validating customer fields, creating a customer, managing addresses.
msOnBeforeGetOrderCustomer
Fired before getting the customer for the order.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Order\Order | Order controller |
msCustomer | msCustomer | null | Customer object (may be null) |
Aborting does not block checkout
Linking an msCustomer to the order is optional. OrderSubmitHandler continues checkout even when the customer was not created or found. The data stays in msOrderAddress. Aborting this event with output() does not forbid checkout for guests. It only leaves the order without a linked msCustomer.
Substituting the customer
A plugin can return a ready msCustomer through returnedValues['msCustomer'] — for example, to resolve it by an external ID:
<?php
switch ($modx->event->name) {
case 'msOnBeforeGetOrderCustomer':
// On input msCustomer is usually null — resolve it yourself by an external key
$externalId = $_SESSION['crm_customer_id'] ?? null;
if ($externalId) {
$found = $modx->getObject(\MiniShop3\Model\msCustomer::class, [
'external_id' => $externalId,
]);
if ($found) {
$modx->event->returnedValues = ['msCustomer' => $found];
}
}
break;
}msOnGetOrderCustomer
Fired after getting the customer for the order.
A plugin error discards a link that was already resolved
Despite the "after" in the name, output() still has an effect here: the order gets no customer_id, even if the msCustomer has already been created or found — and possibly signed in through the session.
Parameters
| Parameter | Type | Description |
|---|---|---|
controller | \MiniShop3\Controllers\Order\Order | Order controller |
msCustomer | msCustomer | null | Customer object |
Example
<?php
switch ($modx->event->name) {
case 'msOnGetOrderCustomer':
/** @var \MiniShop3\Model\msCustomer $customer */
$customer = $scriptProperties['msCustomer'];
if ($customer) {
// Update visit stats
$visits = $customer->get('visits') ?? 0;
$customer->set('visits', $visits + 1);
$customer->set('last_visit', date('Y-m-d H:i:s'));
$customer->save();
}
break;
}msOnBeforeAddToCustomer
Fired before adding or changing a customer field.
Parameters
| Parameter | Type | Description |
|---|---|---|
customer | \MiniShop3\Controllers\Customer\Customer | Customer controller |
key | string | Field key |
value | mixed | Field value |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeAddToCustomer':
$key = $scriptProperties['key'];
$value = $scriptProperties['value'];
// Block certain email domains
if ($key === 'email') {
$blockedDomains = ['tempmail.com', 'throwaway.com'];
$domain = substr($value, strpos($value, '@') + 1);
if (in_array($domain, $blockedDomains)) {
$modx->event->output('Temporary email addresses are not accepted');
return;
}
}
break;
}Modifying data
<?php
switch ($modx->event->name) {
case 'msOnBeforeAddToCustomer':
$key = $scriptProperties['key'];
$value = $scriptProperties['value'];
$values = &$modx->event->returnedValues;
// Format phone
if ($key === 'phone') {
$values['value'] = '+7' . preg_replace('/\D/', '', $value);
}
// Capitalize name
if ($key === 'first_name' || $key === 'last_name') {
$values['value'] = mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
}
break;
}msOnAddToCustomer
Fired after adding a field to the customer.
The client gets an error although the field is saved
msCustomer->save() runs before this event. If a plugin aborts it with output(), add() returns an error to the caller while the field is already stored in the database — the API response and the database disagree.
Parameters
| Parameter | Type | Description |
|---|---|---|
customer | \MiniShop3\Controllers\Customer\Customer | Customer controller |
key | string | Field key |
value | mixed | Saved value |
msCustomer | msCustomer | Customer object |
isNew | bool | Whether customer is new |
Example
<?php
switch ($modx->event->name) {
case 'msOnAddToCustomer':
$key = $scriptProperties['key'];
$value = $scriptProperties['value'];
$customer = $scriptProperties['msCustomer'];
$isNew = $scriptProperties['isNew'];
// Log new customers
if ($isNew) {
$modx->log(modX::LOG_LEVEL_INFO, sprintf(
'[Customer] Created customer #%d: %s = %s',
$customer->get('id'),
$key,
$value
));
}
break;
}msOnBeforeValidateCustomerValue
Fired before validating a customer field value.
Parameters
| Parameter | Type | Description |
|---|---|---|
customer | \MiniShop3\Controllers\Customer\Customer | Customer controller |
key | string | Field key |
value | mixed | Value to validate |
Aborting the operation
A plugin error here turns into a validation error on the field itself:
<?php
switch ($modx->event->name) {
case 'msOnBeforeValidateCustomerValue':
$key = $scriptProperties['key'];
$value = $scriptProperties['value'];
// A custom rule applied before the built-in validation
if ($key === 'inn' && !empty($value) && !preg_match('/^\d{10,12}$/', $value)) {
$modx->event->output('The tax number must contain 10 or 12 digits');
return;
}
break;
}Modifying data
<?php
switch ($modx->event->name) {
case 'msOnBeforeValidateCustomerValue':
$key = $scriptProperties['key'];
$value = $scriptProperties['value'];
$values = &$modx->event->returnedValues;
// Normalize email before validation
if ($key === 'email') {
$values['value'] = strtolower(trim($value));
}
break;
}msOnValidateCustomerValue
Fired after successful validation of a field value.
Can reject a value that already passed validation
Despite the "after successful validation", an error raised here with output() still becomes a validation error of the field, exactly as in msOnBeforeValidateCustomerValue.
Parameters
| Parameter | Type | Description |
|---|---|---|
customer | \MiniShop3\Controllers\Customer\Customer | Customer controller |
key | string | Field key |
value | mixed | Validated value |
Modifying data
<?php
switch ($modx->event->name) {
case 'msOnValidateCustomerValue':
$key = $scriptProperties['key'];
$value = $scriptProperties['value'];
$values = &$modx->event->returnedValues;
// Post-process phone
if ($key === 'phone') {
// Format for display
$values['value'] = preg_replace(
'/(\d{1})(\d{3})(\d{3})(\d{2})(\d{2})/',
'+$1 ($2) $3-$4-$5',
preg_replace('/\D/', '', $value)
);
}
break;
}msOnErrorValidateCustomerValue
Fired on validation error for a customer field.
Parameters
| Parameter | Type | Description |
|---|---|---|
customer | \MiniShop3\Controllers\Customer\Customer | Customer controller |
key | string | Field key |
value | mixed | Invalid value |
errors | array | Errors array |
Replacing the error set
A plugin can replace the error array through returnedValues['errors'], or collapse it into one message by aborting the event with output():
<?php
switch ($modx->event->name) {
case 'msOnErrorValidateCustomerValue':
$key = $scriptProperties['key'];
$errors = $scriptProperties['errors'];
// Customising the error messages
$modx->log(modX::LOG_LEVEL_WARN, sprintf(
'[Customer] Validation error %s: %s',
$key,
json_encode($errors)
));
// Replace the error set (otherwise the original $errors is used)
if ($key === 'phone') {
$modx->event->returnedValues = [
'errors' => [$key => 'Enter a phone number as +7XXXXXXXXXX'],
];
}
break;
}msOnBeforeCreateCustomer
Fired before creating a new customer.
Parameters
| Parameter | Type | Description |
|---|---|---|
customer | \MiniShop3\Controllers\Customer\Customer | Customer controller |
customerData | array | Data for creation |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeCreateCustomer':
$data = $scriptProperties['customerData'];
// Check for duplicate
$existing = $modx->getObject(\MiniShop3\Model\msCustomer::class, [
'email' => $data['email'],
]);
if ($existing) {
$modx->event->output('A customer with this email already exists');
return;
}
break;
}Modifying data
<?php
switch ($modx->event->name) {
case 'msOnBeforeCreateCustomer':
$data = $scriptProperties['customerData'];
$values = &$modx->event->returnedValues;
// Add extra fields
$data['createdon'] = date('Y-m-d H:i:s');
$data['source'] = $_SERVER['HTTP_REFERER'] ?? 'direct';
$data['ip'] = $_SERVER['REMOTE_ADDR'] ?? '';
$values['customerData'] = $data;
break;
}msOnCreateCustomer
Fired after creating a new customer.
Parameters
| Parameter | Type | Description |
|---|---|---|
customer | \MiniShop3\Controllers\Customer\Customer | Customer controller |
customerData | array | Customer data |
msCustomer | msCustomer | Created customer object |
Example
<?php
switch ($modx->event->name) {
case 'msOnCreateCustomer':
/** @var \MiniShop3\Model\msCustomer $customer */
$customer = $scriptProperties['msCustomer'];
// Log
$modx->log(modX::LOG_LEVEL_INFO, sprintf(
'[Customer] Created customer #%d: %s %s (%s)',
$customer->get('id'),
$customer->get('first_name'),
$customer->get('last_name'),
$customer->get('email')
));
// Send to CRM
// $crm->createContact($customer->toArray());
// Newsletter signup
// $mailService->subscribe($customer->get('email'));
break;
}msOnBeforeAddCustomerAddress
Fired before adding an address to the customer.
Parameters
| Parameter | Type | Description |
|---|---|---|
addressData | array | Address data |
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeAddCustomerAddress':
$data = $scriptProperties['addressData'];
// Limit number of addresses
$count = $modx->getCount(\MiniShop3\Model\msCustomerAddress::class, [
'customer_id' => $data['customer_id'],
]);
if ($count >= 5) {
$modx->event->output('Maximum 5 addresses per customer');
return;
}
break;
}Modifying data
<?php
switch ($modx->event->name) {
case 'msOnBeforeAddCustomerAddress':
$data = $scriptProperties['addressData'];
$values = &$modx->event->returnedValues;
// Geocode address
$fullAddress = implode(', ', array_filter([
$data['city'],
$data['street'],
$data['building'],
]));
// $coordinates = $geocoder->geocode($fullAddress);
// $data['lat'] = $coordinates['lat'];
// $data['lng'] = $coordinates['lng'];
$values['addressData'] = $data;
break;
}msOnAddCustomerAddress
Fired after adding an address to the customer.
Parameters
| Parameter | Type | Description |
|---|---|---|
addressData | array | Address data |
msCustomerAddress | msCustomerAddress | Created address object |
Example
<?php
switch ($modx->event->name) {
case 'msOnAddCustomerAddress':
$address = $scriptProperties['msCustomerAddress'];
$modx->log(modX::LOG_LEVEL_INFO, sprintf(
'[Customer] Added address #%d for customer #%d: %s',
$address->get('id'),
$address->get('customer_id'),
$address->get('name')
));
break;
}msOnBeforeUpdateCustomer
Fired before an existing customer is saved — the standard fireBeforeSaveEvent() of the MODX UpdateProcessor.
Nothing in MiniShop3 calls the processor
The event is registered (_build/elements/events.php) and the MiniShop3\Processors\Customer\Update processor fires it correctly — that processor extends the MODX UpdateProcessor. But no built-in manager page and no Web API method calls Customer/Update: neither a grid nor a controller runs $modx->runProcessor('Customer/Update', [...]). The event remains an extension point — your own integration can call the processor directly, and the event fires as expected.
Parameters
| Parameter | Type | Description |
|---|---|---|
mode | string | Always modSystemEvent::MODE_UPD ('upd') |
data | array | msCustomer fields — already the NEW values ($object->toArray()) |
id | int | ID of the customer being saved |
msCustomer | msCustomer | Reference to the customer object |
object | msCustomer | The same reference as msCustomer (MS2-style alias) |
Return a value instead of $modx->event->output()
The Customer/* events above are built on Utils::invokeEvent() and read output()/returnedValues. This one belongs to the standard MODX UpdateProcessor and reads the plugin's return value directly: return a non-empty string or a non-empty array of messages to abort the save.
Aborting the operation
<?php
switch ($modx->event->name) {
case 'msOnBeforeUpdateCustomer':
$data = $scriptProperties['data'];
$id = $scriptProperties['id'];
// Disallow switching the email to one already taken by another customer
if (!empty($data['email'])) {
$existing = $modx->getObject(\MiniShop3\Model\msCustomer::class, [
'email' => $data['email'],
'id:!=' => $id,
]);
if ($existing) {
return 'This email is already used by another customer';
}
}
break;
}msOnUpdateCustomer
Fired after a customer has been saved successfully (fireAfterSaveEvent()). Notification only — the return value is ignored, the save has already happened.
Nothing in MiniShop3 calls the processor
Same as msOnBeforeUpdateCustomer: the MiniShop3\Processors\Customer\Update processor fires the event correctly, but no built-in manager page or Web API method calls that processor. Details in the warning above.
Parameters
| Parameter | Type | Description |
|---|---|---|
mode | string | Always modSystemEvent::MODE_UPD ('upd') |
id | int | ID of the saved customer |
msCustomer | msCustomer | Reference to the saved customer object |
object | msCustomer | The same reference as msCustomer (MS2-style alias) |
Unlike msOnBeforeUpdateCustomer, there is no data key here — the field snapshot is passed only in the before-event.
Example
<?php
switch ($modx->event->name) {
case 'msOnUpdateCustomer':
/** @var \MiniShop3\Model\msCustomer $customer */
$customer = $scriptProperties['msCustomer'];
$modx->log(modX::LOG_LEVEL_INFO, sprintf(
'[Customer] Customer #%d updated: %s',
$customer->get('id'),
$customer->get('email')
));
break;
}Full example: customer verification
<?php
/**
* Plugin: Customer verification
* Events: msOnBeforeCreateCustomer, msOnCreateCustomer
*/
switch ($modx->event->name) {
case 'msOnBeforeCreateCustomer':
$data = $scriptProperties['customerData'];
// Check if email exists
$existing = $modx->getObject(\MiniShop3\Model\msCustomer::class, [
'email' => $data['email'],
]);
if ($existing) {
// Update token for existing customer
$existing->set('token', $data['token']);
$existing->save();
$modx->event->output('Customer found by email');
return;
}
// Check phone
if (!empty($data['phone'])) {
$existingByPhone = $modx->getObject(\MiniShop3\Model\msCustomer::class, [
'phone' => $data['phone'],
]);
if ($existingByPhone) {
$existingByPhone->set('token', $data['token']);
$existingByPhone->save();
$modx->event->output('Customer found by phone');
return;
}
}
// Add metadata
$values = &$modx->event->returnedValues;
$data['verified'] = false;
$data['verification_code'] = substr(md5(uniqid()), 0, 6);
$data['createdon'] = date('Y-m-d H:i:s');
$values['customerData'] = $data;
break;
case 'msOnCreateCustomer':
$customer = $scriptProperties['msCustomer'];
$data = $scriptProperties['customerData'];
// Send verification code
if (!empty($data['verification_code']) && !empty($customer->get('email'))) {
// Send email with code
// $mailer->send($customer->get('email'), 'Verification code: ' . $data['verification_code']);
$modx->log(modX::LOG_LEVEL_INFO, sprintf(
'[Verification] Code %s sent to %s',
$data['verification_code'],
$customer->get('email')
));
}
break;
}msOnBeforeGetOrderUser
Fired before resolving the MODX system user (modUser) for the order. Runs in OrderUserResolver on order submit when the ms3_order_register_user_on_submit system setting is enabled.
How modUser differs from msCustomer
modUser is the MODX system user (login, password, profile). msCustomer is a separate store customer entity (name, phone, session token). Resolving modUser registers the buyer in MODX authentication; it does not manage the store customer profile.
Parameters
| Parameter | Type | Description |
|---|---|---|
resolver | \MiniShop3\Services\Order\OrderUserResolver | User resolver service |
user | \MODX\Revolution\modUser | null | Current user candidate — usually null on input |
orderData | array | Snapshot of order fields (address address_email, address_phone, address_first_name, etc.) |
Substituting the user
A plugin can return a ready modUser in returnedValues['user'] to bypass the default lookup and creation:
<?php
switch ($modx->event->name) {
case 'msOnBeforeGetOrderUser':
$orderData = $scriptProperties['orderData'];
// Custom lookup by external ID, e.g. from CRM
if (!empty($orderData['address_external_id'])) {
$found = $modx->getObject(
\MODX\Revolution\modUser::class,
['username' => 'crm_' . $orderData['address_external_id']]
);
if ($found) {
$modx->event->returnedValues = ['user' => $found];
}
}
break;
}msOnGetOrderUser
Fired after resolving the user for the order.
Parameters
| Parameter | Type | Description |
|---|---|---|
resolver | \MiniShop3\Services\Order\OrderUserResolver | User resolver service |
user | \MODX\Revolution\modUser | null | Final user (or null if resolution failed) |
