Справочник эндпоинта
Полный перечень того, что можно объявить в describe() и чем пользоваться внутри handle(). Как это собирается в рабочий эндпоинт — пошаговое руководство.
Ключи описания
Метод describe() возвращает массив. Ниже — все ключи и значения по умолчанию.
Идентификация и маршрут
| Ключ | По умолчанию | Смысл |
|---|---|---|
id | '' | Идентификатор вида reviews.list — он же ключ реестра и цель для route_aliases. Обязателен и должен быть уникален на сайте |
title | '' | Название для каталога; если пусто, показывается id |
description | '' | Описание для каталога и OpenAPI |
path | '/' | Путь относительно mxapi.route_prefix. Сам префикс сюда не пишется |
methods | ['GET'] | HTTP-методы; приводятся к верхнему регистру |
provider | 'mxapi.core' | Источник эндпоинта, виден в каталоге |
deprecated | false | Пометка «устаревший» для каталога и OpenAPI |
Путь разбирает FastRoute, поэтому доступны его шаблоны: /reviews/{id:\d+}, необязательные части /reviews[/{id}]. Наружу — в каталог и OpenAPI — путь отдаётся без шаблонов: /reviews/{id}.
Параметры пути попадают во входные данные наравне с query и body, объявлять их нужно с 'in' => 'path'.
Доступ
| Ключ | По умолчанию | Смысл |
|---|---|---|
scope | '' | Scope, который клиент обязан иметь в токене. Пусто — scope не проверяется |
permission | '' | Право MODX в namespace mxapi. Пусто — право не проверяется |
auth | 'bearer' | bearer — нужен токен; none — эндпоинт публичный (так объявлен только выпуск токена) |
modx_context | '' | Контекст MODX: конкретный ключ (mgr, web), 'request' — из заголовка X-MxApi-Context, пусто — безразличен |
context | 'public' | public — часть контракта; internal — служебный, в каталог и OpenAPI не попадает никогда |
write | false | Изменяющий эндпоинт: пишется в журнал всегда и участвует в идемпотентности |
Константы вместо строк: EndpointMetadata::AUTH_NONE, AUTH_BEARER, CONTEXT_PUBLIC, CONTEXT_INTERNAL, MODX_CONTEXT_FROM_REQUEST.
Кэширование ответа
| Ключ | По умолчанию | Смысл |
|---|---|---|
cache | 'no-store' | no-store — ответ не проверяется на изменения; etag — ядро отдаёт метку версии и отвечает 304 на If-None-Match. Константы EndpointMetadata::CACHE_NO_STORE и CACHE_ETAG |
Подробнее — ETag и If-None-Match ниже.
write — не косметика
От него зависят три вещи: попадёт ли вызов в журнал при выключенном mxapi.log_reads, будет ли работать Idempotency-Key (только для write) и сохранится ли тело ответа для повтора. Изменяющий эндпоинт без write => true теряет и аудит, и защиту от двойного выполнения.
Документация
| Ключ | По умолчанию | Смысл |
|---|---|---|
parameters | [] | Декларация входа — см. ниже |
request_example | null | Пример запроса для каталога |
response_example | null | Пример ответа для каталога |
response_description | '' | Что возвращается — текстом |
Ключи реализации (наружу не отдаются)
Используются ProcessorEndpoint; видны в админке, но вырезаны из /meta/endpoints и OpenAPI.
| Ключ | Смысл |
|---|---|
processor | MODX 2 — путь процессора, например mgr/review/getlist; MODX 3 — полное имя класса, MyPackage\Processors\Mgr\Review\GetList::class |
processors_path | Каталог процессоров пакета. Только MODX 2: в тройке класс находит автозагрузчик |
field_map | Переименование параметров: ['product' => 'product_id'] — наружу первое, в процессор второе |
properties | Фиксированные свойства процессора; добавляются последними, клиент их не перебьёт |
Свой ключ тоже можно положить в описание и прочитать через $this->getMetadata()->getExtra('ключ', $default) — но он попадёт в публичный каталог, если его нет в списке выше.
Параметры
Одна декларация обслуживает три задачи: валидацию входа, каталог в админке и OpenAPI.
| Поле | По умолчанию | Смысл |
|---|---|---|
name | '' | Имя параметра |
in | 'query' | query, path или body |
type | 'string' | См. таблицу приведения |
required | false | Нет значения → missing_parameter |
default | null | Подставляется, если параметр не передан |
enum | [] | Белый список значений; иначе invalid_parameter |
min / max | null | Границы для числовых значений |
description | '' | Текст для каталога и OpenAPI |
example | null | Пример значения |
Константы: ParameterMetadata::IN_QUERY, IN_PATH, IN_BODY, TYPE_STRING, TYPE_INTEGER, TYPE_NUMBER, TYPE_BOOLEAN, TYPE_ARRAY, TYPE_OBJECT, TYPE_DATE.
Приведение типов
| Тип | Что принимается | Что вернётся |
|---|---|---|
string | скаляр | строка |
integer | число или числовая строка, иначе invalid_parameter | int |
number | то же | float |
boolean | 1, true, yes, on (регистр не важен) — истина; всё остальное — ложь, ошибки нет | bool |
array | массив, JSON-массив или строка через запятую | массив |
object | массив или JSON-объект, иначе invalid_parameter | массив |
date | всё, что понимает strtotime(), иначе invalid_parameter | исходная строка |
Пустая строка = «параметр не передан»
?status= равнозначно отсутствию параметра: подставится default, а для обязательного будет missing_parameter. Если пустая строка для вас осмысленное значение — не полагайтесь на неё, заведите отдельное значение в enum.
Базовые классы
AbstractEndpoint
Реализует getMetadata() из describe() и даёт два помощника.
| Метод | Что делает |
|---|---|
readParams(Request $request) | Возвращает только объявленные параметры, приведённые к типам. Всё лишнее отбрасывается, ошибки валидации бросаются сами |
readPaging(array $params, Config $config) | Возвращает [limit, offset] с учётом mxapi.default_limit и mxapi.max_limit |
readCursor(array $params, Config $config) | Разбирает и проверяет курсор из параметра cursor: возвращает состояние обхода или [], если курсора нет |
nextCursor(array $state, array $params, Config $config) | Собирает и подписывает курсор следующей страницы |
cursorScope(array $params) | Отпечаток выборки, к которому привязан курсор. Переопределяется, если состав «своих» параметров нестандартный |
Наследник обязан реализовать describe() и handle(Request $request, EndpointContext $context): Response.
ProcessorEndpoint
handle() уже реализован: собирает свойства, запускает процессор, разворачивает ответ. Точки расширения:
| Хук | Когда вызывается |
|---|---|
beforeRun(array &$properties, EndpointContext $context) | после сборки свойств, до запуска процессора: лексиконы, рантайм-настройки, доп. свойства |
transformPayload(array $payload, EndpointContext $context) | после процессора, до сборки конверта: доменная нормализация ответа |
extraMeta(array $payload) | для списочного ответа: агрегаты по всей выборке уходят в meta, не смешиваясь с data |
Поведение, которое стоит помнить:
- пагинация включается объявлением параметра
limit. Естьlimit—offsetпереименуется вstart, а вmetaпопадутtotal,limit,offset; - фиксированные
propertiesприменяются после пользовательских — клиент их не перебьёт; - ошибка процессора превращается в
processor_error(HTTP 400) с полевыми ошибками вdetails.errors.
Контекст выполнения
EndpointContext, приходящий в handle():
| Метод | Что даёт |
|---|---|
getPlatform() | Платформа: runProcessor(), getOption(), log(), now(), cacheGet/cacheSet(), findUserById(), checkPermission(), invokeEvent(), репозитории токенов/клиентов/журнала |
getConfig() | Конфигурация mxApi: get(), getInt(), getBool(), getList() |
getAuth() | AuthContext или null для эндпоинта с auth => none |
getMetadata() | Собственный паспорт эндпоинта |
AuthContext: getUser(), getToken(), getClient(), getClientId(), getActor() (значение заголовка X-MxApi-Actor).
Доступ к самому modX — через платформенный адаптер: $context->getPlatform()->getModx(). Метода нет в PlatformInterface намеренно: ядро о MODX не знает, а код, который им пользуется, при переносе на MODX 3 придётся править.
Ответы и ошибки
| Вызов | Что делает |
|---|---|
Response::success($data, array $meta = [], $status = 200) | конверт success / data / meta |
Response::error($code, $message, $status = 400, array $details = []) | обычно не нужен — бросайте ApiException |
Response::stream(callable $streamer, $status = 200) | ответ без конверта: колбэк сам печатает тело (так отдаётся OpenAPI) |
$response->withHeader($name, $value) | иммутабельно: возвращает копию с заголовком |
Ошибку правильнее бросать исключением — ядро само превратит её в ответ и запишет код в журнал:
throw ApiException::missingParameter('product');
throw ApiException::invalidParameter('status', 'ожидается new|approved');
throw ApiException::notFound('review');
throw ApiException::insufficientPermission('mxapi_reviews_write');Именованные конструкторы покрывают весь публичный контракт кодов — таблица кодов. Свободную строку кода придумывать не нужно: клиенты завязаны именно на этот словарь. Если своего кода действительно не хватает, создавайте исключение напрямую — new ApiException('my_code', 'Сообщение', 409).
Необработанное исключение любого другого типа превращается в internal_error (HTTP 500): подробности уходят в лог MODX, наружу — нейтральное сообщение, и только при mxapi.debug текст ошибки попадает в ответ.
Инкрементальные выгрузки
Два независимых механизма для интеграций, которые регулярно выкачивают одно и то же: ETag отвечает на вопрос «изменилось ли вообще», курсор — на вопрос «как пройти большую выборку по страницам, не сбиваясь».
ETag и If-None-Match
Объявите в паспорте 'cache' => EndpointMetadata::CACHE_ETAG — больше ничего не нужно:
'cache' => EndpointMetadata::CACHE_ETAG,Что делает ядро: считает метку от тела успешного ответа, отдаёт её заголовком ETag вместе с Cache-Control: private, no-cache и, если следующий запрос пришёл с тем же значением в If-None-Match, отвечает 304 без тела.
Правила, о которых стоит помнить:
- метка непрозрачна — клиент сравнивает её только с самой собой, разбирать её нельзя;
- 304 расходует лимит частоты наравне с обычным ответом: версия считается внутри цепочки обработчиков, поэтому обойти ограничение заголовком нельзя;
- метка ставится только на
GETи только на успешный ответ с обычным конвертом; потоковые ответы (Response::stream()) не проверяются; - в OpenAPI у такого эндпоинта появляются заголовок
ETagв ответе200и сам ответ304.
Не доводить дело до сборки ответа
По умолчанию тело всё-таки собирается — экономится трафик, но не работа сервера. Если версию выборки видно дёшево (максимальный editedon, счётчик ревизий), реализуйте EtagAwareInterface: при совпадении меток handle() не вызывается вовсе.
use MxApi\Core\Endpoint\EtagAwareInterface;
class ReviewsListEndpoint extends ProcessorEndpoint implements EtagAwareInterface
{
public function computeEtag(Request $request, EndpointContext $context)
{
// Обязана меняться при любом изменении данных ответа
// и различаться для разных параметров запроса.
return $lastChange . ':' . md5(json_encode($this->readParams($request)));
}
}Верните null, если версию сейчас не определить — ответ соберётся обычным путём. Исключение внутри computeEtag() не роняет запрос: ядро пишет предупреждение в лог и отвечает как обычно.
Курсорная пагинация
limit + offset для больших выгрузок плохи вдвойне: база пролистывает пропущенные строки, а вставки и удаления между страницами сдвигают выборку — записи задваиваются и теряются. Курсор (keyset) указывает не «сколько пропустить», а «после какой записи продолжать».
Объявите параметр cursor и работайте с двумя помощниками:
$params = $this->readParams($request);
$config = $context->getConfig();
list($limit) = $this->readPaging($params, $config);
$state = $this->readCursor($params, $config); // [] на первой странице
$after = isset($state['id']) ? (int)$state['id'] : 0;
// Читаем на одну запись больше запрошенного: так видно, есть ли что дальше,
// без подсчёта всей выборки.
$rows = $this->fetchRowsAfter($after, $limit + 1);
$hasMore = count($rows) > $limit;
$rows = array_slice($rows, 0, $limit);
$meta = ['has_more' => $hasMore];
if ($hasMore) {
$meta['next_cursor'] = $this->nextCursor(['id' => end($rows)['id']], $params, $config);
}
return Response::success($rows, $meta);Что получает клиент: meta.has_more и meta.next_cursor, который передаётся как есть в параметре cursor следующего запроса. Курсор безопасен для URL — дополнительное кодирование не нужно.
| Свойство | Как устроено |
|---|---|
| Непрозрачность | Внутри — позиция обхода, то есть кусок запроса к базе. Формат — деталь реализации, разбирать его клиенту нечего |
| Подпись | HMAC-SHA256 ключом сайта (mxapi.cursor_secret). Подделанный курсор и курсор с другого сайта отбрасываются |
| Привязка к выборке | В подпись входит отпечаток параметров, кроме cursor, limit и offset. Сменили фильтр — старая позиция бессмысленна и отвергается; размер страницы менять между запросами можно |
| Отказ | invalid_parameter с details.parameter = "cursor". Молча начать сначала нельзя: инкрементальная выгрузка пошла бы по кругу, и заметили бы это нескоро |
| Без ключа подписи | internal_error. Пустой mxapi.cursor_secret выключает курсоры, а не разрешает неподписанные |
meta.total в курсорном режиме не отдаётся
И не должен: полный подсчёт выборки на каждой странице стоит дороже самой выгрузки — ровно того, ради чего курсор и заводят. Клиенту нужен признак «есть ли ещё», и это has_more, получаемый чтением одной лишней записи.
Сортировка обязана быть устойчивой: ключ, по которому вы продолжаете обход, должен быть уникальным (обычно id) или дополняться уникальным. Иначе записи с одинаковым значением ключа будут пропускаться или повторяться.
Системные события
| Событие | Когда | Что можно |
|---|---|---|
mxApiOnRegisterEndpoints | сборка реестра | вернуть провайдера — имя класса строкой |
mxApiOnRegisterMiddleware | сборка цепочки обработчиков | вернуть промежуточный обработчик — имя класса строкой |
mxApiOnBeforeRequest | запрос принят, до маршрутизации | логирование, метрики |
mxApiOnBeforeEndpointRun | эндпоинт найден, токен проверен, контекст переключён | аудит, подготовка окружения |
mxApiOnAfterEndpointRun | сразу после вызова | пост-обработка, метрики |
mxApiOnResponse | перед отправкой ответа | наблюдение за статусом |
Промежуточные обработчики
use MxApi\Core\Endpoint\EndpointContext;
use MxApi\Core\Http\Request;
use MxApi\Core\Middleware\MiddlewareInterface;
class SignatureCheck implements MiddlewareInterface
{
public function process(Request $request, EndpointContext $context, callable $next)
{
// до эндпоинта
$response = $next($request);
// после эндпоинта
return $response->withHeader('X-Checked', '1');
}
}Подключаются двумя способами:
- пакет — плагином на событие
mxApiOnRegisterMiddleware, ровно как провайдер эндпоинтов: обработчик возвращает имя класса строкой (можно список). Так пакет привозит свою проверку вместе с собой, не заставляя администратора править конфиг на каждом сайте; - код сайта — ключом
middlewareвcore/config/mxapi.php.
Порядок цепочки: встроенные обработчики → обработчики пакетов → обработчики сайта → эндпоинт. Встроенные — лимит частоты и идемпотентность — подключены всегда и идут первыми: лимит отсекает лавину до любой работы с базой, и только потом проверяется повтор по ключу идемпотентности. Владелец сайта оказывается последним рубежом перед эндпоинтом и может перекрыть решение пакета.
<?php
/** @var modX $modx */
if ($modx->event->name !== 'mxApiOnRegisterMiddleware') {
return;
}
require_once $modx->getOption('core_path') . 'components/myreviews/src/Middleware/SignatureCheck.php';
$modx->event->output('MyReviews\\Api\\Middleware\\SignatureCheck');Ненайденный класс, класс без MiddlewareInterface и обработчик, упавший в конструкторе, отбрасываются с записью в лог MODX: сломанный сторонний обработчик встраивается в каждый запрос, поэтому уронить им весь API было бы слишком дёшево. Остальная цепочка при этом работает.
