Skip to content

REST API

Headless-слой: внешнее приложение — Nuxt, Next.js, мобильное, интеграция — ходит на сайт по HTTP и получает JSON. Сессии у такого клиента нет, csrf-токена вернуть эхом неоткуда — вместо этого он предъявляет bearer-токен.

По умолчанию наружу не отдаётся ничего. Пока не выписан токен и не опубликован ни один объект, все эндпоинты отвечают 401, а CORS молчит. Обычный серверный сайт на PageBlocks живёт так, будто этого слоя нет.

Адреса

GET    /api/pb/v1/health               жив ли API; без токена
GET    /api/pb/v1/me                   что за токен и какие у него права
GET    /api/pb/v1/objects              какие объекты доступны этому токену
GET    /api/pb/v1/objects/{имя}        список
GET    /api/pb/v1/objects/{имя}/{id}   одна запись
POST   /api/pb/v1/objects/{имя}        создать   ┐ только если объект это
PATCH  /api/pb/v1/objects/{имя}/{id}   изменить  │ разрешил и у токена есть
DELETE /api/pb/v1/objects/{имя}/{id}   удалить   ┘ право на запись

Префикс api/pb/, а не api/, — чтобы не столкнуться с собственными маршрутами сайта.

Токены

Заводятся в менеджере: PageBlocks → API-токены. Полный токен показывается один раз, при создании; на сервере лежит только его sha256. Потерялся — «Перевыпустить» в меню строки, старый умирает сразу.

bash
curl -H 'Authorization: Bearer pb_…' https://site/api/pb/v1/me

Принимается и X-API-Key: pb_… — для клиентов за прокси, которые вырезают Authorization.

Права — строки вида cities.read. Маршрут требует конкретное право, токен его либо несёт, либо нет:

В токенеПокрывает
cities.readровно это право
cities.*все права группы cities
*всё

У generic-эндпоинтов имя права выводится из имени объекта: cities.read для чтения, cities.write для записи. В своих маршрутах право называется явно:

php
Route::get('api/v1/orders', 'OrderApi@index')->middleware('ApiAuth:orders.read');
Route::get('api/v1/ping',   'PingApi@index')->middleware('ApiAuth'); // любой живой токен

Токену можно задать срок действия и пользователя MODX, от имени которого работают запросы. Снятая галочка «Включено» отзывает токен, не удаляя строку.

Что отдавать: два пути

Галочка в конструкторе

Окно таблицы → вкладка API: включить, отметить поля, при необходимости добавить операции записи. Годится для контентных таблиц конструктора.

Остальное выводится само: поиск идёт по полям, уже отмеченным искомыми, обязательные поля становятся правилами валидации при записи, имя в адресе — транслит названия таблицы (переопределяется).

Два предохранителя, которые выглядят как «не работает», но так и задумано:

  • не отмечено ни одного поля — объект не публикуется, даже с галочкой: случайно поставленная галочка не должна открыть таблицу целиком;
  • операции по умолчанию только на чтение — публикация объекта и разрешение писать в него снаружи это разные решения.

Отдаются только опубликованные строки.

Декларация в core/App/api.php

Для всего, чего нет в конструкторе: свои модели, заявки, пользователи, ресурсы. Файл site-owned, обновление компонента его не трогает.

php
return [
    'cities' => [
        'model'      => City::class,
        'fields'     => ['id', 'city_name', 'alias'],
        'filterable' => ['id', 'alias', 'city_name'],
        'searchable' => ['city_name', 'alias'],
        'sortable'   => ['id', 'city_name'],
        'with'       => ['country'],
        'per_page'     => 20,
        'max_per_page' => 100,
        'default_sort' => 'id',
        'query' => fn(Builder $q) => $q->whereNotNull('published_at'),

        // Запись: без этого доступно только чтение.
        'operations' => ['list', 'show', 'create', 'update'],
        'writable'   => ['city_name', 'alias'],
        'rules'      => ['city_name' => 'required|string'],
        'scopes'     => ['read' => 'cities.read', 'write' => 'cities.write'],
    ],
];

Имя из файла перекрывает одноимённую таблицу конструктора — файл считается более точным выражением намерения.

Компоненты-сателлиты регистрируются кодом, по событию OnPageBlocksApiRegister:

php
ApiRegistry::register('orders', [...]);

Когда ни то, ни другое

Реестр закрывает однотипный CRUD. Агрегация, склейка нескольких моделей, бизнес-правила — это свой маршрут и свой контроллер, ApiAuth навешивается руками. Одно другому не мешает.

Параметры запроса

?filter[model_id]=7&filter[city_name][like]=Киш&sort=-id&fields=id,city_name&limit=20&offset=40
ПараметрЧто делает
filter[поле]=значениеравно
filter[поле][оператор]=значениеeq ne gt gte lt lte like in nin between null
search=словопо полям из searchable
sort=-id,nameминус — по убыванию
fields=id,nameсузить ответ
with=countryподтянуть связь из with
limit, offset, pageпостранично; limit режется по max_per_page

Поле вне белого списка игнорируется молча

Так же ведёт себя неизвестный оператор. Это главный источник недоумения: «фильтр не работает» почти всегда значит, что поле не объявлено в filterable.

Фильтровать можно только по опубликованным полям — иначе содержимое скрытого поля вычисляется перебором по ответам «да/нет».

В like и search символы % и _ экранируются: клиент передаёт слово, а не шаблон.

Формат ответа

json
{
  "data": [ { "id": 1, "city_name": "Москва" } ],
  "meta": { "total": 48748, "count": 20, "limit": 20, "offset": 0, "has_more": true },
  "links": { "self": "…", "next": "…", "prev": "…" }
}

Одна запись — {"data": {…}}. Ошибка — {"error": "not_found", "message": "…"}, у 422 добавляется details с разбором по полям.

Это не менеджерский {success, total, results}. Тот принадлежит гридам ExtJS и меняется вместе с ними; этот — контракт с чужим приложением.

Коды: 401 нет или плох токен, 403 не хватает права, 404 нет объекта или записи, 405 операция не разрешена, 422 не прошла валидация, 429 превышен лимит частоты.

CORS

Выключен, пока в pageblocks_cors_origins пусто.

НастройкаПо умолчанию
pageblocks_cors_originsпусто (выключено); * или https://*.example.com
pageblocks_cors_pathsapi/*
pageblocks_cors_methodsGET,POST,PUT,PATCH,DELETE,OPTIONS
pageblocks_cors_headersAuthorization,Content-Type,X-Requested-With,X-API-Key
pageblocks_cors_credentialsнет
pageblocks_cors_max_age86400

Настройки не появляются сами

Они объявлены в сборке пакета, а она отрабатывает только при сборке — на площадке их заводит установка. Отсутствие настройки читается как «выключено», поэтому забытый шаг выглядит не ошибкой: заголовков Access-Control-* просто нет, браузер блокирует запрос, а в логе сервера чистый 200.

Preflight (OPTIONS) обрабатывается до маршрутизации — иначе он умирал бы на 405, потому что OPTIONS не зарегистрирован там, где есть GET.

Ограничение частоты

120 запросов в минуту на служебные эндпоинты и на объекты, по IP. Сверх лимита — 429, заголовки X-RateLimit-* есть у всех ответов.

OpenAPI и Swagger

PageBlocks → Документация API — Swagger UI со списком адресов, полями параметров и кнопкой «выполнить». Чтобы запрос ушёл, нажмите Authorize и вставьте токен.

Спецификация собирается из того же реестра, который обслуживает запросы, — поэтому она не может устареть: объект появляется в ней ровно тогда, когда поставлена галочка или добавлена строка в App/api.php.

АдресКто пускаетЧто отдаёт
/mgr/pb/docs?ctx=mgrсессия менеджерастраницу Swagger UI
/mgr/pb/openapi.json?ctx=mgrсессия менеджераполную спецификацию
/api/pb/v1/openapi.jsonAPI-токенспецификацию, урезанную по правам токена

ctx=mgr обязателен: контекст маршрута берётся из запроса, без параметра менеджерский адрес просто не найдётся.

Внешнему разработчику отдают не ссылку на страницу, а сам JSON — он открывается в Postman и Insomnia и скармливается генераторам клиентов:

bash
npx openapi-typescript https://site/api/pb/v1/openapi.json -o api.d.ts

Файлы Swagger UI грузятся с CDN. Нет сети или CDN заблокирован — вместо пустой страницы показывается объяснение и адрес спецификации; сам API при этом работает, не открывается только витрина.

Адрес в поле servers берёт схему у текущего запроса, а не из site_url: после переезда на https настройка сплошь и рядом остаётся с http://, и кнопка «выполнить» упиралась бы в редирект или блокировку смешанного содержимого.

Чего пока нет

  • связей глубже одного уровня;
  • webhooks и Idempotency-Key;
  • generic-эндпоинтов для ресурсов MODX — они публикуются через App/api.php как обычная модель.

Отладка

СимптомЧто проверить
Это я или сервер?GET /api/pb/v1/health — токен не нужен
Токен рабочий?GET /api/pb/v1/me — показывает права
Объект опубликован?GET /api/pb/v1/objects — показывает только доступное
404 unknown_object на объект, который «точно есть»почти всегда снятая галочка или пустой список полей
500 с пустым теломcore/cache/logs/error.log, там полные трейсы

© PageBlocks 2019-present