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.

Parameters ​

ParameterTypeDescription
controller\MiniShop3\Controllers\Order\OrderOrder controller
msCustomermsCustomer | nullCustomer 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
<?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

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

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
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
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)