Archi perf : index slug→uuid pour éviter le scan complet à chaque vue article #25

Closed
opened 2026-05-13 22:09:40 +00:00 by cedricAbonnel · 0 comments
Owner

Problème

ArticleManager::getBySlug() appelle getAll() et itère sur tous les articles pour trouver celui dont le slug correspond. Chaque visite d'un article déclenche donc un scan complet du répertoire data/, indépendamment du nombre d'articles.

// src/ArticleManager.php — getBySlug(), ligne ~47
public function getBySlug(string $slug): ?array
{
    foreach ($this->getAll() as $article) {   // scan complet
        if (($article['slug'] ?? '') === $slug) {
            return $article;
        }
    }
    return null;
}

C'est le chemin le plus emprunté du site (chaque lecture d'article), et il est O(n) en nombre d'articles.

Solution proposée

Fichier d'index data/_index/slugs.json

Maintenir un fichier JSON { "mon-slug": "uuid-xxx", ... } mis à jour à chaque create(), update() et delete().

public function getBySlug(string $slug): ?array
{
    $index = $this->loadSlugIndex();
    $uuid  = $index[$slug] ?? null;
    if ($uuid === null) {
        return null;
    }
    return $this->getByUuid($uuid);  // lecture directe d'un seul répertoire
}

getByUuid() lit déjà un seul répertoire en O(1) — c'est le comportement cible.

Gestion de la cohérence

  • create() : ajoute slug → uuid dans l'index
  • update() : supprime l'ancien slug, ajoute le nouveau
  • delete() : supprime l'entrée
  • Si l'index est absent ou corrompu : fallback sur getAll() + reconstruction de l'index

Impact

Opération Avant Après
Vue article (getBySlug) Scan N répertoires + N meta.json Lecture d'un fichier JSON + 1 meta.json + 1 index.md

Critères d'acceptation

  • getBySlug() utilise l'index et ne scanne plus data/
  • L'index est mis à jour à chaque create/update/delete
  • En cas d'index absent, fallback + reconstruction automatique
  • data/_index/ est exclu du rsync de contenu
  • Pas de régression sur les slugs (si un slug change, l'ancienne URL retourne bien 404)

Note

Doit être traité après ou conjointement avec le ticket #35 (ne pas charger le contenu dans getAll()) pour que le fallback reste bon marché.


Migré depuis varlog#36

## Problème `ArticleManager::getBySlug()` appelle `getAll()` et itère sur tous les articles pour trouver celui dont le slug correspond. Chaque visite d'un article déclenche donc un scan complet du répertoire `data/`, indépendamment du nombre d'articles. ```php // src/ArticleManager.php — getBySlug(), ligne ~47 public function getBySlug(string $slug): ?array { foreach ($this->getAll() as $article) { // scan complet if (($article['slug'] ?? '') === $slug) { return $article; } } return null; } ``` C'est le chemin le plus emprunté du site (chaque lecture d'article), et il est O(n) en nombre d'articles. ## Solution proposée ### Fichier d'index `data/_index/slugs.json` Maintenir un fichier JSON `{ "mon-slug": "uuid-xxx", ... }` mis à jour à chaque `create()`, `update()` et `delete()`. ```php public function getBySlug(string $slug): ?array { $index = $this->loadSlugIndex(); $uuid = $index[$slug] ?? null; if ($uuid === null) { return null; } return $this->getByUuid($uuid); // lecture directe d'un seul répertoire } ``` `getByUuid()` lit déjà un seul répertoire en O(1) — c'est le comportement cible. ### Gestion de la cohérence - `create()` : ajoute `slug → uuid` dans l'index - `update()` : supprime l'ancien slug, ajoute le nouveau - `delete()` : supprime l'entrée - Si l'index est absent ou corrompu : fallback sur `getAll()` + reconstruction de l'index ## Impact | Opération | Avant | Après | |---|---|---| | Vue article (`getBySlug`) | Scan N répertoires + N `meta.json` | Lecture d'un fichier JSON + 1 `meta.json` + 1 `index.md` | ## Critères d'acceptation - [ ] `getBySlug()` utilise l'index et ne scanne plus `data/` - [ ] L'index est mis à jour à chaque create/update/delete - [ ] En cas d'index absent, fallback + reconstruction automatique - [ ] `data/_index/` est exclu du rsync de contenu - [ ] Pas de régression sur les slugs (si un slug change, l'ancienne URL retourne bien 404) ## Note Doit être traité après ou conjointement avec le ticket #35 (ne pas charger le contenu dans `getAll()`) pour que le fallback reste bon marché. --- *Migré depuis [varlog#36](https://git.abonnel.fr/cedricAbonnel/varlog/issues/36)*
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: cedricAbonnel/folio#25