Documentation

Installation et référence CLI

Tout ce qu'il faut pour installer RenderLoop, lancer la boucle et brancher la sortie JSON dans un script ou un agent. Version de référence : v0.4.0.

Démarrage rapide

RenderLoop s'installe comme n'importe quelle bibliothèque Composer. Le binaire est exposé dans vendor/bin/.

composer require linquant/render-loop

La boucle complète tient en trois commandes :

# 1. capturer l'état AVANT la modification
vendor/bin/render-loop -a capture -u "http://localhost:8000/ma-page.php" -t before

# 2. … modifier le code, puis s'assurer que le serveur sert le nouveau fichier …

# 3. re-vérifier : le diff doit contenir UNIQUEMENT le changement voulu
vendor/bin/render-loop -a recheck -u "http://localhost:8000/ma-page.php" -f json
Pas envie d'installer ? La démo en ligne exécute le vrai moteur sur deux extraits HTML, sans rien stocker.

Sans snapshot « before », il n'y a rien à differ. La référence utilisée par recheck est la dernière capture de l'URL qui ne vient pas elle-même d'un recheck : plusieurs rechecks successifs ne font donc pas dériver la base de comparaison.

Piège opcache. Juste après une édition, PHP peut servir un fichier périmé pendant une seconde ou deux. Un recheck trop rapide peut afficher un diff vide alors que le code a changé. La démo du dépôt lance son serveur avec -d opcache.enable=0.

Prérequis

  • PHP 8.2 ou supérieur.
  • Extensions ext-dom, ext-libxml, ext-json et ext-curl.
  • ext-curl est exigée par Composer à l'installation. À l'exécution, seule la capture par requête HTTP l'utilise ; la stratégie « push » n'appelle pas curl.
  • Dépendances installées automatiquement : symfony/console, symfony/css-selector et sebastian/diff.

Pour contribuer au projet : composer test (90 tests PHPUnit) et composer phpstan (level 10) doivent être verts. La CI couvre PHP 8.2, 8.3, 8.4 et 8.5.

Les 11 actions CLI

Le binaire est vendor/bin/render-loop (ou bin/render-loop depuis les sources). Dans les exemples, render-loop désigne ce binaire lorsque vendor/bin est dans le PATH.

Les actions recheck, compare, list, calibrate, mask-add, capabilities et advise acceptent -f json ; seuls recheck et compare renvoient le résumé de diff. capture, store, golden-record et golden-verify écrivent toujours du texte, même avec le flag.

ActionCe qu'elle faitOptions clésCode retour
capture Récupère la page par curl et l'enregistre comme snapshot. -u, -t, --method, --header, --data, --ajax 0, ou erreur
store Enregistre le HTML lu sur l'entrée standard (page JS/SPA rendue ailleurs). -u, -t 0, erreur si l'entrée est vide
recheck Re-capture l'URL et diffe contre la dernière baseline. -u, --exit-code, --expect-selector 0 même avec un diff ; non-zéro avec --exit-code ou assertion échouée
compare Diff entre deux snapshots précis. --from, --to, --expect-selector Idem recheck
list Liste les snapshots, éventuellement filtrés par URL. -u (optionnel) 0
calibrate Capture N fois sans changement : ce qui diffère est du bruit, dont elle déduit les sélecteurs. -u, --samples (min. 2), --apply 0
mask-add Ajoute des sélecteurs de bruit à la config, sans calibration. --selector (répétable) 0 ; 1 si aucun sélecteur
golden-record Épingle la sortie rendue actuelle des cas d'une suite. --suite, --golden-dir 0, ou erreur
golden-verify Rejoue chaque cas et compare au golden. --suite, --golden-dir Non-zéro si un cas a changé ou manque
capabilities Manifeste machine de toutes les actions : options requises, forme de sortie, codes retour. -f json 0
advise Mode opératoire et pièges destinés à un agent IA. -f json 0

Options transverses

OptionEffet
-u, --urlURL de la page à capturer ou re-vérifier.
-t, --tagÉtiquette du snapshot, par exemple before.
-d, --dirDossier des snapshots. Défaut : .render-loop/snapshots.
-c, --configConfig de masquage. À pointer vers un fichier de ton projet, pas celui de vendor/.
-f, --formattable (défaut) ou json.
--allow-hostHôte supplémentaire autorisé pour la capture, répétable. localhost et 127.0.0.1 le sont par défaut.
--cookie-nameNom du cookie de session, PHPSESSID par défaut.
--methodForce la méthode HTTP de capture.
--headerEn-tête HTTP supplémentaire, répétable.
--dataCorps de requête ; implique POST.
--ajaxAjoute X-Requested-With: XMLHttpRequest pour capturer un endpoint de fragment.
--max-hunksPlafond de hunks dans le diff. Défaut CLI : 40.
--exit-codeSort en non-zéro quand le diff est non vide, comme git diff --exit-code.
--expect-selectorSélecteur que l'édition a le droit de toucher, répétable. Toute fuite fait échouer la commande.
--from, --toSnapshots de départ et d'arrivée pour compare.
--samplesNombre de captures de calibration, minimum 2.
--applyÉcrit les sélecteurs découverts dans le fichier de config.
--suite, --golden-dirNom de la suite golden et dossier du manifeste. Défaut : golden.

Sortie JSON

recheck et compare renvoient un objet avec deux clés : summary et diff. Le résumé est fait pour être lu en premier.

{
  "summary": {
    "lines_added": 1,
    "lines_removed": 1,
    "hunks": 1,
    "truncated": false,
    "tags_touched": ["p"],
    "changes": [
      {
        "kind": "text",
        "selector": "article.product-detail > p.price",
        "context": "Aeron Chair",
        "before": "$1295.00",
        "after": "$995.00"
      }
    ]
  },
  "diff": "@@ -41,7 +41,7 @@ ..."
}
  • kind vaut text, added ou removed.
  • selector est un chemin CSS au mieux : #id, puis tag.classe, puis tag:nth-of-type(n).
  • context est l'ancre lisible la plus proche (titre, label, en-tête de tableau), null si rien ne convient.
  • truncated passe à true quand le nombre de hunks dépasse --max-hunks.
  • changes est un enrichissement : sur une insertion structurelle qui décale les chemins, il peut retomber à vide. Le diff unifié reste la référence.

Assertion de rayon d'impact

Avec --expect-selector, un bloc assertion s'ajoute. RenderLoop masque temporairement les sélecteurs attendus, re-normalise et re-diffe : ce qui reste est la fuite.

{
  "assertion": {
    "expected_selectors": [".price"],
    "satisfied": false,
    "inside_scope_changed": true,
    "reason": "1 change(s) outside the expected scope",
    "out_of_scope": {
      "lines_added": 1,
      "lines_removed": 1,
      "hunks": 1,
      "diff": "..."
    }
  }
}

satisfied: true signifie qu'aucun changement visible n'a fui hors du sélecteur — pas que l'édition est correcte. Un sélecteur faux ou absent donne satisfied: false avec la raison « matched no element », jamais un faux vert.

Masquage et calibration

Avant comparaison, l'HTML est normalisé : script, style et commentaires retirés, puis les sélecteurs de bruit de la config suppriment les nœuds indésirables. Le masquage est toujours par sélecteur, jamais par expression régulière sur le contenu : un prix qui tombe à 0.00 doit rester visible dans le diff.

# découvrir le bruit : capturer plusieurs fois sans rien changer
render-loop -a calibrate -u "http://localhost:8000/ma-page" --samples=4 --apply -c normalizer.json

# déclarer un bruit connu sans calibration
render-loop -a mask-add --selector='.chat-widget' --selector='.cookie-banner' -c normalizer.json

La config par défaut masque déjà DebugBar, les jetons CSRF courants et quelques métadonnées. calibrate --apply et mask-add sont idempotents : pas de doublon dans le fichier.

Golden master

Le golden master est un mode d'approval testing pour les pages legacy sans tests. Il fige la sortie rendue de cas représentatifs, puis prouve qu'un refactor ne l'a pas fait dériver.

# avant le refactor
render-loop -a golden-record --suite=product

# après le refactor
render-loop -a golden-verify --suite=product

Le manifeste golden/<suite>.json déclare les cas (id, url) et le masque. La sortie normalisée est écrite dans golden/<suite>/<cas>.html : commit-la, le diff se relit dans git comme un fichier .approved. golden-record auto-calibre le bruit par cas.

Le golden ne couvre que les entrées déclarées et suppose un rendu déterministe. Capture contre des données figées pour que le diff reflète le code et non la donnée.

Utilisation programmatique

Le moteur est indépendant de tout framework. Il s'instancie en quelques lignes et s'intègre dans un contrôleur, une commande ou un test.

use RenderLoop\Model\SnapshotRepository;
use RenderLoop\Service\{HtmlCompareService, HtmlNormalizer, HtmlDiffer,
    NormalizerConfig, CurlCaptureStrategy, PushedHtmlStrategy, UrlGuard};

$service = new HtmlCompareService(
    new SnapshotRepository('/var/snapshots'),
    new HtmlNormalizer(NormalizerConfig::default()),
    new HtmlDiffer(),
);

// page rendue côté serveur, bornée aux hôtes autorisés (anti-SSRF)
$fetch = new CurlCaptureStrategy(new UrlGuard(['localhost']));
$service->capture('http://localhost/page.php', $fetch, 'before');

// … modifier le code …

$result = $service->recheck('http://localhost/page.php', $fetch);
if (!$result->diff->isIdentical()) {
    echo $result->diff->unifiedDiff;   // ce qui a changé
    print_r($result->diff->summary()); // résumé compact, adapté à une IA
}

// page JS/SPA : pousser l'HTML déjà rendu par un navigateur headless
$service->capture('http://localhost/app', new PushedHtmlStrategy($renderedHtml), 'before');

Pages authentifiées et sécurité

La capture attache le cookie de session de l'utilisateur. Une URL libre permettrait donc d'exfiltrer ce que voit cet utilisateur vers un hôte tiers : UrlGuard n'accepte que http ou https, et uniquement les hôtes d'une liste blanche.

  • La valeur du cookie passe par RENDERLOOP_COOKIE, jamais en argument (visible dans ps).
  • Les redirections ne sont jamais suivies : une redirection cross-origin contournerait la garde.
  • Un statut HTTP supérieur ou égal à 400 lève une exception.
  • Le timeout de capture est de 15 secondes.
RENDERLOOP_COOKIE=<valeur> render-loop -a capture -u "http://localhost:8000/compte.php"

Limites assumées

  • Rendu uniquement : une écriture en base, un export, un PDF ou une réponse d'API reste invisible.
  • Golden master : seulement les entrées déclarées, et un rendu déterministe. Pour un calcul critique, extraire la fonction et la tester unitairement.
  • changes au mieux : sur du markup sans id ni classe, les sélecteurs sont positionnels et une insertion structurelle peut vider le tableau. Le diff unifié reste fiable.
  • Complément, pas substitut : RenderLoop complète les tests, il ne les remplace pas.