Skip to content

How-to : API de relations d'articles ​

Référence FT : FT334 (NENE2-FT/relatedlog) — Relations typées entre articles avec création d'inverse automatique, types de relation symétriques et asymétriques, filtre par type, et stubs de relation intégrés dans les réponses GET, 17 tests / 40+ assertions PASS.

Ce guide montre comment modéliser des relations typées entre des éléments de contenu — related, sequel, prequel, reference — avec une gestion automatique des inverses pour que chaque relation reste cohérente dans les deux sens.

Schéma ​

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

CREATE TABLE article_relations (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    article_id    INTEGER NOT NULL REFERENCES articles(id),
    related_id    INTEGER NOT NULL REFERENCES articles(id),
    relation_type TEXT    NOT NULL,
    created_at    TEXT    NOT NULL,
    UNIQUE(article_id, related_id, relation_type)
);

UNIQUE(article_id, related_id, relation_type) empêche les arêtes de relation en doublon pour le même type. Différents types entre la même paire sont autorisés.

Types de relation et inverses ​

Type soumisInverse créé automatiquement
relatedrelated (symétrique)
sequelprequel
prequelsequel
referencereference (symétrique)

Quand A→B est sequel, le serveur insère atomiquement B→A comme prequel. Supprimer A→B supprime aussi B→A.

Endpoints ​

MéthodeCheminDescription
POST/articlesCréer un article
GET/articles/{id}Obtenir un article avec les relations intégrées
POST/articles/{id}/relationsAjouter une relation
GET/articles/{id}/relationsLister les relations (optionnel ?type=)
DELETE/articles/{id}/relations/{relatedId}?type=Supprimer une relation (et son inverse)

Créer un article ​

php
POST /articles
{"title": "Hello", "body": "World"}
→ 201  {"id": 1, "title": "Hello", "body": "World", "created_at": "..."}

// Titre manquant
POST /articles  {"body": "No title"}
→ 422

// Corps manquant
POST /articles  {"title": "No body"}
→ 422

GET Article avec relations intégrées ​

php
GET /articles/1
→ 200
{
  "data": {"id": 1, "title": "Intro", ...},
  "relations": [
    {
      "relation": {"relation_type": "sequel"},
      "related":  {"id": 2, "title": "Follow-up"}
    }
  ]
}

// Pas encore de relations
GET /articles/1
→ 200  {"data": {...}, "relations": []}

GET /articles/9999
→ 404

Ajouter une relation ​

php
POST /articles/1/relations
{"related_id": 2, "relation_type": "sequel"}
→ 201  {"relation_type": "sequel", "article_id": 1, "related_id": 2}

// L'inverse est auto-inséré : l'article 2 a maintenant une relation "prequel" pointant vers 1
GET /articles/2/relations
→ 200  {"data": [{"relation_type": "prequel", "related_id": 1}]}

Relation symétrique ​

php
POST /articles/1/relations
{"related_id": 2, "relation_type": "related"}
→ 201

// B obtient aussi automatiquement une relation "related" vers A
GET /articles/2/relations
→ 200  {"data": [{"related_id": 1, "relation_type": "related"}]}

Cas d'erreur ​

php
// related_id inconnu
POST /articles/1/relations  {"related_id": 9999, "relation_type": "related"}
→ 404

// Doublon (même paire + même type existe déjà)
POST /articles/1/relations  {"related_id": 2, "relation_type": "related"}
→ 409

// Auto-relation
POST /articles/1/relations  {"related_id": 1, "relation_type": "related"}
→ 422

// Type de relation invalide
POST /articles/1/relations  {"related_id": 2, "relation_type": "not-a-type"}
→ 422

Plusieurs types entre la même paire ​

La même paire peut avoir plusieurs types de relation différents :

php
POST /articles/1/relations  {"related_id": 2, "relation_type": "related"}   → 201
POST /articles/1/relations  {"related_id": 2, "relation_type": "reference"} → 201

GET /articles/1/relations
→ 200  {"data": [
    {"related_id": 2, "relation_type": "related"},
    {"related_id": 2, "relation_type": "reference"}
  ]}

Lister les relations ​

php
// Toutes les relations
GET /articles/1/relations
→ 200  {"data": [{...}, {...}]}

// Filtrer par type
GET /articles/1/relations?type=sequel
→ 200  {"data": [{"related_id": 2, "relation_type": "sequel"}]}

// Article inconnu
GET /articles/9999/relations
→ 404

Supprimer une relation ​

php
DELETE /articles/1/relations/2?type=related
→ 200  {"deleted": true}

// L'inverse est aussi supprimé automatiquement
GET /articles/2/relations
→ 200  {"data": []}  // n'a plus de "related" vers 1

// Non trouvé
DELETE /articles/1/relations/2?type=related
→ 404

// Paramètre de requête type manquant
DELETE /articles/1/relations/2
→ 422

Implémentation — Gestion atomique des inverses ​

php
private function addRelation(int $articleId, int $relatedId, string $type): void
{
    $this->db->beginTransaction();

    $inverse = match ($type) {
        'sequel'    => 'prequel',
        'prequel'   => 'sequel',
        default     => $type,   // related, reference → symétrique
    };

    $this->repo->insert($articleId, $relatedId, $type);
    $this->repo->insert($relatedId, $articleId, $inverse);

    $this->db->commit();
}

private function removeRelation(int $articleId, int $relatedId, string $type): void
{
    $inverse = match ($type) {
        'sequel'    => 'prequel',
        'prequel'   => 'sequel',
        default     => $type,
    };

    $this->db->beginTransaction();
    $this->repo->delete($articleId, $relatedId, $type);
    $this->repo->delete($relatedId, $articleId, $inverse);
    $this->db->commit();
}

Envelopper les deux insertions/suppressions dans une transaction — si l'une échoue, aucune n'est commitée.


Ce qu'il ne faut PAS faire ​

Anti-patternRisque
Insérer une relation sans vérifier l'existence de l'articleViolation FK ou insertion silencieuse de 0 ligne ; toujours 404 sur les IDs inconnus
Pas de transaction autour de l'insertion directe + inverseUn échec partiel laisse des données asymétriques (A→B existe mais B→A n'existe pas)
Pas de UNIQUE(article_id, related_id, relation_type)Les arêtes en doublon gonflent les compteurs de liste
Autoriser les auto-relationsCycles dans la traversée des relations ; sequel de lui-même n'a aucun sens
Supposer que tous les types sont symétriquessequel→sequel (incorrect) au lieu de prequel
Supprimer uniquement l'arête directeL'inverse orphelin reste ; B "voit" toujours A comme un prequel après la suppression de A

Publié sous licence MIT.