Skip to content

How-to: Content-Entwurfs-Lebenszyklus (Entwurf → Veröffentlicht → Archiviert) mit NENE2 ​

Diese Anleitung führt durch den Aufbau eines Artikelverwaltungssystems mit einer Entwurf/Veröffentlichung/Archivierung-Zustandsmaschine, bei der nur der Autor Zustandsübergänge auslösen kann und nur veröffentlichte Artikel für Leser sichtbar sind.

Field Trial: FT142
NENE2-Version: ^1.5
Behandelte Themen: Zustandsmaschine mit Enum, Übergangswächter, Autor-Eigentumsrecht-Prüfung, statusgefiltertes Öffentlichkeitslistung, Sortierstabilität bei gleicher Sekunde


Was wir bauen ​

  • POST /articles — Artikel erstellen (startet immer als draft)
  • GET /articles — Nur veröffentlichte Artikel auflisten
  • GET /articles/{id} — Artikel abrufen (Autor sieht jeden Status; andere sehen nur published)
  • PUT /articles/{id} — Artikel bearbeiten (nur Entwurf, nur Autor)
  • POST /articles/{id}/publish — Übergang draft → published (nur Autor)
  • POST /articles/{id}/archive — Übergang published → archived (nur Autor)

Datenbankschema ​

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 und archived_at sind nullable — sie werden nur beim entsprechenden Übergang gesetzt.


ArticleStatus-Enum mit Übergangswächtern ​

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

Der Handler liest den aktuellen Status, ruft die Wächtermethode auf und gibt 422 zurück, wenn der Übergang ungültig ist:

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

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

Gültige Übergänge:

  • draft → published (via publish)
  • published → archived (via archive)
  • Es gibt keinen Übergang zurück zum Entwurf.

Autor-Sichtbarkeit — Entwurf für andere verborgen ​

Nicht-Autoren können Entwürfe nicht lesen. 404 zurückgeben (nicht 403), um nicht preiszugeben, dass der Artikel existiert:

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

403 zurückzugeben würde bestätigen, dass der Artikel existiert. 404 ist die richtige Wahl für Inhalte, die noch nicht öffentlich sind.


Sortierstabilität bei gleicher Sekunde ​

Wenn mehrere Artikel innerhalb derselben Sekunde veröffentlicht werden, ergibt ORDER BY published_at DESC allein eine nicht-deterministische Reihenfolge. id DESC als Tiebreaker hinzufügen:

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

Eine höhere id bedeutet später erstellt, wodurch innerhalb derselben Sekunde effektiv nach Einfüge-Reihenfolge sortiert wird.


Häufige Fehler ​

FehlerLösung
403 für Nicht-Autor-Entwurfslesungen zurückgeben404 zurückgeben — verhindert Inhalts-Existenz-Leck
published → draft-Wiedereröffnung erlaubencanEdit() gibt false zurück, außer bei Draft; kein "Depublizieren"-Endpunkt
Bereits veröffentlichten Artikel veröffentlichencanPublish() gibt false für Published zurück → 422
Entwurf archivierencanArchive() gibt false zurück, außer bei Published → 422
Nicht-deterministische Listen-Reihenfolge bei gleichem Zeitstempelid DESC als sekundäre Sortierung hinzufügen

Veröffentlicht unter der MIT-Lizenz.