← Contextes /
php-async-fibers.md 199 lignes · 6.6 KB
Personnaliser Télécharger
# 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.