Webhook-Events
Plugins koennen Events emittieren, auf die Subscribers (externe Services wie Zapier, n8n, Make oder eigene Endpoints) reagieren. newmeta liefert ein vollstaendiges Webhook-System mit HMAC-Signatur, Queue-basiertem Delivery und Retry/Circuit-Breaker. Diese Seite zeigt, wie ein Plugin Events registriert und dispatcht.
Zwei Event-Typen
| Typ | Quelle | Registrierung |
|---|---|---|
| Auto | Generiert aus content_construct=table-Plugins | automatisch bei installWebhookEvents() |
| Manual | Eigene Business-Events, vom Plugin emittiert | $this->webhookEvents im install() |
Ein Plugin kann beides haben: Auto-Events fuer CRUD-Operationen auf seinen Tabellen, plus manuelle Events fuer Vorgaenge wie "Bestellung abgeschlossen" oder "Zertifikat ausgestellt".
Auto-Events (aus content_table)
Jedes Plugin mit content_construct=table bekommt drei Auto-Events pro Tabelle:
| Event-Name | Trigger |
|---|---|
item.{table}.created | nach POST /api/backend/item |
item.{table}.updated | nach PATCH /api/backend/item |
item.{table}.deleted | vor DELETE /api/backend/item (Record noch verfuegbar) |
Beispiel: Ein Plugin mit content_table = ["my_articles"] bekommt automatisch item.my_articles.created, item.my_articles.updated und item.my_articles.deleted — zero code.
Der Auto-Installer in install_controller.php::installWebhookEvents() liest alle Tabellen aus plugin_backend und legt die Events in webhook_events an:
// Auszug aus install_controller.php
$pbQ = query("SELECT content_table FROM plugin_backend
WHERE plugin_id = $pluginID
AND content_construct = 'table'
AND content_table != ''");
while ($row = fetch_assoc($pbQ)) {
$table = real_escape_string($row['content_table']);
foreach (['created', 'updated', 'deleted'] as $op) {
$eventName = "item.$table.$op";
query("INSERT IGNORE INTO webhook_events
(plugin_id, event_name, event_type, content_table, description)
VALUES ($pluginID, '$eventName', 'auto', '$table', '...')");
}
}Manuelle Events registrieren
Fuer Business-Events, die nicht durch CRUD ausgeloest werden, befuelle $this->webhookEvents im install():
public function install()
{
// ... andere Properties
$this->webhookEvents = [
'{
"name": "blog.published",
"type": "manual",
"description": "Blog-Artikel veroeffentlicht"
}',
'{
"name": "blog.unpublished",
"type": "manual",
"description": "Blog-Artikel zurueckgezogen"
}'
];
}Jeder Eintrag ist ein JSON-String mit name, type (konventionell "manual") und optionaler description. Das install_controller::installWebhookEvents() nimmt INSERT IGNORE — doppeltes Install ist safe.
Event dispatchen
Im Plugin-Code (API-Model, Script, Controller):
require_once $_SERVER['DOCUMENT_ROOT'] . '/_core/system/webhook/WebhookDispatcher.php';
WebhookDispatcher::dispatch('blog.published', [
'post_id' => $postId,
'title' => $title,
'author_id' => $authorId,
'published_at' => date('c'),
]);dispatch($eventName, $data) ist non-blocking — es schreibt einen Eintrag in webhook_queue und kehrt sofort zurueck. Der WebhookDeliveryWorker picked den Job per console/bin process-webhooks auf und macht den HTTP-POST.
Sensitive-Data-Filter
Vor dem Queue-Insert filtert der Dispatcher automatisch sensible Keys aus dem Payload — rekursiv, case-insensitive:
password, secret, hash, key, token, saltBeispiel: ['email' => '[email protected]', 'password' => 'p4ss', 'settings' => ['api_key' => 'xyz']] wird zu ['email' => '[email protected]', 'settings' => []]. Das schuetzt vor versehentlichem Leaken von Credentials in Webhooks.
Zusaetzliche Keys koennen in WebhookDispatcher::SENSITIVE_KEYWORDS ergaenzt werden.
HMAC-Signatur
Der Worker signiert jeden Payload mit einem HMAC-SHA256-Hash und dem Subscription-Secret:
$signature = hash_hmac('sha256', $payload, $subscription->secret);Der Receiver validiert:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $payload, 'your_webhook_secret');
if (!hash_equals($expected, $signature)) {
http_response_code(401);
die('Invalid signature');
}
$data = json_decode($payload, true);Das Secret wird bei der Subscription-Erstellung im Backend (/admin/api-keys → Webhook-Section) einmal generiert und angezeigt — danach nicht wieder abrufbar. Rotation: neuer Secret via "Rotate"-Button, alter wird sofort invalidiert.
Delivery-Headers
Jeder ausgehende HTTP-POST hat folgende Header:
Content-Type: application/json
User-Agent: newmeta-CMS/3.0 Webhook
X-Webhook-Event: {event_name}
X-Webhook-Signature: {hmac_sha256}
X-Delivery-ID: {queue_id}Zusaetzlich: Custom-Headers aus webhook_subscriptions.custom_headers (JSON key:value) werden ans Ende gemerged.
Retry + Circuit Breaker
Bei 4xx/5xx-Response oder Connection-Error:
- Retry mit Exponential Backoff: nach dem initialen Fehlversuch folgen bis zu 5 Retries mit Wartezeit 30s → 2min → 8min → 32min → 2h (also insgesamt bis zu 6 Delivery-Attempts). Default aus
webhook_subscriptions.max_retries = 5, pro Subscription anpassbar (im Code:(int)($job['max_retries'] ?: 5)). - Circuit Breaker: Nach 10 aufeinanderfolgenden Fehlschlaegen wird die Subscription automatisch deaktiviert (
active = 0) unddisabled_reasongesetzt. Admin muss manuell reaktivieren. - Cleanup: Erfolgreich zugestellte Queue-Items werden nach 24h automatisch geloescht.
Worker starten
Der WebhookDeliveryWorker laeuft ueber die CLI:
php console/bin process-webhooks --time-limit=25Empfohlener Cron-Setup fuer 30s-Taktung:
* * * * * php /pfad/console/bin process-webhooks --time-limit=25
* * * * * sleep 30 && php /pfad/console/bin process-webhooks --time-limit=25Details zum Scheduling: Scheduled Tasks.
Event-Matching (Wildcards)
Subscriptions koennen drei Matching-Modi nutzen:
| Subscription-Events | Matched |
|---|---|
"blog.published" | nur blog.published |
"item.*" | alle Auto-Events (item.blog.created, item.users.deleted, …) |
"*" | alle Events systemweit |
Das ermoeglicht z. B. Audit-Subscriptions, die alles loggen, ohne bei jedem neuen Plugin manuell erweitert werden zu muessen.
System-Events (nicht plugin-gebunden)
Der Core dispatched 11 System-Events, die kein Plugin registrieren muss:
| Event | Zeitpunkt |
|---|---|
page.published | Nach Pagebuilder-Publish |
page.draft_discarded | Draft verworfen |
media.uploaded | Media-Upload erfolgreich |
media.deleted | Media-Datei geloescht |
auth.login | Backend-Login |
auth.logout | Backend-Logout |
order.created | Shop-Order (Stripe + PayPal dispatchen separat) |
form.submitted | Form-Submit ueber /api/form/submit |
user.registered | Neuer Frontend-User registriert |
emailmarketing.subscribed | Newsletter-Subscribe |
deployment.completed | Nach Deploy-Target-Run |
Datenbank-Tabellen
| Tabelle | Zweck |
|---|---|
webhook_events | Event-Registry (pro Plugin, auto/manual) |
webhook_subscriptions | User-erstellte Abonnements (Name, URL, Secret, Event-Liste, Custom-Headers, max_retries, failure_count, disabled_reason) |
webhook_queue | Pending/Retry/Delivered-Queue |
webhook_deliveries | Log (Request-Payload, Response-Code, Duration) |
Schema-Migration: _migrations/core/010_create_webhooks.sql. Details in Migrations.
Management-UI
- Subscriptions anlegen:
/admin/api-keys→ Tab "Webhooks" → Event-Picker (grouped Multi-Select mit "Select All" pro Plugin), HTTPS-URL, Secret-Auto-Generator, Test-Ping-Button - Delivery-Historie:
/admin/task-manager→ Tab "Webhooks" → Queue + Stats (Success-Rate)
Testing-Tipps
- Test-Ping: Im Webhook-Management-Panel sendet
Test-Pingeinen Dummy-Event an die Subscription-URL. Ideal fuer Initial-Setup. - ngrok fuer lokale Receiver:
ngrok http 3001und diengrok-URL als Subscription-URL eintragen — HTTPS wird erzwungen, das passt. - Queue-Inspektion:
SELECT * FROM webhook_queue WHERE status IN ('pending', 'retry') ORDER BY next_retry_at.retry_countunderror_messagezeigen, warum ein Delivery haengt.
Haeufige Fehler
dispatch() ohne require_once
WebhookDispatcher ist kein Autoloader-Teil — jeder Call muss vorher die Datei inkludieren: require_once $_SERVER['DOCUMENT_ROOT'] . '/_core/system/webhook/WebhookDispatcher.php'. Sonst Fatal error: Class 'WebhookDispatcher' not found.
HTTP statt HTTPS als Subscription-URL
Die Subscription-Verwaltung zwingt https://. Fuer lokales Testing auf http:// ausweichen (ngrok liefert HTTPS) — nie produktive Webhooks auf HTTP, weil die Signatur-Validierung gegen MITM nichts nuetzt.
Receiver antwortet langsam → Timeouts
Der Worker hat 10s cURL-Timeout. Receiver, die laenger brauchen (synchrone DB-Queries, externe API-Calls), verursachen Retries. Empfehlung: Receiver antwortet sofort mit 200, macht die Arbeit asynchron (Queue oder Background-Job).
INSERT IGNORE — doppelte Events erkennen
installWebhookEvents() nutzt INSERT IGNORE, Events mit demselben (plugin_id, event_name) werden stumm ignoriert. Wenn eine description aktualisiert werden soll, muss das Plugin in update() einen UPDATE webhook_events-Call machen — ein erneutes install() aendert nichts.
Sensitive-Keyword-Filter matcht zu aggressiv
Der Filter matcht jeden Key, der einen der Keywords enthaelt (case-insensitive substring). api_key, key, private_key werden gefiltert — auch harmlose Felder wie category_key oder key_user_id sind weg. Das ist bewusst konservativ. Bei Bedarf: sensible Daten in flachen, nicht-keyword-matched Sub-Keys (z. B. public_identifier) halten.
Siehe auch
- Plugin-Anatomie —
$this->webhookEvents-Property - API-Endpunkte — Events aus Endpoints dispatchen
- Scheduled Tasks —
process-webhooks-Worker per Cron - Migrations —
_migrations/core/010_create_webhooks.sql - Webhook-Management-Panel:
/admin/api-keys→ Tab "Webhooks" - Delivery-Historie:
/admin/task-manager→ Tab "Webhooks"