Skip to content

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:

php
$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:

FeldZweck
endpointPfad unter /api/, z. B. backend/menueditor/api/backend/menueditor
modelPathRelativer Pfad zur model.php, ausgehend vom Plugin-api/-Ordner

Bei installAPI() schreibt der Core pro Eintrag eine Zeile in die pluginAPI-Tabelle:

sql
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:

URLKlasse in model.php
/api/authclass auth extends apiBaseModel
/api/pagesclass pages
/api/user/ordersclass user_orders
/api/user/statusclass user_status
/api/shop/cartclass shop_cart
/api/checkoutclass checkout
/api/backend/authclass backend_auth
/api/backend/pagebuilderclass backend_pagebuilder
/api/backend/actions/scriptclass backend_actions_script
/api/elearning/my-coursesclass elearning_my_courses
/api/backend/user/passwordclass 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_menueditor

Das 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
<?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:

php
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:

EndpointpublicMethodsWarum
/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:

php
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:

php
$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 Allowed

success() 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:

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:

php
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:

php
// _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-Dispatching
  • demoAPI/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-Handling
  • elearning/api/elearning/my-courses/model.php — User-spezifischer GET mit $_SESSION-Auth
  • demoAPI/api/webhook/stripe/model.phpskipOriginCheck=true fuer 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.phpmodelPath 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