Skip to content

How-to : Construire un cycle de vie de brouillon de contenu (Draft → Published → Archived) avec NENE2

Ce guide explique comment construire un système de gestion d'articles avec une machine à états draft/publish/archive, où seul l'auteur peut effectuer les transitions et où seuls les articles publiés sont visibles par les lecteurs.

Field Trial : FT142 Version NENE2 : ^1.5 Sujets couverts : machine à états avec enum, gardes de transition, vérification de propriété de l'auteur, liste publique filtrée par statut, stabilité du tri à la même seconde


Ce que nous construisons

  • POST /articles — créer un article (commence toujours en draft)
  • GET /articles — lister uniquement les articles publiés
  • GET /articles/{id} — obtenir un article (l'auteur voit n'importe quel statut ; les autres voient seulement published)
  • PUT /articles/{id} — modifier un article (draft uniquement, auteur uniquement)
  • POST /articles/{id}/publish — transition draft → published (auteur uniquement)
  • POST /articles/{id}/archive — transition published → archived (auteur uniquement)

Schéma de la base de données

sql
CREATE TABLE articles (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    author_id    INTEGER NOT NULL,
    title        TEXT    NOT NULL,
    body         TEXT    NOT NULL DEFAULT '',
    status       TEXT    NOT NULL DEFAULT 'draft',
    published_at TEXT,
    archived_at  TEXT,
    created_at   TEXT    NOT NULL,
    updated_at   TEXT    NOT NULL,
    CHECK (status IN ('draft', 'published', 'archived')),
    FOREIGN KEY (author_id) REFERENCES users(id)
);

published_at et archived_at sont nullables — ils ne sont définis qu'à la transition correspondante.


Enum ArticleStatus avec gardes de transition

php
enum ArticleStatus: string
{
    case Draft     = 'draft';
    case Published = 'published';
    case Archived  = 'archived';

    public function canEdit(): bool
    {
        return $this === self::Draft;
    }

    public function canPublish(): bool
    {
        return $this === self::Draft;
    }

    public function canArchive(): bool
    {
        return $this === self::Published;
    }
}

Le gestionnaire lit le statut actuel, appelle la méthode de garde et retourne 422 si la transition est invalide :

php
$status = ArticleStatus::tryFrom($article['status']) ?? ArticleStatus::Draft;

if (!$status->canPublish()) {
    return $this->responseFactory->create(['error' => 'only draft articles can be published'], 422);
}

Transitions valides :

  • draft → published (via publish)
  • published → archived (via archive)
  • Il n'y a pas de retour vers draft.

Visibilité de l'auteur — brouillon caché aux autres

Les non-auteurs ne peuvent pas lire les brouillons. Retourner 404 (pas 403) pour éviter de révéler que l'article existe :

php
if ($article['status'] !== 'published' && $article['author_id'] !== $actorId) {
    return $this->responseFactory->create(['error' => 'article not found'], 404);
}

Retourner 403 confirmerait que l'article existe. 404 est le bon choix pour du contenu qui n'est pas encore public.


Stabilité du tri à la même seconde

Quand plusieurs articles sont publiés dans la même seconde, ORDER BY published_at DESC seul donne un ordre non déterministe. Ajouter id DESC comme critère de départage :

sql
SELECT ... FROM articles WHERE status = 'published' ORDER BY published_at DESC, id DESC

Un id plus élevé signifie créé plus tard, ce qui effectivement trie par ordre d'insertion dans la même seconde.


Pièges courants

PiègeCorrection
Retourner 403 pour les lectures de brouillon par des non-auteursRetourner 404 — empêche la révélation de l'existence du contenu
Autoriser la réouverture published → draftcanEdit() retourne false sauf pour Draft ; pas d'endpoint "dépublication"
Publier un article déjà publiécanPublish() retourne false pour Published → 422
Archiver un brouilloncanArchive() retourne false sauf pour Published → 422
Ordre de liste non déterministe au même timestampAjouter id DESC comme tri secondaire

Publié sous licence MIT.