# CLAUDE.md — PHP Async : Event Loop et Fibers > Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour implémenter de la concurrence coopérative en PHP avec ReactPHP, Fibers PHP 8.1, et Amp. --- ## Section 1 : Le modèle bloquant de PHP-FPM PHP-FPM est mono-thread par worker : les opérations s'enchaînent séquentiellement. Un handler qui fait trois appels HTTP externes les exécute l'un après l'autre. ```text t=0ms → appel API users (200ms d'attente réseau) t=200ms → appel API orders (200ms d'attente réseau) t=400ms → appel API products (200ms d'attente réseau) t=600ms → réponse renvoyée au client ``` Sur 600 ms, le thread attend 580 ms. Le CPU est libre, mais le thread est bloqué sur les I/O. L'async permet de recouvrir ce temps d'attente. **Contraintes à garder en tête :** - PHP-FPM : workers éphémères — pas de processus persistant, event loop inutile - PHP CLI : processus persistant — event loop viable - L'async ne résout **que** le problème I/O-bound, pas CPU-bound --- ## Section 2 : Event loop ReactPHP ReactPHP implémente une event loop en PHP. Plutôt que de bloquer sur une I/O, on enregistre un callback déclenché quand la réponse arrive. ### Timer simple ```php $loop = React\EventLoop\Factory::create(); $loop->addTimer(1, function () { echo "Tâche A\n"; }); $loop->addTimer(2, function () { echo "Tâche B\n"; }); $loop->run(); ``` ### Trois requêtes HTTP en parallèle ```php $loop = React\EventLoop\Factory::create(); $browser = new React\Http\Browser($loop); $promises = [ $browser->get('https://api.example.com/users'), $browser->get('https://api.example.com/orders'), $browser->get('https://api.example.com/products'), ]; React\Promise\all($promises)->then(function (array $responses) { foreach ($responses as $response) { echo $response->getBody() . "\n"; } }); $loop->run(); ``` Résultat : ~200 ms au lieu de 600 ms. Pas de threads supplémentaires — un seul thread qui recouvre le temps d'attente réseau. --- ## Section 3 : Fibers PHP 8.1 Les Fibers sont un mécanisme natif de bas niveau pour le multitasking coopératif. Une Fiber peut suspendre son exécution et rendre la main à l'appelant. ```php $fiber = new Fiber(function (): void { $value = Fiber::suspend('première suspension'); echo "Reprise avec : " . $value . "\n"; }); $value = $fiber->start(); // démarre la Fiber, reçoit la valeur suspendue echo "Suspendu avec : " . $value . "\n"; $fiber->resume('bonjour'); // reprend la Fiber avec une valeur ``` Sortie : ```text Suspendu avec : première suspension Reprise avec : bonjour ``` **Principe clé :** une Fiber ne tourne pas en parallèle du code principal — elle lui cède le contrôle via `Fiber::suspend()`. La suspension est coopérative, pas préemptive. --- ## Section 4 : Amphp v3 — async lisible comme du synchrone Les Fibers seules ne construisent pas un système async. Il faut un scheduler (liste des Fibers actives, reprise des Fibers prêtes) et une event loop pour les I/O. Amphp v3 fournit tout ça. ```php use Amp\Http\Client\HttpClientBuilder; $client = HttpClientBuilder::buildDefault(); // Ces trois appels sont lancés en concurrence, // mais le code se lit comme s'il était séquentiel $responses = Amp\Future\awaitAll([ async(fn() => $client->request(new Request('https://api.example.com/users'))), async(fn() => $client->request(new Request('https://api.example.com/orders'))), async(fn() => $client->request(new Request('https://api.example.com/products'))), ]); ``` Avantage Amp vs ReactPHP : pas de chaîne de callbacks, pas de gestion manuelle des promises. Le scheduler gère la suspension/reprise des Fibers en coulisses. --- ## Section 5 : Limitations — ce que l'async ne résout pas ### CPU-bound : async inutile ```text Appel HTTP 200ms = 199ms attente CPU libre → async aide Hash bcrypt 200ms = 200ms CPU à pleine charge → async n'aide pas ``` ### PDO est bloquant PDO bloque le thread jusqu'à la réponse SQL. Utiliser PDO dans une Fiber annule tout le bénéfice. ```php // ❌ MAUVAIS : PDO bloque la Fiber et toute l'event loop async(function () { $pdo = new PDO('pgsql:host=localhost;dbname=mydb', 'user', 'pass'); $stmt = $pdo->query('SELECT * FROM orders'); return $stmt->fetchAll(); }); // ✅ BON : driver async natif use Amp\Postgres; async(function () { $conn = yield Postgres\connect('host=localhost user=user dbname=mydb'); return yield $conn->query('SELECT * FROM orders'); }); ``` Drivers async disponibles : `amphp/mysql`, `amphp/postgres`. ### Extensions PHP synchrones Si une lib tierce appelle curl bloquant ou un SDK réseau en interne, toute la boucle se fige. L'event loop ne peut rien contre du code bloquant qu'elle ne contrôle pas. --- ## Section 6 : Vrai parallélisme quand l'event loop ne suffit pas ### pcntl_fork (PHP CLI uniquement) ```php $pid = pcntl_fork(); if ($pid === 0) { // Process fils processHeavyTask(); exit(0); } else { // Process parent continue doOtherWork(); pcntl_waitpid($pid, $status); // attend la fin du fils } ``` Attention : les connexions DB et les fichiers sont partagés entre parent et fils — fermer et rouvrir les connexions dans le fils. ### Workers externes (production) La solution la plus pragmatique : déléguer le travail lourd à une queue (Redis, RabbitMQ) consommée par des workers séparés. Symfony Messenger gère ça proprement. ```text Web request → enqueue job → réponse rapide au client Worker 1 → consume job → traitement lourd Worker 2 → consume job → traitement lourd ``` ### PHP-FPM scaling horizontal Pour la majorité des APIs web : plusieurs workers FPM traitent plusieurs requêtes en parallèle — pas dans le même thread, mais dans des processes distincts. C'est le modèle par défaut en prod. --- ## Section 7 : Quand utiliser l'async PHP | Cas | Recommandation | |-----|----------------| | Script CLI longue durée, I/O réseau | ReactPHP ou Amp | | API web classique, quelques requêtes SQL | PHP-FPM + workers + cache Redis | | Traitement lourd CPU | pcntl_fork ou workers queue | | Multiples appels HTTP parallèles en CLI | ReactPHP `Promise\all()` ou Amp `awaitAll()` | | WebSocket serveur persistant | ReactPHP ou Amp | **Règle de décision :** identifier si le goulot est I/O-bound (temps d'attente réseau/disque) ou CPU-bound. L'async n'aide que dans le premier cas. Pour un goulot CPU, fork ou workers. Pour une API web standard, PHP-FPM avec plusieurs workers résout le problème sans ajouter la complexité d'une event loop.