REST M2M — Архитектура машинного API в Wepps

Вам нужно создать REST API для работы с данными таблиц? Может быть, это интеграция с 1С, синхронизация с внешней CRM, или обмен данными между системами. Обычно это приводит к дублированию кода: одинаковые методы, одинаковые ошибки, одинаковые траты времени.

RestV1M2M решает эту проблему простым способом: два файла, явные методы на каждую таблицу и один универсальный utils для работы с любой таблицей.

17.07.2026

REST M2M - Архитектура машинного API в Wepps

Вам нужно создать REST API для работы с данными таблиц? Может быть, это интеграция с 1С, синхронизация с внешней CRM, или обмен данными между системами. Обычно это приводит к дублированию кода: одинаковые методы, одинаковые ошибки, одинаковые траты времени.

RestV1M2M решает эту проблему простым способом: два файла, явные методы на каждую таблицу и один универсальный utils для работы с любой таблицей.

Три проблемы, которые решает RestV1M2M

Проблема 1: Дублирование кода CRUD операций

Решение: Один RestV1M2MUtils (экземпляр на таблицу) с методами add(), fetch(), set(), remove() (single) и addBatch(), setBatch(), remove() (batch). Все операции используют WeppsCore\Data.

Проблема 2: Сложность понимания кода

Чем больше абстракций (Manager → Entity → Router), тем это сложнее отлаживать и расширять.

Решение: Явные HTTP методы в RestV1M2M — видно сразу что где. Никаких магических вызовов, никаких фабрик.

Проблема 3: Трудно добавлять новые таблицы

Каждая новая таблица требовала создания целого класса + регистрации + маппинга.

Решение: RestConfig.php содержит все роуты. Добавьте запись в конфиг — готово. Новые таблицы через конфигурацию, специальная логика через before/after-колбэки.

Архитектура: Диспетчер + Utils

Общая схема

┌──────────────────────────────────────────────────┐
│               Rest.php (фреймворк)               │
│     Парсит URL, маршрутизирует на классы         │
└──────────────────────┬───────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────┐
│            RestV1M2M (диспетчер)                 │
│                                                  │
│  getUsers()    → getUtils('s_Users')->fetch()    │
│  postUsers()   → create('s_Users', records)      │
│  putUsers()    → update('s_Users', records)      │
│  deleteUsers() → getUtils('s_Users')->remove()   │
└──────────────────────┬───────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────┐
│        RestV1M2MUtils (CRUD логика)              │
│   (экземпляр на таблицу, кэш по tableName)       │
│                                                  │
│  fetch($query, $conditions) → Data::fetch()      │
│  item($id)                  → Data::fetch()      │
│  add($record)               → Data::add()        │
│  addBatch($records)         → Data::add() batch  │
│  set($id, $data)            → Data::set()        │
│  setBatch($items)           → Data::set() batch  │
│  remove($ids)               → Data::remove()     │
└──────────────────────┬───────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────┐
│           WeppsCore\Data (ядро системы)          │
│                                                  │
│  fetch()  - получить записи с пагинацией         │
│  add()    - создать запись                       │
│  set()    - обновить запись                      │
│  remove() - удалить запись                       │
└──────────────────────────────────────────────────┘

Компоненты

1. RestV1M2M.php — Диспетчер HTTP методов

Назначение: Встречает HTTP запросы и делегирует их utils'у.

Структура: явные методы для каждой таблицы. Для каждой таблицы есть 4 метода (GET list, GET item, POST, PUT, DELETE):

class RestV1M2M extends RestV1
{
    /**
     * Кэш экземпляров RestV1M2MUtils по имени таблицы.
     */
    private array $utils = [];

    /**
     * Получить (и закэшировать) экземпляр RestV1M2MUtils для таблицы.
     */
    protected function getUtils(string $tableName): RestV1M2MUtils
    {
        if (!isset($this->utils[$tableName])) {
            $this->utils[$tableName] = new RestV1M2MUtils($tableName);
        }
        return $this->utils[$tableName];
    }

    // USERS
    public function getUsers(): array
    {
        $utils = $this->getUtils('s_Users');
        $utils->setFields('Id,Guid,Name,NameFirst,NameSurname,NamePatronymic,IsHidden,UserPermissions,CreateDate,Login,Email,Phone,Comment,Country,Region,City,Address,PostalCode');
        return $utils->fetch($this->get);
    }

    public function getUsersItem(): array
    {
        $utils = $this->getUtils('s_Users');
        $utils->setFields('Id,Guid,Name,...');
        return $utils->item((int) ($this->get['id'] ?? 0));
    }

    public function postUsers(): array
    {
        $records = $this->normalizeInput();
        return $this->create('s_Users', $records);
    }

    public function putUsers(): array
    {
        $records = $this->normalizeInput();
        return $this->update('s_Users', $records);
    }

    public function deleteUsers(): array
    {
        $ids = $this->normalizeIds($this->normalizeInput());
        return $this->getUtils('s_Users')->remove($ids);
    }

    // ORDERS, GOODS, GOODS.NAVIGATOR, GOODS.STATUSES,
    // GOODS.ATTRIBUTES, GOODS.ATTRIBUTESVALUES, GOODS.VARIATIONS,
    // GOODS.STOCKS, FILES, TASKS.RESULT — аналогично
}

Преимущества явных методов:

  • ✅ Легко отладить - видно какой метод вызывается
  • ✅ IDE показывает все доступные методы
  • ✅ Нет магических вызовов через __call()
  • ✅ Одна ответственность - только диспетчинг

2. RestV1M2MUtils.php — CRUD Utils (экземпляр на таблицу)

Назначение: Вся бизнес-логика работы с БД. Конструктор принимает имя таблицы, поэтому один экземпляр работает только с одной таблицей:

class RestV1M2MUtils
{
    private string $tableName;

    public function __construct(string $tableName)
    {
        $this->tableName = $tableName;
    }

    /**
     * Получить список записей с пагинацией
     * @param array $query - параметры: page, limit, search
     * @param string|null $conditions - дополнительные условия WHERE
     * @return array - {status, message, data, pagination}
     */
    public function fetch(array $query, ?string $conditions = null): array { ... }

    /**
     * Получить одну запись по ID
     */
    public function item($id): array { ... }

    /**
     * Добавить одну запись (201 Created)
     */
    public function add(array $record): array { ... }

    /**
     * Пакетная вставка записей (207 Multi-Status)
     */
    public function addBatch(array $records): array { ... }

    /**
     * Обновить одну запись (200 OK)
     */
    public function set(int $id, array $data): array { ... }

    /**
     * Пакетное обновление записей (207 Multi-Status)
     */
    public function setBatch(array $items): array { ... }

    /**
     * Удалить записи (всегда 207 Multi-Status)
     */
    public function remove(array $ids): array { ... }
}

Ключевые моменты:

  • Все методы используют WeppsCore\Data (нет собственной SQL логики)
  • Все возвращают единый формат: {status, message, data}
  • Стандартные HTTP коды: 200 (success), 201 (created), 207 (multi-status), 400 (bad request), 404 (not found), 409 (duplicate), 500 (error)
  • Single-операции возвращают 201/200, batch-операции — 207 с per-item статусами
  • remove() работает ТОЛЬКО с batch-форматом (массив id)

3. RestConfig.php — Конфигурация маршрутов

Где регистрируются все REST методы (версия m2m):

'm2m' => [
    'get' => [
        'users' => [
            'class' => RestV1M2M::class,
            'method' => 'getUsers',
            'role_required' => [1],
            'auth_required' => true,
            'note' => 'M2M: список пользователей',
        ],
        'users.item' => [
            'class' => RestV1M2M::class,
            'method' => 'getUsersItem',
            'role_required' => [1],
            'auth_required' => true,
            'query_validation' => [
                'id' => ['type' => 'int2', 'required' => true],
            ],
        ],
        // ... orders, goods, goods.navigator, goods.statuses, ...
    ],
    'post' => [
        'goods' => [
            'class' => RestV1M2M::class,
            'method' => 'postGoods',
            'role_required' => [1],
            'auth_required' => true,
            'async' => true,   // ← поставить в очередь s_Tasks (202 Accepted)
            'validation' => [
                'guid' => ['type' => 'guid', 'required' => true],
                'name' => ['type' => 'string', 'required' => true],
                'alias' => ['type' => 'string', 'required' => true],
                'navigatorId' => ['type' => 'int2', 'required' => true],
                'price' => ['type' => 'float2', 'required' => true],
            ],
        ],
        // ... users, orders, goods.navigator, files, ...
    ],
    'put' => [
        'users' => [
            'class' => RestV1M2M::class,
            'method' => 'putUsers',
            'role_required' => [1],
            'auth_required' => true,
            'note' => 'M2M: обновление пользователя по id. ID через ?id= или {"id": 123}',
        ],
        // ...
    ],
    'delete' => [
        'users' => [
            'class' => RestV1M2M::class,
            'method' => 'deleteUsers',
            'role_required' => [1],
            'auth_required' => true,
            'note' => 'M2M: удаление по id. Формат тела: {"data": [123, 456, ...]}',
        ],
        // ...
    ],
],

Авто-дополнение валидацииinheritEndpointConfig() достраивает правила по умолчанию:

  • GET: query_validation = {page, limit} (если не задано явно)
  • PUT: validation = {id: int, required} + поля из POST как необязательные (частичное обновление)
  • DELETE: validation = {ARRAY: int[], required} — batch-удаление по массиву id

💡 Если для эндпоинта задан явный validation — он имеет приоритет и переопределяет авто-дополнение. Это позволяет, например, сделать PUT «строгим» с нужным набором обязательных полей (см. PUT — обновление).

Доступные таблицы (эндпоинты)

На данный момент в RestConfig.php зарегистрированы 12 эндпоинтов (версия m2m):

Эндпоинт Таблица в БД Описание
users, users.item s_Users Пользователи системы
orders, orders.item Orders Заказы (с вложенными data.items[] — позиции)
goods, goods.item Products Товары
goods.variations ProductsVariations Вариации товаров (цвет/размер/sku/остатки)
goods.stocks ProductsVariations (Field4) Остатки товаров (sku, количества)
goods.navigator s_Navigator Категории товаров (разделы навигатора)
goods.statuses s_Vars (группа ПродукцияСтатусы) Статусы товаров
goods.attributes s_Properties Свойства (атрибуты) товаров
goods.attributesGroups s_PropertiesGroups Группы свойств товаров
goods.attributesValues s_PropertiesValues Значения свойств товаров
files s_Files Файлы (картинки, документы)
tasks.result s_Tasks Результат async-задачи по id (только GET)

Для каждого эндпоинта кроме tasks.result доступны методы GET (list), GET (item), POST, PUT, DELETE — единый набор CRUD-операций.

Batch-семантика операций

M2M API поддерживает два режима работы: одиночный объект и batch (массив).

POST — создание

Формат Ответ
Одиночный объект {"data": {...}} 201 Created + {"data": {"id": N}}
Массив (макс. 100 записей) {"data": [{...}, {...}]} 207 Multi-Status + массив результатов по индексам

📌 Откуда взялось «макс. 100»: лимит устанавливается константой MAX_BATCH_SIZE = 100 в RestV1M2M.php и применяется в normalizeInput(int $maxBatchSize = self::MAX_BATCH_SIZE) — единой точке входа для всех batch-запросов (POST/PUT/DELETE). Если в {"data": [...]} передано больше 100 записей, массив аккуратно обрезается до первых 100 (array_slice(0, $maxBatchSize)); лишние записи молча отбрасываются без ошибки. Лимит можно переопределить аргументом вызова ($this->normalizeInput(500)), но по умолчанию он берётся из константы. Также «макс. 100» упоминается в note-описаниях эндпоинтов в RestConfig.php. Дальше по цепочке normalizeInput()create()/update()addBatch()/setBatch() обрабатывают уже обрезанный массив одной транзакцией.

PUT — обновление

ID записи передаётся только в теле JSON:

{"data": {"id": 123, "name": "..."}}

Передача ID через query-строку (?id=123) для PUT не поддерживается — ID берётся из каждой записи в data ($record['id']), несуществующие ID отсекаются проверкой SELECT Id FROM ... WHERE Id IN (...) до batch-обновления.

Несуществующий ID → 404 Record not found. PUT — это частичное обновление (работает как PATCH): обязательные поля не требуются, обновляются только переданные. Так сделано намеренно — для упрощения кодовой базы не вводятся отдельные PATCH-методы, единый PUT покрывает и полное, и частичное обновление.

💡 Настройка под свои требования: несмотря на частичную семантику по умолчанию, на уровне конфига можно задать необходимое и достаточное количество обязательных полей для PUT. Для этого в RestConfig.php у PUT-эндпоинта указывается явный validation — он переопределяет авто-дополнение inheritEndpointConfig():

'put' => [
    'users' => [
        'class' => RestV1M2M::class,
        'method' => 'putUsers',
        // Явный validation переопределяет inheritEndpointConfig():
        // теперь PUT /rest/m2m/users требует id + name (иначе 400)
        'validation' => [
            'id' => ['type' => 'int2', 'required' => true],
            'name' => ['type' => 'string', 'required' => true],
        ],
    ],
],

Так можно сделать PUT «строгим» (полная замена, все поля обязательны), либо гибридным — комбинация обязательных и необязательных полей под конкретную бизнес-логику.

DELETE — удаление

Тело запроса — массив ID:

{
  "data": [123, 456, 789]
}

Ответ всегда 207 Multi-Status с per-item статусами (успех/404 Already deleted or not found).

Пример 207 Multi-Status ответа

{
  "status": 207,
  "message": "Multi-Status",
  "data": [
    {"status": 201, "message": "Created", "data": {"id": 501}},
    {"status": 409, "message": "Duplicate key constraint: guid = 550e8400-..."},
    {"status": 400, "message": "Field 'name' is required"}
  ]
}

Ограничения

  • Максимум 100 записей за один batch-запрос: константа MAX_BATCH_SIZE = 100 в RestV1M2M.php, обрезка в normalizeInput(int $maxBatchSize = self::MAX_BATCH_SIZE) (array_slice(0, $maxBatchSize)). Лишние записи отбрасываются без ошибки — клиенту нужно разбивать большие массивы на порции по 100
  • Пагинация: page max 1...N, limit max 100 (лимит задаётся в RestV1M2M::calculatePagination(int $maxLimit = 100))
  • Проверка уникальности полей (IsUnique = 1 в s_ConfigFields) → 409 Duplicate key constraint

Хелперы диспетчера (create/update)

RestV1M2M содержит protected-хелперы для общих операций:

/**
 * Создать записи (одиночную или batch)
 * Одиночная → 201 Created, batch → 207 Multi-Status
 */
protected function create(string $tableName, array $records): array
{
    $results = [];
    foreach ($records as $record) {
        $this->validate($tableName, $record, true);   // полная валидация
    }
    $utils = $this->getUtils($tableName);
    $results = $utils->addBatch($records);            // batch-вставка
    $this->updateSearchIndex($tableName, $records);   // обновление поискового индекса

    // одиночная → прямой результат
    // batch → ['status' => 207, 'message' => 'Multi-Status', 'data' => $results]
}

/**
 * Обновить записи (PUT, частичное обновление)
 */
protected function update(string $tableName, array $records): array
{
    foreach ($records as $record) {
        if (empty($record['id'])) {
            return ['status' => 400, 'message' => 'id required', 'data' => null];
        }
        $this->validate($tableName, $record, false);  // required=false
    }
    // ... setBatch, 404 для несуществующих id
}

Вспомогательные методы диспетчера:

  • normalizeInput(int $maxBatchSize = self::MAX_BATCH_SIZE) — разворачивает {"data": ...}; одиночный объект оборачивает в массив [объект]; batch определяется по isset($raw[0]) && is_array(...); batch обрезается до $maxBatchSize (по умолчанию 100)
  • normalizeIds(array $records) — принимает скалярные ID или {"id": N} объекты, возвращает массив int > 0
  • calculatePagination(int $maxLimit = 100) — ограничивает page/limit
  • validate(string $tableName, array $record, bool $requireAll) — вызывает $this->rest->validateData() с правилами из getFieldRules(), JSON-поля исключаются

Расширение: Custom логика через before/after колбэки

Специальная логика задаётся через колбэки прямо в методах диспетчера — без подклассов и наследования. Колбэки выполняются ВНУТРИ транзакции — исключение откатывает всю операцию.

Колбэки не ограничены валидацией — они подгоняют данные под бизнес-требования и могут запускать дополнительные бизнес-операции: создавать аккаунты пользователей, выполнять массовые операции (например, пересборку вариаций или перезагрузку остатков) и ставить задачи в очередь s_Tasks. Вся логика живёт рядом с эндпоинтом в одном месте — понятно, что происходит при каждом запросе.

Транзакционность: всё или ничего

Каждая batch-операция (и single тоже) выполняется в одной транзакции через Connect::transaction() (beginTransaction → работа → commit):

BEGIN TRANSACTION
  ├─ before-колбэки     ← модифицируют записи
  ├─ INSERT/UPDATE      ← запись в БД
  ├─ after-колбэки      ← доп. операции (пересборка, привязки, файлы)
COMMIT                  ← все изменения фиксируются вместе

Откат при неуспехе: если любой колбэк или SQL-запрос бросит исключение — происходит rollBack() и вся операция откатывается целиком: ни одна запись из batch не сохранится. Это даёт гарантию консистентности — не бывает «половины» batch: либо применены все записи с сопутствующей логикой, либо ни одной.

// Пример: колбэк бросает исключение → транзакция откатывается
$utils->setAfter(function (array $results, string $tableName, RestV1M2MUtils $utils): array {
    foreach ($results as $result) {
        if (($result['status'] ?? 0) === 201) {
            $sync = someExternalService($result['data']['id']);
            if (!$sync) {
                throw new \Exception('External sync failed'); // → rollBack всего batch
            }
        }
    }
    return $results;
});

⚠️ Исключение в колбэке откатывает и вставку, и сопутствующие операции. Поэтому внешние вызовы (HTTP, файлы) лучше выполнять после успешного COMMIT или так, чтобы их можно было безопасно повторить.

public function postGoodsNavigator(): array
{
    $records = $this->normalizeInput();
    $utils = $this->getUtils('s_Navigator');

    // AFTER: после создания автоматически привязать раздел к расширению каталога
    $utils->setAfter(function (array $results, string $tableName, RestV1M2MUtils $utils): array {
        foreach ($results as $i => $result) {
            if (($result['status'] ?? 0) === 201) {
                $id = $result['data']['id'] ?? null;
                if ($id) {
                    $utils->set($id, ['ParentId' => ..., 'Extension' => ...]);
                }
            }
        }
        return $results;
    });

    return $this->create('s_Navigator', $records);
}

Список колбэков в текущем коде:

Метод Колбэк Действие
postGoodsNavigator after Устанавливает ParentId и Extension (каталог)
put/deleteGoodsNavigator before Валидация разделов каталога (buildInvalidIdValidationErrors)
postGoodsStatuses after Приоритет Priority += 5
postGoodsAttributes before Тип text-multi, группа = 1
postGoodsAttributes after Приоритет Priority += 5
put/deleteGoodsStatuses before Запрет обновления/удаления не-групповых ID
postGoodsAttributesValues before Генерация w_guid через Lists::getPropertiesValuesGuid()
postGoodsVariations before name = sku, alias через ProcessingProducts::getProductsVariationsAlias(), Priority = MAX+1
post/put/deleteGoodsVariations after/before Пересборка вариаций rebuildProductsVariations()
putGoodsStocks before При page=1 — полная перезагрузка остатков (Field4 = 0)
postFiles before Загрузка файла (base64/url) через Lists::getUploadFileName()
postFiles after Очистка временных файлов
deleteFiles before/after Сбор FileUrl, физическое удаление файлов (unlink)

Сигнатуры колбэков

// BEFORE — может модифицировать записи до вставки
// Возвращает массив записей (или null, чтобы оставить как есть)
'before' => fn(array $items, string $tableName, RestV1M2MUtils $utils): ?array

// AFTER — может заменить результаты после вставки
// Возвращает массив результатов (или null, чтобы оставить как есть)
'after'  => fn(array $results, string $tableName, RestV1M2MUtils $utils): ?array

Колбэки — это обычные PHP-замыкания, поэтому можно захватывать переменные из области видимости диспетчера через use($var) и обмениваться данными между before и after:

public function postGoods(): array
{
    $records = $this->normalizeInput();
    $utils = $this->getUtils('Products');
    $generatedAliases = [];   // переменная для обмена между колбэками

    // BEFORE: генерируем alias, сохраняем результат в $generatedAliases
    $utils->setBefore(function (array $items, string $tableName, RestV1M2MUtils $utils) use (&$generatedAliases): array {
        foreach ($items as $i => $item) {
            $generatedAliases[$i] = transliterate($item['name'] ?? '');
            $items[$i]['alias'] = $generatedAliases[$i];
        }
        return $items;
    });

    // AFTER: используем $generatedAliases из before-колбэка
    $utils->setAfter(function (array $results, string $tableName, RestV1M2MUtils $utils) use ($generatedAliases): array {
        foreach ($results as $i => $result) {
            if (($result['status'] ?? 0) === 201) {
                // пишем alias во внешний сервис / лог / связанную таблицу
            }
        }
        return $results;
    });

    return $this->create('Products', $records);
}

Для передачи по ссылке (чтобы after видел изменения, сделанные в before) используйте use (&$var) — как в примере выше.

// Установка ошибок валидации из колбэка (per-item)
$utils->setValidationErrors([
    0 => ['status' => 400, 'message' => 'Invalid id values provided', 'data' => ['id' => 999]],
]);

Пагинация со скрытием (handlePagination)

Некоторые POST/PUT методы поддерживают массовое скрытие через блок pagination в теле запроса. Это паттерн синхронизации: клиент отправляет все записи постранично, а сервер скрывает те, которые не были переданы.

Где поддерживается (методы с handlePagination в RestV1M2M):

Метод HTTP Логика
postUsers POST /rest/m2m/users Массовое скрытие не переданных пользователей
putUsers PUT /rest/m2m/users Массовое скрытие не переданных пользователей
putGoods PUT /rest/m2m/goods Массовое скрытие не переданных товаров
putGoodsAttributes PUT /rest/m2m/goods.attributes Массовое скрытие не переданных свойств
putGoodsAttributesValues PUT /rest/m2m/goods.attributesValues Массовое скрытие не переданных значений свойств
putGoodsVariations PUT /rest/m2m/goods.variations Массовое скрытие не переданных вариаций
putGoodsStocks PUT /rest/m2m/goods.stocks При page=1обнуление всех остатков (Field4 = 0), затем запись новых

Формат блока в теле запроса:

{
  "data": [ { "id": 1, ... }, { "id": 2, ... } ],
  "pagination": {
    "page": 1,       // номер текущей страницы (с 1)
    "count": 3       // всего страниц
  }
}

Как работает паттерн IsHiddenCandidateRestV1M2MUtils::handlePagination()):

// page=1 → все записи помечаются IsHiddenCandidate=1
// каждая страница сбрасывает флаг для обновлённых/созданных id
// последняя страница (page === count) финализирует: IsHidden = IsHiddenCandidate
$pagination = $utils->handlePagination($this->data['pagination'] ?? null);

Пошагово:

  1. Первая страница (page: 1) — все записи таблицы помечаются IsHiddenCandidate = 1.
  2. Каждая страница — переданные id сбрасывают флаг IsHiddenCandidate = 0 (запись будет оставлена видимой).
  3. Последняя страница (page === count) — финализация: IsHidden = IsHiddenCandidate, флаги очищаются.

Важно: пагинация передаётся в теле запроса ($this->data['pagination']), а не в query-строке. Без блока pagination колбэки не создаются — выполняется обычное точечное обновление переданных записей.

Исключение — putGoodsStocks: использует свой before-колбэк (не handlePagination), который при page: 1 обнуляет ВСЕ остатки (Field4 = 0) перед записью новых значений. Это полная перезагрузка остатков при синхронизации.

// PUT /rest/m2m/goods.stocks — полная перезагрузка остатков
$utils->setBefore(function (array $records, string $tableName, RestV1M2MUtils $utils) {
    $pagination = $this->data['pagination'] ?? null;
    if (isset($pagination['count']) && isset($pagination['page'])) {
        if ((int) $pagination['page'] == 1) {
            $sql = "UPDATE {$tableName} SET Field4 = 0 WHERE 1";
            Connect::$instance->query($sql);
        }
    }
    return $records;
});

Пример запроса с массовым скрытием (пользователи, 3 страницы):

# Страница 1 — все пользователи помечаются кандидатами на скрытие
PUT /rest/m2m/users HTTP/1.1
Content-Type: application/json
Authorization: Bearer ...

{
  "data": [ { "id": 1, "name": "John" }, { "id": 2, "name": "Jane" } ],
  "pagination": { "page": 1, "count": 3 }
}

# Страница 2
PUT /rest/m2m/users HTTP/1.1
{ "data": [ { "id": 3, "name": "Bob" } ], "pagination": { "page": 2, "count": 3 } }

# Страница 3 — последняя, финализирует скрытие непереданных пользователей
PUT /rest/m2m/users HTTP/1.1
{ "data": [ { "id": 4, "name": "Alice" } ], "pagination": { "page": 3, "count": 3 } }

После этого пользователи с id 5, 6, ... (не переданные ни на одной странице) будут скрыты (IsHidden = 1).

## Примеры использования REST API

### 1. Получить список пользователей

```bash
GET /rest/m2m/users?page=1&limit=20 HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:
{
  "status": 200,
  "message": "OK",
  "data": [
    {
      "id": 1,
      "guid": "550e8400-e29b-41d4-a716-446655440000",
      "login": "user@example.com",
      "name": "John Doe",
      "email": "john@example.com",
      "phone": "+1234567890"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "count": 145
  }
}

2. Создать пользователя

POST /rest/m2m/users HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "data": {
    "guid": "550e8400-e29b-41d4-a716-446655440000",
    "name": "John Doe",
    "nameFirst": "John",
    "nameSurname": "Doe",
    "login": "john@example.com",
    "email": "john@example.com",
    "phone": "9123456789"
  }
}

Response: 201 Created
{
  "status": 201,
  "message": "Created",
  "data": {
    "id": 42
  }
}

3. Создать товары (batch)

POST /rest/m2m/goods HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "data": [
    {
      "guid": "550e8400-e29b-41d4-a716-446655440001",
      "name": "iPhone 15",
      "alias": "iphone-15",
      "navigatorId": 8,
      "price": 999.99
    },
    {
      "guid": "550e8400-e29b-41d4-a716-446655440002",
      "name": "iPhone 15 Pro",
      "alias": "iphone-15-pro",
      "navigatorId": 8,
      "price": 1199.99
    }
  ]
}

Response: 207 Multi-Status (после обработки async-очереди)

Примечание: POST goods настроен как 'async' => true — запрос ставится в очередь s_Tasks и сразу возвращает 202 Accepted с task_id. Результат получается через GET /rest/m2m/tasks.result?id={task_id}.

4. Обновить остатки товаров

# Точечное обновление одного остатка
PUT /rest/m2m/goods.stocks HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "data": {
    "id": 501,
    "stocks": 15
  }
}

Response: 200 OK
{
  "status": 200,
  "message": "Updated",
  "data": {
    "id": 501
  }
}

Полная перезагрузка остатков (пакетная синхронизация) — добавьте pagination с page: 1:

PUT /rest/m2m/goods.stocks HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "data": [
    { "id": 501, "stocks": 15 },
    { "id": 502, "stocks": 0 }
  ],
  "pagination": { "page": 1, "count": 1 }
}

При page: 1 все остатки сначала обнуляются (Field4 = 0), затем переданным id записываются новые значения. Так остатки, отсутствующие в синхронизации, гарантированно становятся 0.

5. Удалить заказы (batch)

DELETE /rest/m2m/orders HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "data": [5001, 5002]
}

Response: 207 Multi-Status
{
  "status": 207,
  "message": "Multi-Status",
  "data": [
    {"status": 200, "message": "Deleted", "data": {"id": 5001}},
    {"status": 404, "message": "Already deleted or not found", "data": {"id": 5002}}
  ]
}

Аутентификация

Все M2M-эндпоинты защищены:

  • auth_required => true — обязателен JWT-токен
  • role_required => [1] — только пользователи с правами UserPermissions = 1

Авторизация общая с APP — токен получается через POST /rest/v1/auth.login (в Bruno-коллекциях это auth/auth.login.bru, сохраняет access_token/refresh_token в переменные, а папка clientM2M использует auth: bearer с {{access_token}}):

POST /rest/v1/auth.login HTTP/1.1
Content-Type: application/json

{
  "data": {
    "login": "m2m-admin@example.com",
    "password": "secret"
  }
}

Response:
{
  "status": 200,
  "message": "Login successful",
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "access_exp": 1774567890,
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_exp": 1777159890
  }
}

Далее access_token передаётся заголовком в каждом M2M-запросе:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Валидация данных

Правила валидации для M2M-запросов берутся из таблицы s_ConfigFields (не зашиты в код):

Как это работает

Для каждого POST/PUT запроса диспетчер вызывает validate():

private function validate(string $tableName, array $record, bool $requireAll): void
{
    $rules = $this->getUtils($tableName)->getFieldRules();
    if (empty($rules)) {
        return; // правил нет в БД — пропустить
    }
    $this->rest->validateData($record, $rules); // вызов из Rest.php
}

Источник правил — getFieldRules()

public function getFieldRules(): array
{
    // Кэш в Memcached на 1 час (ключ api_validation_rules_{table})
    return Connect::$instance->memcached->get('auto', function () {
        return Connect::$instance->db->query(
            "SELECT `TableField`, `ApiMapping`, `ApiFieldType`, `IsRequired`
             FROM s_ConfigFields
             WHERE `TableName` = ?
             ORDER BY `TableField` ASC",
            [$this->tableName]
        )->fetchAll();
    });
}

Поля из s_ConfigFields:

  • ApiMapping — маппинг camelCase → PascalCase (например nameFirstNameFirst)
  • ApiFieldType — тип валидации (string, int, float2, date, guid...)
  • IsRequired — обязательность поля
  • TableField — имя поля в таблице

Результат — массив правил:

[
    'nameFirst' => ['type' => 'string', 'required' => true],
    'nameSurname' => ['type' => 'string', 'required' => true],
    'phone' => ['type' => 'string', 'required' => true],
]

Особенности

  • JSON-поля исключаются из валидации (обрабатываются отдельно, автоматически декодируются)
  • PUT — валидация с required = false (частичное обновление)
  • Непереданные поля не проверяются; лишние поля400 Unexpected field '...'
  • Проверка уникальности полей IsUnique = 1409 Duplicate key constraint: {поле} = {значение}
  • Правила кэшируются в Memcached на 1 час; чтобы обновить — очистить кэш или подождать

⚠️ Типы в конфиге и в s_ConfigFields должны совпадать. Валидация выполняется дважды: сначала Rest::executeHandler() проверяет тип из validation в RestConfig.php, затем RestV1M2M::validate() проверяет ApiFieldType из s_ConfigFields (через getFieldRules()). Если типы расходятся (например, в конфиге string, а в s_ConfigFieldsguid), валидация по s_ConfigFields даст ошибку Field '...' must be guid для значения, которое прошло проверку по конфигу. Держите типы синхронизированными в обоих местах — конфиг отвечает за тип запроса, s_ConfigFields — за тип поля в БД.

Ошибки валидации

{
  "status": 400,
  "message": "Field 'email' is required",
  "data": null
}
{
  "status": 400,
  "message": "Field 'id' must be int",
  "data": null
}

Async-режим

Некоторые эндпоинты обрабатываются асинхронно через очередь s_Tasks:

  • POST goods'async' => true — запрос ставится в очередь, ответ 202 Accepted с task_id
  • POST goods.navigator'async' => false — выполняется немедленно
  • POST goods.variations'async' => false — выполняется немедленно (планируется async)
  • PUT goods.variations, PUT goods.stocks'async' => false

Поток async-запроса

POST /rest/m2m/goods  ({"data": [...]})
    │
    ▼
Rest::queueTask() → INSERT в s_Tasks
    │
    ▼
202 Accepted: {"status": 202, "data": {"task_id": 123}}
    │
    ▼  (отдельно, по cron/CLI)
php Request.php tasks.process   ← обработка очереди s_Tasks
    │
    ▼
GET /rest/m2m/tasks.result?id=123
    │
    ▼
200 OK: {"status": 200, "data": {"id": 123, "is_processed": 1, "http_status": 207, "response": {...}}}

Состояния ответа tasks.result (polling):

  • Задача ещё в очереди: is_processed: false, http_status: null, response: null
  • Задача выполнена: is_processed: true, http_status: <статус операции> (например 201 или 207), response: { "status": ..., "data": ... }
  • Задачи с таким id нет: 404

Опрашивайте эндпоинт с интервалом (например, раз в 5–10 секунд), пока is_processed не станет true.

CLI-обработка очереди: php packages/WeppsExtensions/Addons/Rest/Request.php tasks.process

Итоговая диаграмма потока запроса

HTTP Request: GET /rest/m2m/users?page=1&limit=20
       │
       ▼
┌──────────────────────────────┐
│ Rest.php (фреймворк)         │
│ routeRequest()               │
│ version='m2m', method='users'│
│ authenticateBearerToken()    │  ← auth_required + role_required [1]
└──────────┬───────────────────┘
           │
           ▼
┌──────────────────────────────────┐
│ RestV1M2M.getUsers()             │
│ ↓                                │
│ getUtils('s_Users')->fetch(      │
│   ['page'=>1, 'limit'=>20]       │
│ )                                │
└──────────┬───────────────────────┘
           │
           ▼
┌──────────────────────────────────┐
│ RestV1M2MUtils.fetch()           │
│ ↓                                │
│ Data('s_Users')->fetch(...)      │
│ ↓                                │
│ keysToCamelCase() (ApiMapping)   │
│ ↓                                │
│ return ['status' => 200, ...]    │
└──────────┬───────────────────────┘
           │
           ▼
HTTP 200: { status, message, data, pagination }

Возможности RestV1M2M

Аспект Реализация
URL /rest/m2m/...
Utils Экземпляр на таблицу (getUtils('s_Users'), кэш)
Методы Явные getUsers()/postUsers()/... + хелперы create()/update()
Входные данные normalizeInput() — обёртка {"data": ...}, batch-детект
Batch POST batch (макс. 100) → 207, DELETE batch → 207
Кастомная логика Before/after колбэки (setBefore()/setAfter())
Валидация validate() + getFieldRules() из TableField/ApiMapping/ApiFieldType/IsRequired
Аутентификация Всегда auth_required + role_required [1]
Async async => true → 202 + s_Tasks
Эндпоинты 12 эндпоинтов (users, orders, goods.*, files, tasks.result)

Выводы

RestV1M2M — это просто и эффективно потому что:

  1. Явные методы — каждый HTTP метод это отдельная функция, видно одновременно
  2. Utils на таблицу — один экземпляр на таблицу с кэшем, вся CRUD-логика в одном классе
  3. Без магии — нет __call(), нет магических фабрик, нет сложных наследований
  4. Опирается на Data — используем проверенный класс WeppsCore\Data
  5. Легко добавлять таблицы — методы + запись в RestConfig (валидация достраивается автоматически через inheritEndpointConfig())
  6. Batch-операции — создание/обновление/удаление до 100 записей за запрос (207 Multi-Status)
  7. Кастомизация через колбэки — before/after внутри транзакции, без создания подклассов
  8. Транзакционность — каждая операция выполняется в транзакции (beginTransactioncommit); исключение в колбэке или SQL откатывает весь batch (rollBack) — не бывает частично применённых изменений
  9. Валидация из БД — правила автоматически из s_ConfigFields (TableField/ApiMapping/ApiFieldType/IsRequired), кэш 1 час
  10. Асинхронность — тяжёлые операции через s_Tasks (202 + tasks.result)
  11. Единая валидацияvalidateData()/validateType() из Rest.php через $this->rest, нет дублирования логики

Примеры в Bruno

Готовые коллекции для тестирования лежат в репозитории: .tools/bruno/WeppsPlatformV1/clientM2M/ — папки users/, orders/, goods/attributes/, attributesValues/, navigator/, statuses/, stocks/, variations/), files/, tasks.result.get.bru.

Переменная окружения: {{base_url}} = https://ваш-домен

Как фреймворк: Гибкость разработки

Полный контроль над кодом, архитектурой и расширениями для сложных проектов

wapps framework

Как CMS: Простота управления

Интуитивная админ-панель для редакторов контента без программирования

wapps cms

Как платформа: Готовые решения

Быстрый старт проектов с возможностью глубокой кастомизации под любые задачи

wapps platform
R
REST V1 APP - API для мобильных приложений

RestV1APP — это REST API специально для мобильных приложений и фронтенда. Возвращает агрегированные данные, готовые к отображению на экране.

V1 APP — бизнес-ориентированные endpoints (getHome, postCart, placeOrder).

14.07.2026
R
REST API - Общая архитектура Wepps Platform

REST API Wepps Platform — это система предоставления данных через HTTP endpoints.

Состоит из версионированных API с разными подходами: M2M для машинных интеграций (1С, CRM), APP для мобильных приложений и фронтенда, wepps для админки, v0 для тестовых endpoints и cli для командной строки.

08.07.2026
A
AI-Ready проект: как сделать код понятным для искусственного интеллекта

AI-Ready — это когда искусственный интеллект может:

- Быстро понять архитектуру проекта
- Генерировать корректный код в стиле проекта
- Находить нужную информацию без долгих поисков
- Предлагать релевантные изменения

02.02.2026