Skip to content

Como Construir um Ciclo de Vida de Rascunho de Conteúdo (Rascunho → Publicado → Arquivado) com NENE2 ​

Este guia percorre a construção de um sistema de gerenciamento de artigos com uma máquina de estados draft/publish/archive, onde apenas o autor pode fazer transições de estado e apenas artigos publicados são visíveis para leitores.

Field Trial: FT142 Versão do NENE2: ^1.5 Tópicos abordados: máquina de status com enum, guardas de transição, verificação de propriedade do autor, lista pública filtrada por status, estabilidade de ordenação no mesmo segundo


O que estamos construindo ​

  • POST /articles — criar um artigo (sempre começa como draft)
  • GET /articles — listar apenas artigos publicados
  • GET /articles/{id} — obter artigo (autor vê qualquer status; outros veem apenas published)
  • PUT /articles/{id} — editar artigo (somente draft, somente o autor)
  • POST /articles/{id}/publish — transição draft → published (somente o autor)
  • POST /articles/{id}/archive — transição published → archived (somente o autor)

Schema do banco de dados ​

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 e archived_at são nullable — são definidos apenas na transição correspondente.


Enum ArticleStatus com guardas de transição ​

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

O handler lê o status atual, chama o método de guarda e retorna 422 se a transição for inválida:

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

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

Transições válidas:

  • draft → published (via publish)
  • published → archived (via archive)
  • Não existe transição de volta para draft.

Visibilidade do autor — rascunho oculto de outros ​

Não-autores não podem ler rascunhos. Retorne 404 (não 403) para evitar vazar que o artigo existe:

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

Retornar 403 confirmaria a existência do artigo. 404 é a escolha correta para conteúdo que ainda não é público.


Estabilidade de ordenação no mesmo segundo ​

Quando múltiplos artigos são publicados dentro do mesmo segundo, ORDER BY published_at DESC sozinho dá uma ordem não determinística. Adicione id DESC como desempate:

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

Um id maior significa criado mais recentemente, então isso ordena efetivamente pela ordem de inserção dentro do mesmo segundo.


Armadilhas comuns ​

ArmadilhaCorreção
Retornar 403 para leituras de rascunho por não-autoresRetornar 404 — previne vazamento de existência do conteúdo
Permitir reabertura published → draftcanEdit() retorna false exceto para Draft; sem endpoint "unpublish"
Publicar um artigo já publicadocanPublish() retorna false para Published → 422
Arquivar um rascunhocanArchive() retorna false exceto para Published → 422
Ordem de lista não determinística no mesmo timestampAdicione id DESC como ordenação secundária

Publicado sob a licença MIT.