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

Customer events ​

Adding and validating customer fields, creating a customer, managing addresses.

msOnBeforeGetOrderCustomer ​

Fired before getting the customer for the order.

Aborting does not block checkout

Linking an msCustomer to the order is optional: OrderSubmitHandler carries on with checkout even when no customer was 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.

Parameters ​

ParameterTypeDescription
controller\MiniShop3\Controllers\Order\OrderOrder controller
msCustomermsCustomer | nullCustomer object (may be null)

Substituting the customer ​

A plugin can return a ready msCustomer through returnedValues['msCustomer'] — for example, to resolve it by an external ID:

php
<?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 ​

ParameterTypeDescription
controller\MiniShop3\Controllers\Order\OrderOrder controller
msCustomermsCustomer | nullCustomer object

Example ​

php
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
keystringField key
valuemixedField value

Aborting the operation ​

php
<?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
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
keystringField key
valuemixedSaved value
msCustomermsCustomerCustomer object
isNewboolWhether customer is new

Example ​

php
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
keystringField key
valuemixedValue to validate

Aborting the operation ​

A plugin error here turns into a validation error on the field itself:

php
<?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
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
keystringField key
valuemixedValidated value

Modifying data ​

php
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
keystringField key
valuemixedInvalid value
errorsarrayErrors array

Example ​

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

        // Customize error messages
        $modx->log(modX::LOG_LEVEL_WARN, sprintf(
            '[Customer] Validation error %s: %s',
            $key,
            json_encode($errors)
        ));
        break;
}

msOnBeforeCreateCustomer ​

Fired before creating a new customer.

Parameters ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
customerDataarrayData for creation

Aborting the operation ​

php
<?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
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
customerDataarrayCustomer data
msCustomermsCustomerCreated customer object

Example ​

php
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
addressDataarrayAddress data

Aborting the operation ​

php
<?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
<?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 ​

ParameterTypeDescription
customer\MiniShop3\Controllers\Customer\CustomerCustomer controller
addressDataarrayAddress data
msCustomerAddressmsCustomerAddressCreated address object

Example ​

php
<?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 ​

ParameterTypeDescription
modestringAlways modSystemEvent::MODE_UPD ('upd')
dataarraymsCustomer fields — already the NEW values ($object->toArray())
idintID of the customer being saved
msCustomermsCustomerReference to the customer object
objectmsCustomerThe 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
<?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 ​

ParameterTypeDescription
modestringAlways modSystemEvent::MODE_UPD ('upd')
idintID of the saved customer
msCustomermsCustomerReference to the saved customer object
objectmsCustomerThe 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
<?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
<?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 ​

ParameterTypeDescription
resolver\MiniShop3\Services\Order\OrderUserResolverUser resolver service
user\MODX\Revolution\modUser | nullCurrent user candidate — usually null on input
orderDataarraySnapshot 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
<?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 ​

ParameterTypeDescription
resolver\MiniShop3\Services\Order\OrderUserResolverUser resolver service
user\MODX\Revolution\modUser | nullFinal user (or null if resolution failed)