Настройка MODX как Headless CMS через REST API
MODX как headless CMS — нетривиальная задача: из коробки нет JSON API. Решения: кастомный коннектор, пакет modREST, или полная реализация через сниппеты с header('Content-Type: application/json').
Вариант 1: Кастомный JSON-коннектор
Создать ресурс с типом содержимого application/json и сниппетом-обработчиком:
// Сниппет: ApiProducts
// Ресурс: /api/products/ (contentType: application/json, published, cacheable: нет)
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$action = $_GET['action'] ?? 'list';
$id = (int)($_GET['id'] ?? 0);
$limit = min((int)($_GET['limit'] ?? 20), 100);
$offset = (int)($_GET['offset'] ?? 0);
switch ($action) {
case 'get':
echo json_encode(getProduct($modx, $id));
break;
case 'list':
default:
echo json_encode(getProducts($modx, $limit, $offset));
break;
}
function getProducts($modx, $limit, $offset): array {
$c = $modx->newQuery('modResource');
$c->where(['parent' => 5, 'published' => 1, 'deleted' => 0]);
$c->limit($limit, $offset);
$c->sortby('menuindex', 'ASC');
$total = $modx->getCount('modResource', $c);
$resources = $modx->getCollection('modResource', $c);
$items = [];
foreach ($resources as $resource) {
$items[] = [
'id' => $resource->id,
'title' => $resource->get('pagetitle'),
'slug' => $resource->get('alias'),
'description' => $resource->get('introtext'),
'price' => (float)$resource->getTVValue('price'),
'image' => $resource->getTVValue('product_image'),
'url' => $modx->makeUrl($resource->id, '', '', 'full'),
];
}
return [
'total' => $total,
'limit' => $limit,
'offset' => $offset,
'items' => $items,
];
}
Доступ: GET /api/products/?limit=10&offset=0.
Вариант 2: Полноценный REST API через класс
// core/components/myapi/processors/products/getlist.class.php
class ProductsGetListProcessor extends modProcessor {
public function process(): string {
$limit = min((int)$this->getProperty('limit', 20), 100);
$offset = (int)$this->getProperty('offset', 0);
$search = $this->getProperty('search', '');
$c = $this->modx->newQuery('modResource');
$c->where(['parent' => 5, 'published' => 1]);
if ($search) {
$c->where(['pagetitle:LIKE' => "%{$search}%"]);
}
$total = $this->modx->getCount('modResource', $c);
$c->limit($limit, $offset);
$collection = $this->modx->getCollection('modResource', $c);
$list = [];
foreach ($collection as $resource) {
$list[] = $this->prepareResource($resource);
}
return $this->outputArray($list, $total);
}
private function prepareResource($resource): array {
return [
'id' => $resource->id,
'title' => $resource->get('pagetitle'),
'price' => $resource->getTVValue('price'),
];
}
}
Аутентификация API
// Проверка API-ключа в заголовке
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
$validKey = $modx->getOption('myapi.secret_key');
if (!hash_equals($validKey, $apiKey)) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// JWT верификация (с библиотекой firebase/php-jwt через Composer)
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$token = str_replace('Bearer ', '', $_SERVER['HTTP_AUTHORIZATION'] ?? '');
try {
$decoded = JWT::decode($token, new Key($modx->getOption('jwt_secret'), 'HS256'));
$userId = $decoded->sub;
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['error' => 'Invalid token']);
exit;
}
CORS настройка
// Плагин CORS
// Событие: OnHandleRequest
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: https://frontend.yourdomain.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key');
header('Access-Control-Max-Age: 86400');
http_response_code(204);
exit;
}
if (strpos($_SERVER['REQUEST_URI'], '/api/') === 0) {
header('Access-Control-Allow-Origin: https://frontend.yourdomain.com');
}
Webhooks при изменении контента
// Плагин: ContentWebhook
// Событие: OnDocFormSave
$webhookUrl = $modx->getOption('webhook_url');
if (empty($webhookUrl)) return;
$payload = json_encode([
'event' => $mode === modSystemEvent::MODE_NEW ? 'created' : 'updated',
'id' => $resource->id,
'alias' => $resource->get('alias'),
'published' => (bool)$resource->get('published'),
]);
// Асинхронная отправка (fire and forget)
$ch = curl_init($webhookUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 3,
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
Сроки
Базовый JSON API для чтения контента (3–5 эндпоинтов) — 3–4 дня. Полноценный CRUD API с аутентификацией и webhooks — 7–10 дней.







