07_POST_UPDATE_HOOKS.md 7.0 KB

Post-Update-Hook und Migrationen

Überblick

Nach einem erfolgreichen Ausrollen führt der Client einen Post-Update-Schritt aus. Er besteht aus zwei unabhängigen Mechanismen, die einzeln oder gemeinsam genutzt werden:

  1. Migrationen – geordnete, einmalig laufende Skripte, die mit dem Release ausgeliefert werden. Der übliche Ort für Datenbankänderungen.
  2. Projekt-Callback – eine Funktion des Projekts, die nach jedem Update läuft. Für Cache leeren, abgeleitete Dateien neu bauen, Rechte setzen.

Relevante Dateien:

  • manage-client/lib/hooks.php – beide Mechanismen
  • MANAGE_MIGRATIONS_DIR, MANAGE_MIGRATIONS_STATE, MANAGE_UPDATE_POST_HOOK

Reihenfolge: erst die Migrationen, dann der Callback – damit der Callback sich auf das neue Schema verlassen kann. Scheitert eine Migration, wird der Callback nicht ausgeführt.

Migrationen

Ablage

Migrationen liegen im Verzeichnis aus MANAGE_MIGRATIONS_DIR (Standard migrations/ im Anwendungsstamm) und werden mit dem Release-Paket ausgeliefert.

migrations/
  2026-08-20-01-add-orders-index.php
  2026-08-21-01-backfill-categories.php

Ausgeführt wird in Dateinamen-Reihenfolge. Ein Datum als Präfix mit laufender Nummer sortiert zuverlässig. Der Dateiname ohne .php ist die Kennung der Migration; wird eine bereits ausgeführte Datei umbenannt, läuft sie erneut.

Aufbau

Empfohlene Form – die Datei gibt eine Funktion zurück:

<?php

return function (array $context): void {
    $context["pdo"]->exec("ALTER TABLE orders ADD INDEX idx_created (created_at)");
};

Alternativ definiert die Datei eine Funktion up():

<?php

function up(array $context): void
{
    // …
}

Die zurückgegebene Funktion ist die bessere Wahl: Zwei Migrationen, die beide up() definieren, würden sich im selben Prozess in die Quere kommen.

Der Kontext

[
    "app_root"     => "/var/www/meinprojekt",
    "instance"     => "meinprojekt-prod",
    "from_version" => "v1.2.3",
    "to_version"   => "v1.3.0",
    "backup_dir"   => "/…/data/manage/updates/20260820-092114-v1.3.0",
    "run_id"       => "20260820-092114",
    "migration_id" => "2026-08-20-01-add-orders-index",
    "pdo"          => PDO,   // nur wenn MANAGE_BACKUP_DATABASE konfiguriert ist
]

pdo verwendet die Zugangsdaten, die ohnehin für den Datenbank-Dump konfiguriert sind. Eine zweite Konfiguration ist nicht nötig. Projekte ohne Datenbank arbeiten mit app_root.

Beispiel: Datenbank

<?php

return function (array $context): void {
    $pdo = $context["pdo"];

    // Idempotent halten: die Migration kann nach einem Teilfehler erneut laufen.
    $exists = $pdo->query(
        "SELECT COUNT(*) FROM information_schema.statistics
         WHERE table_schema = DATABASE()
           AND table_name = 'orders'
           AND index_name = 'idx_created'"
    )->fetchColumn();

    if ((int) $exists === 0) {
        $pdo->exec("ALTER TABLE orders ADD INDEX idx_created (created_at)");
    }
};

Beispiel: JSON-Daten

<?php

return function (array $context): void {
    $file = $context["app_root"] . "/data/products.json";
    $data = json_decode((string) file_get_contents($file), true) ?: [];

    foreach ($data as $index => $product) {
        if (!array_key_exists("category_id", $product)) {
            $data[$index]["category_id"] = null;
        }
    }

    file_put_contents(
        $file,
        json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
    );
};

Zustand

Ausgeführte Migrationen werden in MANAGE_MIGRATIONS_STATE festgehalten:

{
    "applied": [
        {
            "id": "2026-08-20-01-add-orders-index",
            "applied_at": "2026-08-20T09:21:14+00:00",
            "version": "v1.3.0",
            "duration_ms": 42
        }
    ]
}

Diese Datei liegt im Datenverzeichnis und ist damit von Updates ausgenommen. Sie sollte im Backup enthalten sein, wenn data/manage/ in den Quellen steht – ist es standardmäßig nicht, weil das Backup-Verzeichnis sich sonst selbst sichern würde. Wer den Migrationszustand mitsichern will, nimmt ihn einzeln auf:

["as" => "manage", "file" => "data/manage/migrations.json"],

Projekt-Callback

define("MANAGE_UPDATE_POST_HOOK", [
    "file"     => MANAGE_APP_ROOT . "/includes/after-update.php",
    "callback" => "myProjectAfterUpdate",
]);
<?php

function myProjectAfterUpdate(array $context): void
{
    // Cache leeren, abgeleitete Dateien neu bauen, Verzeichnis anlegen …
    array_map("unlink", glob($context["app_root"] . "/data/cache/*.php") ?: []);
}

Als Fehlschlag gilt: eine geworfene Exception, return false oder return ["success" => false, "error" => "…"]. Alles andere gilt als Erfolg.

Der Callback erhält denselben Kontext wie eine Migration, zusätzlich migrations mit der Liste der in diesem Lauf ausgeführten Kennungen.

Fehlerverhalten

Der wichtigste Punkt: Zu diesem Zeitpunkt sind die Dateien bereits ausgerollt, und es gibt keine Rücknahme. Ein Fehler wird deshalb laut gemeldet statt still verschluckt.

Konkret:

  • Der Lauf stoppt bei der ersten fehlgeschlagenen Migration. Die folgenden bleiben offen und werden nicht versucht.
  • Der Callback wird bei einer fehlgeschlagenen Migration übersprungen.
  • manageUpdateApply() kehrt normal zurück, mit deployed => true und hook["success"] => false.
  • Die Kommandozeile beendet sich mit Exit-Code 1 und nennt die betroffene Datei – meldet aber ausdrücklich, dass das Ausrollen erfolgreich war.
  • Die Oberfläche zeigt einen roten Hinweis mit dem Namen der Migration.
  • Beides steht im Client-Protokoll.

Wiederherstellung nach einem Fehler:

# 1. Ursache beheben (Migration korrigieren, Rechte setzen, Datenbank prüfen)
# 2. Offene Migrationen ansehen
php manage-client/bin/manage-client.php migrate --dry-run
# 3. Nachziehen
php manage-client/bin/manage-client.php migrate

Ein erneutes update --force rollt die Dateien nochmals aus, führt aber keine bereits ausgeführten Migrationen erneut aus.

Idempotenz

Migrationen sollen mehrfach ausführbar sein. Grund: Wenn eine Migration mittendrin scheitert – etwa nach der Hälfte einer Datenumstellung – wird sie nicht als ausgeführt vermerkt und läuft beim nächsten migrate erneut von vorn. Nur eine idempotente Migration übersteht das unbeschadet.

Praktisch heißt das: vor dem Ändern prüfen, ob die Änderung schon da ist, und Datenumstellungen so schreiben, dass bereits umgestellte Sätze übersprungen werden.

Ohne Post-Update-Schritt ausrollen

php manage-client/bin/manage-client.php update --skip-hook

Rollt nur die Dateien aus. Die Migrationen bleiben offen und können später mit migrate nachgezogen werden. Nützlich, wenn die Dateien dringend gebraucht werden, die Datenbankänderung aber in ein Wartungsfenster gehört.

Weiter