API-Endpunkte
Plugins registrieren REST-Endpoints ueber das $this->apiEndpoints-Property. Jeder Endpoint hat ein PHP-Model, das apiBaseModel erweitert. Diese Seite erklaert die Registrierung, das Klassen-Naming (Pfad → Unterstriche) und das Basis-Pattern fuer Action-Handler.
Registrierung in bootstrap.php
Im install() des Plugins:
$this->apiEndpoints = [
'{"modelPath": "backend/menueditor/model.php", "endpoint": "backend/menueditor"}',
'{"modelPath": "menueditor/model.php", "endpoint": "menueditor"}'
];Eintraege sind JSON-Strings — je ein Eintrag pro Endpoint. Jeder String enthaelt zwei Felder:
| Feld | Zweck |
|---|---|
endpoint | Pfad unter /api/, z. B. backend/menueditor → /api/backend/menueditor |
modelPath | Relativer Pfad zur model.php, ausgehend vom Plugin-api/-Ordner |
Bei installAPI() schreibt der Core pro Eintrag eine Zeile in die pluginAPI-Tabelle:
CREATE TABLE pluginAPI (
id INT AUTO_INCREMENT PRIMARY KEY,
endpoint VARCHAR(255) NOT NULL, -- z. B. "backend/menueditor"
modelPath VARCHAR(255) NOT NULL, -- z. B. "backend/menueditor/model.php"
plugin_id INT NOT NULL -- FK → plugins.id
);Der Router im apiBaseController laedt die pluginAPI-Eintraege beim Request und dispatcht an das passende Model.
Klassen-Naming (KRITISCH)
Der Klassenname wird aus dem Pfad abgeleitet, mit zwei Ersetzungen:
/→_-→_
Beispiele:
| URL | Klasse in model.php |
|---|---|
/api/auth | class auth extends apiBaseModel |
/api/pages | class pages |
/api/user/orders | class user_orders |
/api/user/status | class user_status |
/api/shop/cart | class shop_cart |
/api/checkout | class checkout |
/api/backend/auth | class backend_auth |
/api/backend/pagebuilder | class backend_pagebuilder |
/api/backend/actions/script | class backend_actions_script |
/api/elearning/my-courses | class elearning_my_courses |
/api/backend/user/password | class backend_user_password |
Dashes muessen zu Unterstrichen
/api/elearning/my-courses wird zu class elearning_my_courses — mit Unterstrich statt Bindestrich. Macht man daraus class elearning_my-courses, findet der Loader die Klasse nicht und gibt 401 Unauthorized zurueck (weil der Pre-Auth-Check fehlschlaegt, bevor ueberhaupt geroutet wird).
Drei Segmente = zwei Unterstriche
Der Core wandelt den Pfad an drei Stellen im apiBaseController um: checkOriginGlobal(), checkApiAccess(), loadApiEndpoint(). Alle drei erwarten dieselbe Konvention. Eigene Zwischen-Resolver muessen die Konvertierung selbst machen.
Verzeichnis-Struktur
{plugin-name}/
└── api/
├── menueditor/
│ └── model.php # class menueditor (public API)
└── backend/
└── menueditor/
└── model.php # class backend_menueditorDas menueditor-Plugin hat also zwei API-Endpoints: eine public-API (/api/menueditor) und eine backend-API (/api/backend/menueditor). Beide sind in bootstrap.php registriert, beide haben eigene model.php-Klassen.
Basis-Template
Jedes Model extends apiBaseModel. Die Methode apiAction() ist der Eintrittspunkt — dispatcht auf HTTP-Methoden oder Actions:
<?php
class backend_menueditor extends apiBaseModel
{
protected array $publicMethods = []; // alle Methoden brauchen Auth
protected function apiAction(): void
{
if (empty($_SESSION['backend_loggedin']) || !$_SESSION['backend_loggedin']) {
$this->error('Unauthorized', 401);
return;
}
$method = strtoupper($_SERVER['REQUEST_METHOD']);
switch ($method) {
case 'GET': $this->getMethod(); break;
case 'POST': $this->postMethod(); break;
case 'PATCH': $this->patchMethod(); break;
case 'DELETE': $this->deleteMethod(); break;
default: $this->notSupported(); break;
}
}
private function getMethod(): void
{
$action = $_GET['action'] ?? 'list';
switch ($action) {
case 'list': $this->listMenus(); break;
case 'tree': $this->loadTree(); break;
default: $this->error('Unknown action', 400);
}
}
private function listMenus(): void
{
$query = query("SELECT id, name, slug FROM menueditor_menus ORDER BY id ASC");
$this->success(['menus' => fetch_all($query)]);
}
}publicMethods — Auth umgehen
Das Property $publicMethods steuert, welche HTTP-Methoden ohne Authentifizierung erreichbar sind:
protected array $publicMethods = []; // Default: alle Methoden brauchen Auth
protected array $publicMethods = ['GET']; // GET ohne Auth (z. B. oeffentliche API)
protected array $publicMethods = ['POST']; // POST ohne Auth (z. B. Webhook-Empfang)Typische Patterns:
| Endpoint | publicMethods | Warum |
|---|---|---|
/api/auth | ['POST'] | Login muss ohne Session erreichbar sein |
/api/pages | ['GET'] | Seiten-Abruf durch anonyme Besucher |
/api/backend/* | [] | Alle Backend-Endpoints brauchen Auth |
/api/webhook/stripe | ['POST'] | Stripe sendet Webhooks ohne Session |
skipOriginCheck — CORS umgehen
Fuer eingehende Webhooks von Drittsystemen muss der CORS-Origin-Check uebersprungen werden:
protected bool $skipOriginCheck = true;Nur fuer echte Webhook-Empfaenger (webhook/stripe, webhook/paypal, sw6/webhook) und bewusst oeffentliche APIs (emailmarketing/subscribe) — in allen anderen Faellen bleibt false (Default), damit der globale Origin-Schutz greift.
Response-Helfer
apiBaseModel stellt Response-Methoden bereit, die die HTTP-Antwort senden und exit aufrufen:
$this->success(['menus' => $menus]); // HTTP 200, JSON-Body
$this->success(null, 204); // HTTP 204 No Content
$this->error('Nicht gefunden', 404); // HTTP 404
$this->error('Unauthorized', 401); // HTTP 401
$this->error('Forbidden', 403); // HTTP 403
$this->error('Ungueltiger Request', 400); // HTTP 400
$this->notFound('Seite nicht gefunden'); // HTTP 404 Convenience-Wrapper
$this->notSupported(); // HTTP 422 Method Not Allowedsuccess() setzt zusaetzlich den APCu-Cache-Header bei anonymen GETs und ruft fastcgi_finish_request(), damit laufende Tasks im PHP-Shutdown-Handler weiterlaufen koennen, ohne den Response zu blockieren.
Datenbank-Zugriff
Model nutzt die Helper aus include/mysql.php:
$query = query("SELECT * FROM my_articles WHERE id = " . (int) $id . " LIMIT 1");
if (num_rows($query) === 0) {
$this->error('Not found', 404);
return;
}
$row = fetch_assoc($query);
$this->success($row);Immer real_escape_string() fuer Strings aus User-Input und (int) / intval() fuer IDs. Details: Plugin-Anatomie › Datenbank-Helper.
Request-Body lesen
JSON-Bodies manuell dekodieren — apiBaseModel macht das nicht automatisch:
private function postMethod(): void
{
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body)) {
$this->error('Invalid JSON body', 400);
return;
}
$name = real_escape_string($body['name'] ?? '');
// ...
}Rate-Limiting auf eigene Endpoints
Fuer sensible Endpoints (Auth, Checkout, Zertifikat-Ausstellung) sollten strengere Per-IP-Limits gelten. Die Limits werden in _core/system/api/Ratelimiter.php im $endpointLimits-Array gepflegt:
// _core/system/api/Ratelimiter.php
private static array $endpointLimits = [
'backend/auth' => ['capacity' => 30, 'refillRate' => 0.5, 'ttl' => 300],
'auth' => ['capacity' => 30, 'refillRate' => 0.5, 'ttl' => 300],
'elearning/certificate' => ['capacity' => 5, 'refillRate' => 0.08, 'ttl' => 300],
'elearning/exam' => ['capacity' => 10, 'refillRate' => 0.17, 'ttl' => 300],
'emailmarketing/subscribe' => ['capacity' => 5, 'refillRate' => 0.08, 'ttl' => 300],
'checkout' => ['capacity' => 10, 'refillRate' => 0.17, 'ttl' => 300],
];Keys: capacity = maximale Burst-Tokens, refillRate = Tokens pro Sekunde, ttl = APCu-Bucket-Lifetime in Sekunden. Prefix-Matching: backend/auth matcht auch backend/auth/login. Eintraege sind additiv zum globalen Limit (Frontend 100/min, Backend 250/min, API-Key 60/min) — sie setzen also nur strengere Grenzen, nicht weichere.
Beispiel-Endpoints im Repo
Gute Vorlagen zum Abschauen:
demoAPI/api/backend/menueditor/model.php— klassisches Backend-CRUD mit GET/POST/PATCH/DELETE und Action-DispatchingdemoAPI/api/auth/model.php— Public POST mit mehreren Action-Typen (typ=login|register|logout|...)demoAPI/api/pages/model.php— Public GET mit Multi-Language + Redirect-Handlingelearning/api/elearning/my-courses/model.php— User-spezifischer GET mit$_SESSION-AuthdemoAPI/api/webhook/stripe/model.php—skipOriginCheck=truefuer Drittsystem-Webhook
Haeufige Fehler
Dash-Pfad → 401 Unauthorized
Endpoint /api/elearning/my-courses erwartet class elearning_my_courses (zwei Unterstriche). Benennt man die Klasse elearning_my-courses, findet der Loader sie nicht, der Pre-Auth-Check schlaegt fehl und der Client bekommt 401 — nicht 404, was die Fehlersuche verwirrt.
modelPath falsch geschrieben
modelPath ist der Pfad relativ zum Plugin-api/-Ordner, inklusive .php. Fuer /api/backend/menueditor liegt das Model unter menueditor/api/backend/menueditor/model.php — modelPath heisst also "backend/menueditor/model.php", nicht "api/backend/menueditor/model.php".
apiAction() ohne Auth-Check
Der Core prueft fuer backend/*-Endpoints nicht automatisch $_SESSION['backend_loggedin']. Jeder Backend-Endpoint muss den Check selber in apiAction() oder gleich zu Beginn jeder Action machen. Public-APIs mit $publicMethods = ['GET'] sind davon ausgenommen.
$this->success() ruft exit auf
Alle Response-Helfer (success, error, notFound, notSupported) beenden die Script-Ausfuehrung. Kein Code nach $this->success() laeuft weiter — fuer laufende Shutdown-Tasks entweder fastcgi_finish_request() explizit triggern oder einen register_shutdown_function() vor dem Response setzen.
Request-Body wird nicht auto-dekodiert
apiBaseModel liefert keinen $request-Helper. json_decode(file_get_contents('php://input'), true) muss jeder Endpoint selbst machen — idealerweise mit Validierung auf is_array() danach.
Siehe auch
- Plugin-Anatomie —
$this->apiEndpoints-Property ininstall() - Plugins-Uebersicht › plugin_backend-Tabelle — Zusammenspiel von
pluginAPIundplugin_backend - Migrations — eigene Tabellen fuer Endpoint-Daten
- Webhook-Events — Events aus Endpoints dispatchen
- Scheduled Tasks — Endpoints als Queue-Job triggern
- API Reference — interaktive OpenAPI-Dokumentation aller Core-Endpoints