Skip to content

Guide d'implémentation du versionnage de contenu

Vue d'ensemble

Ce guide explique comment implémenter le versionnage de contenu avec NENE2 (conservation de tout l'historique, référence à une version spécifique, rollback). Les modifications d'articles sont conservées en mode append-only avec tous les versions, et un rollback vers n'importe quelle révision est possible.


Schéma DB

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 est la table parente qui contient la version actuelle la plus récente. article_versions accumule l'historique des modifications de contenu en mode append-only.


Conception des endpoints

MéthodeCheminDescription
POST/articlesCréer un article (commit initial en v1)
GET/articles/{id}Obtenir la dernière version
PUT/articles/{id}Mettre à jour (appender une nouvelle version)
GET/articles/{id}/versionsListe des versions
GET/articles/{id}/versions/{version}Obtenir une version spécifique
POST/articles/{id}/rollbackRollback vers la version indiquée

Points clés de conception

Versionnage append-only

Les deux opérations update et rollback appendenent une nouvelle version. L'existant n'est pas écrasé :

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;
}

Avantage : n'importe quelle version est toujours accessible. Le rollback DB et le rollback logique sont indépendants.

Rollback = Sauvegardé comme nouvelle version

Le rollback est l'opération "créer une nouvelle version avec le contenu d'une version spécifique". Cela permet que le rollback lui-même reste dans l'historique, utilisable pour les audits :

v1: Titre original
v2: Titre modifié
v3: Titre original  ← rollback vers v1 sauvegardé ici comme nouvelle version
php
public function rollback(int $id, int $version, string $now): bool
{
    $target      = $this->findVersion($id, $version);   // cible du rollback
    $article     = $this->find($id);
    $nextVersion = (int) $article['current_version'] + 1;

    // Sauvegarder le contenu de la version cible comme nouvelle version
    $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;
}

La liste des versions exclut le corps

L'API de liste retourne uniquement les métadonnées sans body. body est inclus lors de la récupération individuelle :

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

PHPStan : cohérence entre valeur de retour nullable et vérification null

Lors du rappel de find() après un rollback, PHPStan peut considérer la vérification null comme "toujours vraie". Concevoir formatArticle(?array) pour accepter null évite d'avoir besoin d'assert :

php
// Incorrect : l'assert est considéré "toujours vrai" par PHPStan
$article = $this->repo->find($id);
assert($article !== null);
return $this->json->create($this->formatArticle($article));

// Correct : concevoir formatArticle pour accepter null
return $this->json->create(array_merge($this->formatArticle($this->repo->find($id)), ['rolled_back_from' => $version]));

Exemples de réponse

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
}

Implémentation de référence

../NENE2-FT/contentvlog/ — FT162 field trial (18 tests · historique append-only · rollback)

Publié sous licence MIT.