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:
Relevante Dateien:
manage-client/lib/hooks.php – beide MechanismenMANAGE_MIGRATIONS_DIR, MANAGE_MIGRATIONS_STATE, MANAGE_UPDATE_POST_HOOKReihenfolge: 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 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.
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.
[
"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.
<?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)");
}
};
<?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)
);
};
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"],
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.
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:
manageUpdateApply() kehrt normal zurück, mit deployed => true und
hook["success"] => false.1 und nennt die betroffene Datei –
meldet aber ausdrücklich, dass das Ausrollen erfolgreich war.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.
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.
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.
manageUpdateRunMigrations()