
API покупателя
Покупатель (msCustomer) не зависит от modUser. Поле user_id необязательно.
- msCustomer: профиль (email, телефон, пароль, статистика заказов)
- msCustomerToken: токены аутентификации и верификации
- msCustomerAddress: сохранённые адреса доставки
Контроллер Customer
Контроллер $ms3->customer отдаёт профиль, адреса и токен сессии.
Часть методов отдаёт результат в конверте
getFields(), set(), add(), generateToken() и updateToken() возвращают конверт ['success' => bool, 'message' => string, 'data' => array]. Это тот же Utils::success() / Utils::error(), что и в Web API. Нужные значения лежат в data. При пустом токене success равен false.
Остальные методы отдают значение напрямую:
| Метод | Возвращает |
|---|---|
getObject(), getByToken() | msCustomer или null |
create() | msCustomer или null |
addAddress() | bool |
getAddresses() | массив адресов |
getOrCreate() | int |
validate() | значение или массив ошибок |
$ms3 = $modx->services->get('ms3');
// Получить данные текущего покупателя (по токену сессии)
$response = $ms3->customer->getFields();
// ['success' => true, 'message' => '', 'data' => ['id' => 5, 'email' => 'user@example.com', ...]]
$fields = $response['data'];
// Получить объект msCustomer
$customer = $ms3->customer->getObject();
// Найти покупателя по токену
$customer = $ms3->customer->getByToken($token);
// Установить несколько полей
$result = $ms3->customer->set([
'first_name' => 'Иван',
'last_name' => 'Иванов',
'phone' => '+79991234567',
]);
// Добавить/обновить одно поле (с валидацией и событиями)
$result = $ms3->customer->add('email', 'new@example.com');Создание покупателя
// Программное создание
$customer = $ms3->customer->create([
'email' => 'user@example.com',
'first_name' => 'Иван',
'phone' => '+79991234567',
]);
// Вызывает события msOnBeforeCreateCustomer / msOnCreateCustomer
// Поиск или создание при оформлении заказа
$customerId = $ms3->customer->getOrCreate($orderData);
// Ищет по токену → по email → авторегистрация или создание записиУправление адресами
// Добавить адрес
$ms3->customer->addAddress([
'customer_id' => $customerId,
'city' => 'Москва',
'street' => 'Ленина',
'building' => '10',
'room' => '5',
]);
// Получить адреса покупателя
$addresses = $ms3->customer->getAddresses($customerId);Валидация полей
// Регистрация правил валидации
$ms3->customer->registerValidation(
// Правила (формат Rakit Validator)
['email' => 'required|email', 'phone' => 'required|min:10'],
// Сообщения об ошибках
['email:required' => 'Укажите email']
);
// Валидация значения
$result = $ms3->customer->validate('email', 'user@example.com');
// Значение или массив ошибокТокены сессии
// Сгенерировать новый токен
$response = $ms3->customer->generateToken();
// ['success' => true, 'message' => '', 'data' => ['token' => 'abc123...', 'lifetime' => 86400000]]
$token = $response['data']['token'];
// Обновить существующий токен (при пустом аргументе создаст новый)
$response = $ms3->customer->updateToken($currentToken);lifetime задаёт миллисекунды до истечения: секунды из ms3_customer_token_ttl, умноженные на 1000. Транспортный пакет создаёт настройку со значением 86400 (сутки). У свежего токена в lifetime около 86400000 (issue #848).
Аутентификация (AuthManager)
AuthManager аутентифицирует покупателя через подключаемые провайдеры. is_active и is_blocked он проверяет после того, как провайдер вернул покупателя. handleFailedLoginByEmail() вызывает процессор входа — только если покупатель не найден или пароль неверен.
$authManager = $modx->services->get('ms3_auth_manager');Аутентификация
// Стандартная аутентификация (email + пароль)
$customer = $authManager->authenticate([
'email' => 'user@example.com',
'password' => 'secret123',
]);
// Возвращает msCustomer или null
if ($customer) {
// Обновляет last_login_at, сбрасывает failed_login_attempts
}Подключаемые провайдеры
По умолчанию зарегистрирован PasswordAuthProvider (email + пароль). authenticate() перебирает провайдеры и спрашивает каждого, подходят ли ему переданные данные (supports()).
Подошедший провайдер перебор не завершает. Если его собственный authenticate() вернул null (например, пароль не совпал), перебор продолжается дальше по списку. Прерывает его только один случай: провайдер нашёл покупателя, но тот неактивен или заблокирован — тогда authenticate() сразу отдаёт null. Причину отказа показывает getLastAuthFailure(): inactive, blocked, invalid_credentials или none при успехе. На blocked и inactive счётчик неудачных попыток наращивать не нужно — этим и пользуется процессор Api\Customer\Login.
Свой провайдер:
use MiniShop3\Controllers\Auth\AuthProviderInterface;
use MiniShop3\Model\msCustomer;
class TelegramAuthProvider implements AuthProviderInterface
{
public function getName(): string
{
return 'telegram';
}
public function supports(array $credentials): bool
{
return isset($credentials['telegram_hash'], $credentials['telegram_id']);
}
public function authenticate(array $credentials): ?msCustomer
{
if (!$this->verifyTelegramHash($credentials)) {
return null;
}
return $this->modx->getObject(msCustomer::class, [
'phone' => $credentials['phone'], // или другой идентификатор
]);
}
}
// Регистрация провайдера
$authManager->registerProvider(new TelegramAuthProvider($modx));Токены и сессии
// Создать токен аутентификации
$token = $authManager->createToken($customer, 'api', 86400);
// msCustomerToken: token, type='api', expires_at = now + 86400
// Проверить токен (msCustomer или null, если истёк либо использован)
$customer = $authManager->validateToken($tokenString, 'api');
// Отозвать все токены покупателя
$count = $authManager->revokeTokens($customer);
// Отозвать только определённый тип
$count = $authManager->revokeTokens($customer, 'api');
// Очистить просроченные токены (для cron-задачи)
$deleted = $authManager->cleanupExpiredTokens();
// Войти: новый токен + сессия + cookie + перенос черновика корзины
// (['token' => '...', 'expires_at' => '...'] или null)
$session = $authManager->establishCustomerSession($customer);
// Псевдоним предыдущего метода, оставлен для читаемости вызовов из API
$session = $authManager->establishApiSession($customer);
// Выйти: отозвать токены типа api, очистить сессию и cookie,
// выдать гостевой токен взамен (bool)
$authManager->logoutCurrentCustomer();
// Найти покупателя по email, приведя адрес к единому виду (msCustomer или null)
$customer = $authManager->findCustomerByEmail('user@example.com');
// Нарастить счётчик неудачных попыток, когда покупателя ещё нет под рукой (void)
$authManager->handleFailedLoginByEmail('user@example.com');establishApiSession() ничего своего не делает
Метод просто вызывает establishCustomerSession(). Отдельной «сессии API», отличной от сессии фронтенда, в компоненте нет: и то и другое — один токен в таблице ms3_customer_tokens плюс запись в сессии PHP.
establishCustomerSession() всегда выпускает новый токен, даже если покупатель уже входил. Это защита от подмены сессии: старая строка токена удаляется, поэтому подставленная заранее cookie доступ после входа не сохранит. Черновик корзины при этом переносится на новый токен.
Типы токенов
| Тип | Константа | Описание | Одноразовый |
|---|---|---|---|
api | msCustomerToken::TYPE_API | Токен API-сессии | Нет |
refresh | msCustomerToken::TYPE_REFRESH | Токен обновления сессии | Нет |
magic_link | msCustomerToken::TYPE_MAGIC_LINK | Ссылка для входа без пароля | Да |
email_verification | msCustomerToken::TYPE_EMAIL_VERIFICATION | Подтверждение email | Да |
password_reset | msCustomerToken::TYPE_PASSWORD_RESET | Сброс пароля | Да |
После первого применения одноразовый токен получает used_at.
Токен сброса пароля не помечают через markAsUsed(). После успешной смены пароля AuthManager::revokeTokens() вызывается без типа и отзывает все токены покупателя. Сессии на других устройствах тоже прекращаются.
Блокировка при неудачных попытках
// Обработка неудачного входа (вызывается автоматически)
$authManager->handleFailedLogin($customer);
// Увеличивает failed_login_attempts
// При достижении ms3_customer_max_login_attempts (по умолчанию 5)
// блокирует на ms3_customer_block_duration секунд.
// Установщик пишет 300. Если строки настройки нет, код берёт 3600.Регистрация (RegisterService)
$registerService = $modx->services->get('ms3_register_service');
$result = $registerService->register([
'email' => 'user@example.com',
'password' => 'Secret123!', // опционально — сгенерируется автоматически
'first_name' => 'Иван',
'last_name' => 'Иванов',
'phone' => '+79991234567',
'privacy_accepted' => true, // GDPR-согласие
'token' => $sessionToken, // привязка к сессии (из checkout)
]);
if ($result['success']) {
$customer = $result['customer']; // msCustomer
$autoPassword = $result['auto_password']; // null если пароль был указан
}Поведение при регистрации
- Проверка уникальности email и телефона
- Если пароль не указан, генерируется 16-символьный случайный пароль
- Хеширование пароля (bcrypt)
- Создание
msCustomer - Если
ms3_customer_require_email_verificationвключена, уходит письмо верификации - Если пароль сгенерирован и
ms3_customer_send_welcome_email= true, уходит приветственное письмо с паролем
Валидация пароля
$result = $registerService->validatePassword('weak');
// ['valid' => false, 'message' => лексикон ms3_customer_err_password_too_short]Минимальную длину в сообщении придётся подставить самостоятельно
Код передаёт в лексикон значение (['length' => $minLength]), но строка ms3_customer_err_password_too_short в поставке написана с плейсхолдером {length}. modLexicon подставляет только [[+length]], поэтому в message приходит нераскрытый плейсхолдер: «Пароль должен содержать минимум {length} символов».
Пока это так, в своём интерфейсе берите длину из настройки ms3_password_min_length и подставляйте сами:
$minLength = (int) $modx->getOption('ms3_password_min_length', null, 8);
$message = str_replace('{length}', (string) $minLength, $result['message']);Либо перепишите строку лексикона в своём языковом наборе под [[+length]] — тогда подстановка заработает штатно.
| Настройка | По умолчанию | Описание |
|---|---|---|
ms3_password_min_length | 8 | Минимальная длина |
ms3_password_require_uppercase | false | Требовать заглавную букву |
ms3_password_require_number | false | Требовать цифру |
ms3_password_require_special | false | Требовать спецсимвол |
Верификация email (EmailVerificationService)
$verification = $modx->services->get('ms3_email_verification_service');
// Отправить письмо верификации
$sent = $verification->sendVerificationEmail($customer);
// Проверить токен (по ссылке из письма)
$customer = $verification->verifyToken($tokenString);
// Устанавливает email_verified_at, помечает токен как использованный
// Проверить, подтверждён ли email
if ($verification->isVerified($customer)) {
// Email подтверждён
}
// Повторная отправка (с защитой от спама — 5 минут между отправками)
$result = $verification->resendVerificationEmail($customer);ms3_email_verification_token_ttl (по умолчанию 86400): время жизни токена верификации в секундах.
Web API покупателя — /api/v1/customer/*
Контроллер: CustomerProfileController. Методы появились в 1.10.x / 1.11.0.
| Метод | Путь | Описание |
|---|---|---|
POST | /api/v1/customer/add | Одно поле профиля: key + value. С 1.11.0 можно все колонки msCustomer из xPDO-карты (включая Object Extension), кроме 18 полей из CustomerPublicDto::SYSTEM_NON_EDITABLE_FIELDS: id, user_id, customer_group_id, token, password, email_verified_at, флаги и счётчики блокировки, агрегаты заказов, служебные даты. first_name, last_name, email, phone проходят Rakit-валидацию, остальные значения нормализуются по phptype из метаданных поля. Смена email сбрасывает email_verified_at. |
POST | /api/v1/customer/changeAddress | Выбирает сохранённый адрес в черновике заказа. Принимает address_hash. |
GET | /api/v1/customer/email/verify | Подтверждает email по токену из письма. Параметры: token, html=1 (HTML-редирект вместо JSON), format=json. По умолчанию редирект на сайт с ms3_email_verified=1|0. Свой URL: ms3_email_verification_url. URL успеха: ms3_email_verification_success_url. |
POST | /api/v1/customer/email/resend-verification | Повторно отправляет письмо верификации авторизованному покупателю. Сервис не даёт отправлять чаще раза в 5 минут. |
Ограничение частоты запросов (RateLimiter)
RateLimiter считает попытки в кеше MODX.
$limiter = $modx->services->get('ms3_rate_limiter');
// Проверить и инкрементировать счётчик
$allowed = $limiter->check(
'login', // действие
$clientIp, // идентификатор (IP, email и т.д.)
5, // максимум попыток
300 // окно в секундах
);
if (!$allowed) {
// Лимит превышен
}
// Проверить без инкремента
if ($limiter->isBlocked('login', $clientIp, 5)) {
// Заблокирован
}
// Сбросить счётчик (после успешного входа)
$limiter->reset('login', $clientIp);Адреса покупателя (CustomerAddressManager)
$addressManager = $modx->services->get('ms3_customer_address_manager');
// Добавить адрес
$success = $addressManager->add([
'customer_id' => $customerId,
'city' => 'Москва',
'street' => 'Ленина',
'building' => '10',
'room' => '5',
]);
// Автоматически: генерирует name, вычисляет hash для дедупликации
// Вызывает msOnBeforeAddCustomerAddress / msOnAddCustomerAddress
// Получить все адреса
$addresses = $addressManager->getByCustomerId($customerId);
// Получить один адрес
$address = $addressManager->getById($addressId);
// Обновить адрес
$addressManager->update($addressId, $customerId, [
'city' => 'Санкт-Петербург',
'street' => 'Невский',
]);
// Удалить адрес (проверяет принадлежность покупателю)
$addressManager->delete($addressId, $customerId);Дедупликация адресов
Хеш адреса: MD5 от city|street|building|room в нижнем регистре. Адрес с уже существующим хешем повторно не создаётся.
Проверка дубликатов (CustomerDuplicateChecker)
Вызывается при создании покупателя.
$checker = $modx->services->get('ms3_customer_duplicate_checker');
// Найти существующего покупателя по данным
$existing = $checker->findDuplicate([
'email' => 'user@example.com',
'phone' => '+79991234567',
]);
// Ищет по OR: совпадение email ИЛИ телефона
// Проверить, есть ли данные для поиска
if ($checker->hasCheckableData($data)) {
// Есть email или phone
}
// Изменить поля для проверки
$checker->setCheckFields(['email']); // только по emailНастройка ms3_customer_duplicate_fields (JSON) задаёт поля для проверки. По умолчанию: ["email", "phone"].
Нормализация при сравнении
| Поле | Нормализация |
|---|---|
email | Приведение к нижнему регистру |
phone | Извлечение только цифр (минимум 7) |
| Остальные | Обрезка пробелов |
Фабрика покупателей (CustomerFactory)
Создаёт покупателя из данных заказа при финализации в админке.
$factory = $modx->services->get('ms3_customer_factory');
$customer = $factory->createFromOrderData([
'first_name' => 'Иван',
'last_name' => 'Иванов',
'email' => 'user@example.com',
'phone' => '+79991234567',
]);Если ms3_customer_sync_create_moduser = true, фабрика также создаёт modUser + modUserProfile и добавляет в группу из ms3_customer_sync_user_group.
Поля msCustomer
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
user_id | integer | 0 | ID пользователя MODX (опционально) |
customer_group_id | integer | null | Группа покупателя. Админка пишет её через PUT /api/mgr/customers/{id} |
first_name | string | '' | Имя |
last_name | string | '' | Фамилия |
email | string | '' | |
phone | string | '' | Телефон |
token | string | '' | Токен текущей сессии (unique) |
password | string | null | Хеш пароля (bcrypt) |
email_verified_at | datetime | null | Дата подтверждения email |
is_active | boolean | true | Активен |
is_blocked | boolean | false | Заблокирован |
failed_login_attempts | integer | 0 | Неудачных попыток входа |
blocked_until | datetime | null | Заблокирован до |
created_at | datetime | now | Дата создания |
updated_at | datetime | null | Дата обновления |
last_login_at | datetime | null | Последний вход |
orders_count | integer | 0 | Количество заказов |
total_spent | float | 0.00 | Сумма заказов |
last_order_at | datetime | null | Дата последнего заказа |
privacy_accepted_at | datetime | null | Дата согласия на обработку данных |
privacy_ip | string | null | IP при согласии |
Поля msCustomerAddress
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
customer_id | integer | 0 | ID покупателя |
hash | string | null | MD5-хеш адреса (дедупликация) |
name | string | null | Название (автогенерация из город/улица) |
country | string | null | Страна |
index | string | null | Почтовый индекс |
region | string | null | Регион |
city | string | null | Город |
metro | string | null | Станция метро |
street | string | null | Улица |
building | string | null | Дом |
entrance | string | null | Подъезд |
floor | string | null | Этаж |
room | string | null | Квартира/офис |
comment | text | null | Комментарий |
createdon | datetime | null | Дата создания |
updatedon | datetime | null | Дата обновления |
active | integer | 1 | Адрес активен |
is_default | integer | 0 | Адрес по умолчанию |
Поля msCustomerToken
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
customer_id | integer | 0 | ID покупателя |
token | string | '' | Строка токена, в базе до 128 символов. Код выпускает 64 hex-символа |
type | enum | 'api' | Тип: api, refresh, magic_link, email_verification, password_reset |
expires_at | datetime | — | Срок действия |
created_at | datetime | now | Дата создания |
used_at | datetime | null | Дата использования (для одноразовых) |
Системные настройки
| Настройка | По умолчанию | Описание |
|---|---|---|
ms3_customer_max_login_attempts | 5 | Попыток входа до блокировки |
ms3_customer_block_duration | 300 | Длительность блокировки (секунды) |
ms3_customer_token_ttl | 86400 | Время жизни токена сессии (секунды) |
ms3_customer_require_email_verification | false | Требовать подтверждение email |
ms3_customer_send_welcome_email | true | Отправлять приветственное письмо |
ms3_customer_auto_login_after_register | true | Автовход после регистрации |
ms3_customer_auto_register_on_order | true | Авторегистрация при оформлении |
ms3_customer_auto_login_on_order | true | Автовход при оформлении |
ms3_customer_require_privacy_consent | true | Требовать согласие на обработку данных |
ms3_customer_duplicate_fields | ["email", "phone"] | Поля для проверки дубликатов |
ms3_customer_sync_create_moduser | false | Создавать modUser при создании покупателя |
ms3_customer_sync_user_group | 0 | ID группы MODX для новых пользователей |
ms3_password_min_length | 8 | Минимальная длина пароля |
ms3_password_reset_token_ttl | 3600 | Время жизни токена сброса пароля |
ms3_email_verification_token_ttl | 86400 | Время жизни токена верификации |
Настройка-двойник в админке
Рядом в том же разделе стоит ms3_customer_api_token_ttl. Её не читает ни одна строка кода, хотя значение такое же. Рабочая настройка: ms3_customer_token_ttl из таблицы выше (issue #797).
Ещё у двух настроек значение установщика расходится с запасным значением в коде (#898):
| Ключ | Установщик | Если строки нет |
|---|---|---|
ms3_customer_block_duration | 300 | 3600 в AuthManager::handleFailedLogin() |
ms3_customer_require_email_verification | false | true в RegisterService и процессоре регистрации |
Manager REST покупателей
Админка ходит сюда, а не в Processors\Customer\Update. Поэтому при правке покупателя в админке событие msOnBeforeUpdateCustomer не вызывается.
| Метод | Путь | Право |
|---|---|---|
GET | /api/mgr/customers | msorder_list |
GET | /api/mgr/customers/{id} | msorder_view |
PUT | /api/mgr/customers/{id} | msorder_save |
DELETE | /api/mgr/customers/{id} | msorder_remove |
DELETE | /api/mgr/customers/bulk | msorder_remove |
GET | /api/mgr/customers/{id}/addresses | msorder_list |
POST | /api/mgr/customers/{id}/addresses | msorder_save |
PUT | /api/mgr/customers/{id}/addresses/{address_id} | msorder_save |
DELETE | /api/mgr/customers/{id}/addresses/{address_id} | msorder_remove |
Группы покупателей, /api/mgr/customer-groups:
| Действие | Право |
|---|---|
| Список | msorder_list |
| Карточка | msorder_view |
| Запись | msorder_save |
| Удаление | msorder_remove |
Процессоры для runProcessor() остаются рядом.
| Процессор | Право |
|---|---|
Customer\Get, Address\Get | msorder_view |
оба GetList | msorder_list |
оба Update | msorder_save |
оба Remove | msorder_remove |
GetListCombobox, Multiple | своего права в классе нет |
Customer\GetListCombobox принимает параметр id и при combo=1 поднимает перечисленные записи наверх выражением FIELD(). Значение попадает в SQL без подготовленного выражения, поэтому с 1.14.2-beta1 процессор прогоняет его через IntArrayDecoder::decode(). На выходе остаётся список положительных целых, остальное отбрасывается. Пишете свой combo-процессор с такой же сортировкой — повторите этот шаг, иначе id станет точкой внедрения SQL.
События
| Событие | Когда вызывается |
|---|---|
msOnBeforeCreateCustomer / msOnCreateCustomer | Создание покупателя |
msOnBeforeUpdateCustomer / msOnUpdateCustomer | Процессор Customer/Update. Встроенная админка его не вызывает (#847) |
msOnBeforeAddToCustomer / msOnAddToCustomer | Изменение поля через контроллер |
msOnBeforeValidateCustomerValue / msOnValidateCustomerValue | Валидация значения поля |
msOnErrorValidateCustomerValue | Ошибка валидации значения |
msOnBeforeGetOrderCustomer / msOnGetOrderCustomer | Поиск/создание при оформлении |
msOnBeforeAddCustomerAddress / msOnAddCustomerAddress | Добавление адреса |
Параметры событий: События.
