Skip to content

Implementierungsleitfaden für Content-Versionierung

Überblick

Dieser Leitfaden erklärt, wie mit NENE2 Content-Versionierung (vollständige Verlaufsprotokollierung, Referenzierung bestimmter Versionen, Rollback) implementiert wird. Alle Versionen von Artikeländerungen werden append-only gespeichert und Rollbacks zu beliebigen Revisionen werden bereitgestellt.


DB-Schema

sql
CREATE TABLE articles (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    title           TEXT    NOT NULL,
    body            TEXT    NOT NULL,
    current_version INTEGER NOT NULL DEFAULT 1,
    created_at      TEXT    NOT NULL,
    updated_at      TEXT    NOT NULL
);

CREATE TABLE article_versions (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    article_id INTEGER NOT NULL,
    version    INTEGER NOT NULL,
    title      TEXT    NOT NULL,
    body       TEXT    NOT NULL,
    created_at TEXT    NOT NULL,
    UNIQUE (article_id, version),
    FOREIGN KEY (article_id) REFERENCES articles(id)
);

articles ist die übergeordnete Tabelle mit der aktuellen neuesten Version. article_versions sammelt den Inhaltänderungsverlauf append-only.


Endpunkt-Design

MethodePfadBeschreibung
POST/articlesArtikel erstellen (als v1 initialen Commit)
GET/articles/{id}Neueste Version abrufen
PUT/articles/{id}Aktualisieren (neue Version anhängen)
GET/articles/{id}/versionsVersionsliste
GET/articles/{id}/versions/{version}Bestimmte Version abrufen
POST/articles/{id}/rollbackZu angegebener Version zurücksetzen

Design-Kernpunkte

Append-Only-Versionierung

Sowohl update als auch rollback hängen eine neue Version an. Bestehende Zeilen werden nicht überschrieben:

php
public function update(int $id, string $title, string $body, string $now): bool
{
    $article     = $this->find($id);
    $nextVersion = (int) $article['current_version'] + 1;

    $this->db->insert(
        'UPDATE articles SET title = ?, body = ?, current_version = ?, updated_at = ? WHERE id = ?',
        [$title, $body, $nextVersion, $now, $id],
    );
    $this->db->insert(
        'INSERT INTO article_versions (article_id, version, title, body, created_at) VALUES (?, ?, ?, ?, ?)',
        [$id, $nextVersion, $title, $body, $now],
    );
    return true;
}

Vorteil: Jede beliebige Version ist immer referenzierbar. DB-Rollback und logischer Rollback sind unabhängig voneinander.

Rollback = Als neue Version speichern

Rollback ist die Operation "mit dem Inhalt einer bestimmten Version eine neue Version erstellen". Dadurch bleibt der Rollback selbst im Verlauf und kann für Audits verwendet werden:

v1: Original title
v2: Modified title
v3: Original title  ← Rollback zu v1 wird hier als neue Version gespeichert
php
public function rollback(int $id, int $version, string $now): bool
{
    $target      = $this->findVersion($id, $version);   // Rollback-Ziel
    $article     = $this->find($id);
    $nextVersion = (int) $article['current_version'] + 1;

    // Inhalt der Zielversion als neue Version speichern
    $this->db->insert('UPDATE articles SET title = ?, body = ?, current_version = ? ...', [...]);
    $this->db->insert('INSERT INTO article_versions ...', [$id, $nextVersion, $target['title'], $target['body'], $now]);
    return true;
}

Versionsliste ohne Body

Die Listen-API gibt nur Metadaten ohne body zurück. Beim Abrufen einer bestimmten Version wird body einbezogen:

GET /articles/{id}/versions → [{version: 1, title: "...", created_at: "..."}, ...]
GET /articles/{id}/versions/1 → {version: 1, title: "...", body: "...", created_at: "..."}

PHPStan: Nullable-Rückgabewert und null-Prüfungs-Konsistenz

Wenn nach einem Rollback find() erneut aufgerufen wird, kann PHPStan die null-Prüfung als "immer wahr" betrachten. Das Problem lässt sich lösen, indem formatArticle(?array) so gestaltet wird, dass null akzeptiert wird, ohne assert zu benötigen:

php
// Falsch: assert wird von PHPStan als "immer wahr" betrachtet
$article = $this->repo->find($id);
assert($article !== null);
return $this->json->create($this->formatArticle($article));

// Richtig: formatArticle so gestalten, dass null akzeptiert wird
return $this->json->create(array_merge($this->formatArticle($this->repo->find($id)), ['rolled_back_from' => $version]));

Beispielantworten

POST /articles

json
{
  "id": 1,
  "title": "My Post",
  "body": "Hello world",
  "current_version": 1,
  "created_at": "2026-01-01T00:00:00Z",
  "updated_at": "2026-01-01T00:00:00Z"
}

GET /articles/{id}/versions

json
{
  "versions": [
    {"id": 1, "article_id": 1, "version": 1, "title": "My Post", "created_at": "..."},
    {"id": 2, "article_id": 1, "version": 2, "title": "Updated", "created_at": "..."}
  ],
  "count": 2
}

POST /articles/{id}/rollback

json
{
  "id": 1,
  "title": "My Post",
  "current_version": 3,
  "rolled_back_from": 1
}

Referenzimplementierung

../NENE2-FT/contentvlog/ — FT162 Field Trial (18 Tests, Append-Only-Verlauf, Rollback)

Veröffentlicht unter der MIT-Lizenz.