Skip to content

How-to : API de tags / étiquettes

Ce guide démontre une API de tagging d'entités générique où des tags arbitraires peuvent être attachés à n'importe quel ID d'entité, avec recherche inverse basée sur les tags.

Vue d'ensemble du pattern

  • Les tags sont stockés globalement et identifiés par un slug (a-z0-9-, 1–50 caractères).
  • Toute entité (identifiée par un ID entier) peut avoir plusieurs tags.
  • POST /tags — Créer ou récupérer un tag (find-or-create ; idempotent).
  • GET /tags — Lister tous les tags connus.
  • GET /tags/{tag}/entities — Recherche inverse : quelles entités ont ce tag ?
  • POST /entities/{entityId}/tags — Attacher un tag à une entité.
  • GET /entities/{entityId}/tags — Lister tous les tags d'une entité.
  • DELETE /entities/{entityId}/tags/{tag} — Détacher un tag d'une entité.

Schéma

sql
CREATE TABLE IF NOT EXISTS tags (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    name       TEXT    NOT NULL UNIQUE,
    created_at TEXT    NOT NULL
);
CREATE TABLE IF NOT EXISTS entity_tags (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    entity_id  INTEGER NOT NULL,
    tag_id     INTEGER NOT NULL,
    created_at TEXT    NOT NULL,
    FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
    UNIQUE (entity_id, tag_id)
);

Pattern find-or-create

La création de tag est idempotente — POST /tags retourne 201 à la première création, 200 si le tag existe déjà :

php
public function findOrCreate(string $name): array
{
    $existing = $this->findByName($name);
    if ($existing !== null) {
        return $existing;
    }
    $this->pdo->prepare(
        'INSERT INTO tags (name, created_at) VALUES (:name, :now)'
    )->execute([':name' => $name, ':now' => $this->now()]);

    return $this->findByName($name) ?? [];
}

Le handler attachTag utilise aussi find-or-create pour que les clients puissent attacher des tags sans une étape de création séparée.

Validation du nom de tag

Les noms de tags sont normalisés en minuscules et validés avec un regex de format strict :

php
private const string TAG_PATTERN = '/\A[a-z0-9-]{1,50}\z/';

$name = strtolower(trim((string) ($body['name'] ?? '')));
if (!preg_match(self::TAG_PATTERN, $name)) {
    return $this->problem(422, 'validation-failed', '...');
}

Les espaces, majuscules, underscores et caractères spéciaux sont tous rejetés.

Recherche inverse (Tag → Entités)

GET /tags/{tag}/entities retourne 404 si le tag n'existe pas dans la base de données, et un tableau vide s'il existe mais n'est pas utilisé :

php
if ($this->repo->findByName($tag) === null) {
    return $this->problem(404, 'not-found', 'Tag not found.');
}
return $this->json(['tag' => $tag, 'entity_ids' => $this->repo->entitiesForTag($tag)]);

SQL pour la recherche inverse :

sql
SELECT entity_id FROM entity_tags WHERE tag_id = :tid ORDER BY entity_id ASC

Idempotence d'attacher / détacher

Attacher le même tag à la même entité deux fois retourne 200 (pas 201) avec "attached": false :

php
$attached = $this->repo->attach($entityId, (int) $tag['id']);
return $this->json([...], $attached ? 201 : 200);

Détacher un tag qui n'est pas attaché retourne 404.

Validation de l'ID d'entité

Les IDs d'entité sont validés avec ctype_digit() pour éviter ReDoS et assurer des entiers non négatifs :

php
if (!ctype_digit($raw) || strlen($raw) > 18) {
    return null;
}
$id = (int) $raw;
return $id > 0 ? $id : null;

Routes

POST   /tags                           Créer ou récupérer un tag
GET    /tags                           Lister tous les tags
GET    /tags/{tag}/entities            Recherche inverse : entités avec ce tag
POST   /entities/{entityId}/tags       Attacher un tag à une entité
GET    /entities/{entityId}/tags       Lister les tags d'une entité
DELETE /entities/{entityId}/tags/{tag} Détacher un tag d'une entité

Voir aussi

  • Source FT209 : ../NENE2-FT/taglog/
  • Connexe : docs/howto/note-taking.md (FT202, recherche de notes basée sur les tags)

Publié sous licence MIT.