Aller au contenu

Architecture

Cette page explique les choix structurants de Kirexo — pas comment faire, mais pourquoi. Pour l'inventaire complet des règles et des patterns attendus, voir CLAUDE.md à la racine. Pour le cadrage fonctionnel et la liste des services backing, voir projet.md.

Pourquoi dunglas/symfony-docker et FrankenPHP

Le bootstrap du projet se base sur dunglas/symfony-docker, qui s'appuie sur FrankenPHP. Cette brique nous donne HTTP/2, HTTP/3 et surtout le worker mode PHP natif — un interpréteur qui reste chargé entre les requêtes, sans FPM ni overhead de bootstrap Symfony à chaque appel. En dev, on y gagne une boucle de feedback rapide ; en prod, on y gagne une latence très basse sans jongler avec php-fpm, une reverse-proxy supplémentaire et un Caddy/Nginx séparé. Les prérequis locaux restent minimaux : git, docker, docker-compose, castor, jq, glab. Rien d'autre, volontairement — aucun PHP ou Node installé côté hôte, tout vit dans les conteneurs.

Le worker mode est activé de bout en bout : Symfony 7.4 et plus le supporte nativement via public/index.php, sans qu'il faille installer un package runtime/* ni surcharger APP_RUNTIME. La seule configuration vit dans le Caddyfile de FrankenPHP, qui pointe le worker sur l'entrée HTTP standard du projet. Conséquence pratique : pas de double chemin de boot (FPM d'un côté, worker de l'autre) à maintenir, et pas de dépendance tierce dont le support Symfony serait à surveiller.

Pourquoi CQRS via Symfony Messenger

Le projet sépare strictement les intentions d'écriture (« publier cet article ») des intentions de lecture (« liste des articles publiés »). Concrètement, deux bus Messenger distincts cohabitent : un command.bus qui achemine les commandes vers RabbitMQ pour un traitement asynchrone, et un query.bus synchrone qui reste en process. Les controllers dispatchent des messages, ils ne calculent rien eux-mêmes — la logique métier vit dans des Handler dédiés, testables isolément sans HTTP.

Ce découpage a deux bénéfices directs. D'abord, les actions métier coûteuses (publier un article qui déclenche N diffusions plugin) ne bloquent jamais la requête utilisateur : la commande est acceptée, puis traitée en tâche de fond. Ensuite, l'isolement par transport permet de faire tomber un plugin sans faire tomber les autres ni le cœur.

Pourquoi les plugins sont des Symfony Bundles isolés

Chaque destination de diffusion (Mastodon, RSS, web, API, et plus tard Telegram, LinkedIn, Discord…) est un Symfony Bundle autonome, implémentant une interface commune (DiffusionPluginInterface), auto-enregistré par tag de service, et relié au cœur uniquement par des événements asynchrones (ex. ArticlePublished). Chaque plugin consomme via son propre transport Messenger : un échec côté Mastodon ne fait pas tomber la diffusion RSS.

La contrainte forte est qu'un plugin doit pouvoir être supprimé sans toucher au reste. C'est cette contrainte qui force le couplage à passer exclusivement par l'interface et par les événements — pas par des appels directs, pas par une entité Doctrine partagée, pas par un service injecté depuis le cœur. Le prix à payer est une surface d'interface un peu plus large ; le bénéfice est qu'ajouter une nouvelle destination ne demande jamais de modifier le cœur.

Pourquoi les fuseaux horaires sont découplés entre shell et PHP

Le conteneur php tourne en dev avec TZ=Europe/Paris (défini dans compose.override.yaml) mais PHP reste indépendamment verrouillé sur UTC via date.timezone = UTC dans frankenphp/conf.d/10-app.ini. Ce n'est pas une incohérence — c'est volontaire, et hérité des conventions de dunglas/symfony-docker.

La logique : TZ ne s'applique qu'à des consommateurs périphériques — le shell, la commande date, les logs Docker, la statusline de l'IDE. Mettre l'horloge du shell sur le fuseau local évite le décalage gênant qui apparaissait quand on lisait docker compose logs ou la statusline Claude Code (« mais il est 20 h chez moi, pourquoi ça affiche 18 h ? »). C'est un confort de dev, rien de plus.

À l'inverse, PHP doit rester en UTC partout — en dev comme en prod. Le stockage des DateTimeImmutable en base, les comparaisons de dates dans les Handlers, l'horodatage des événements Messenger, les expirations de cache et de tokens : tout doit s'aligner sur un fuseau unique et indépendant de la localisation de la machine. La conversion vers le fuseau de l'utilisateur final n'a lieu qu'au moment du rendu (filtres Twig date, formatters côté front), pas une seconde plus tôt. C'est cette discipline qui rend les fenêtres de publication d'articles cohérentes peu importe d'où l'utilisateur écrit ou consulte, et qui évite la classe d'incidents où une migration vers un serveur dans un autre fuseau décale silencieusement la moitié des données.

La prod renforce cette discipline en allant un cran plus loin : compose.prod.yaml ne définit pas TZ, donc le shell du conteneur reste lui aussi en UTC. C'est cohérent avec un environnement où il n'y a pas d'humain qui regarde la statusline en temps réel, et où l'alignement total shell/PHP sur UTC simplifie la corrélation entre logs Docker, logs Graylog et entrées Sentry. La référence détaillée de la variable TZ vit dans la Référence des variables d'environnement.

Pourquoi Diátaxis pour la documentation

La documentation suit Diátaxis, qui impose quatre quadrants strictement séparés : Tutoriels (apprendre en faisant), Guides (résoudre un problème précis), Référence (consulter la vérité) et Explications (comprendre le pourquoi — cette page). Chaque document appartient à un seul quadrant. Cette contrainte évite les pages-fourre-tout qui essaient d'expliquer et d'enseigner et de lister exhaustivement, et qui finissent par mal faire les trois.

La doc est publiée via mkdocs (thème material, image custom définie dans mkdocs/Dockerfile) et servie en local par le service Docker docs démarré avec la stack (castor docker:up), accessible sur http://localhost:8000. L'image custom ajoute deux plugins : git-revision-date-localized (date de dernière modif de chaque page, lue depuis git) et social (génération automatique des images Open Graph de partage). Toute modification qui change ce que l'utilisateur perçoit depuis un navigateur, ou qui introduit un concept utilisateur (nouveau plugin, nouvelle commande Castor exposée, nouvelle variable d'environnement), doit s'accompagner d'une doc à jour — c'est une règle non négociable de CLAUDE.md.