בניית REST API עבור MODX: מערכת CMS ללא ראש עבור SPA ואפליקציות מובייל
תארו לעצמכם: יש לכם אתר מסחר אלקטרוני על MODX שפועל כבר 5 שנים, והשיווק דורש אפליקציה מובייל, בעוד שמפתחים צריכים JSON API כדי לחבר חזית React. או אתר כרטיס ביקור שבו עורכי תוכן עורכים מאמרים בפאנל הניהול, אבל הם מתפרסמים בבוט טלגרם ובמערכת CMS ללא ראשויקיפדיה. MODX היא פלטפורמה חזקה, אבל חסר לה ממשק JSON מובנה. פתרונות: מחבר מותאם אישית, חבילת modREST, או יישום מלא באמצעות סניפטים עם header('Content-Type: application/json'). עם למעלה מ-70 פרויקטי MODX מוצלחים ו-15 שנות ניסיון, אנו עוזרים לכם לבחור את האפשרות האופטימלית ולהקים API אינטרנטי מותאם אישית במפתח אחד.
בעיות שאנו פותרים
- חוסר ב-API סטנדרטי. MODX אינו מספק ממשק RESTful – צריך לכתוב שכבה משלכם. טעות אופיינית היא ניסיון להוציא JSON דרך משאב MODX רגיל, מה שמדליף נתוני רינדור.
- אינטגרציה עם חזיתות מודרניות. ללא API, אי אפשר לחבר SPA, אפליקציות מובייל או ארכיטקטורת JAMstack. טעות: רבים מנסים להשתמש ב-MODX כמנוע תבניות, ומערבבים לוגיקת תצוגה ו-API.
- אימות ואבטחה. API פתוח הוא סיכון; יש צורך בטוקנים, CORS ובדיקות הרשאות. 90% מהבעיות מתחילות בהגדרת CORS שגויה או באחסון סודות בקוד. לאחר קאשינג, 95% מקריאות ה-API מסתיימות תוך פחות מ-200ms.
הסיבה ש-MODX חסר REST API מובנה
MODX תוכנן במקור כ-CMS מסורתי עם תבניות. REST API לא היה חלק מהפונקציונליות הבסיסית. עם זאת, הארכיטקטורה מאפשרת הוספה גמישה של כל נקודת קצה באמצעות פרוססורים וסניפטים. אנו משתמשים בגמישות זו כדי ליצור API מלא ללא אובדן ביצועים.
איך אנחנו עושים את זה
אנו מנתחים את מבנה התוכן שלכם, קובעים את נקודות הקצה הנדרשות (5–12 בממוצע לפרויקט טיפוסי), ובוחרים שיטת יישום. אנו משתמשים ב-PHP 8.2+, xPDO עבור ORM, JWT (ספריית firebase/php-jwt) לאימות, ו-Redis לקאשינג. אנו גם מיישמים אידמפוטנטיות HTTP בנקודות קצה POST/PUT כדי להבטיח ניסיונות חוזרים בטוחים, והגבלת קצב באמצעות אלגוריתם דלי טוקנים. תוצאה: TTFB יורד ב-60% – מ-800 ms ל-120 ms, כמו בפרויקט האחרון לחנות מקוונת על Next.js (12 נקודות קצה: קטלוג, מוצר, פילטרים, חיפוש, עגלה).
השוואת אפשרויות יישום
| אפשרות | מורכבות | מהירות (בקשות/שנייה) | גמישות | תמיכה |
|---|---|---|---|---|
| סניפט מותאם אישית | נמוכה | 150 | נמוכה | ניהול עצמי |
| פרוססור MODX | בינונית | 300 | בינונית | דיבאג מובנה |
| חבילת modREST | נמוכה | 200 | בינונית | מוגבלת |
| מחלקה מותאמת אישית עם xPDO | גבוהה | 500 | גבוהה | מלאה |
לייצור, אנו ממליצים על מחלקה מותאמת אישית – שליטה ואבטחה מקסימליים. יישום מבוסס פרוססור מהיר פי 3 מסניפט לבקשות מרובות.
יישום JSON API: דוגמאות קוד
מחבר מותאם אישית באמצעות סניפט
צרו משאב עם סוג תוכן 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,
];
}גישה: // Сниппет: 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, ]; } .
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);
תהליך ומה כלול
- ניתוח – אנו לומדים את מבנה התוכן הנוכחי, מגדירים נקודות קצה ומודל נתונים. תוצאה: מפרט API.
- עיצוב – אנו מתכננים את ה-REST API: נתיבים, שיטות, פורמטי תגובה, סכמות אימות (JWT או מפתח API).
- יישום – אנו כותבים סניפטים או פרוססורים, מגדירים CORS, מוסיפים webhooks להודיע לחזית על שינויים.
- בדיקות – אנו בודקים עומס (עד 1000 בקשות/שנייה), שגיאות, אבטחה (Postman/Insomnia + בדיקות אוטומטיות).
- פריסה – אנו פורסים לייצור, מגדירים קאשינג (Redis, TTL=10 שניות), ניטור (New Relic).
מה כלול: נקודת קצה לקריאה וכתיבת תוכן (CRUD), אימות, תיעוד README עם דוגמאות בקשות, הגדרת CORS, webhooks, העברת גישה והדרכה למפתח שלכם, אחריות ל-30 יום. חיסכון בזמן: עד 70% בהשוואה לפיתוח מאפס, מה שמהווה עבור פרויקט טיפוסי הפחתת עלויות משמעותית. אחריות להחזר כספי על כל עבודת ה-API.
לוח זמנים ועלות
| היקף העבודה | לוח זמנים | מחיר התחלתי |
|---|---|---|
| JSON API בסיסי (3–5 נקודות קצה, קריאה בלבד) | 3–4 ימים | $500 |
| CRUD מלא עם אימות ו-webhooks | 7–10 ימים | $1,500 |
| אינטגרציה מורכבת (10+ נקודות קצה, קאשינג, ניטור) | החל משבועיים | $3,000 |
העלות מחושבת באופן אישי – אנו נעריך את הפרויקט שלכם תוך יום עסקים אחד. התקציב הממוצע להקמה משתנה בהתאם למורכבות. לשם השוואה, פיתוח דומה מאפס יקר פי 2–3.
דרישות טכניות להפעלת API
- PHP 8.1+ - MODX 3.x - Redis או Memcached לקאשינג - Composer לניהול תלות (JWT, Monolog) - תעודת HTTPSטעויות יישום אופייניות
- CORS לא מוגדר – החזית לא יכולה לקרוא את ה-API מהדפדפן. בדקו כותרות ו-preflight OPTIONS.
- אימות חלש – מפתחות API ב-URL (העבירו אותם בכותרות) וחוסר HTTPS. השתמשו ב-
GET /api/products/?limit=10&offset=0להשוואת מפתחות. - שאילתת N+1 – בעת שליפת רשימת משאבים ללא טעינת TVs מוקדמת. הוסיפו
// 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'), ]; } }או השתמשו ב-JOIN. - התעלמות מקאש – כל בקשה פוגעת במסד הנתונים, ומגדילה את TTFB. הגדירו קאשינג של Redis או פרוססור.
- חוסר פאג'ינציה – עם 50,000 פריטים התגובה יכולה להיות מעל 10 MB. השתמשו ב-limit ו-offset.
איך לבחור אפשרות יישום?
הבחירה תלויה במשימות שלכם: להוצאת נתונים פשוטה באתר סטטי – סניפט מותאם אישית מספיק. ל-SPA עם הרשאות – פרוססורים או מחלקות. אם אינכם בטוחים, צרו קשר – נייעץ ונעזור לכם להחליט.
קבלו ייעוץ והזמינו הקמת REST API במפתח אחד. הניסיון שלנו: למעלה מ-70 פרויקטים מוצלחים על MODX, מומחים מוסמכים מאז 2010, מחזור מלא מניתוח ועד פריסה.







