Streamer en PHP : trois niveaux, trois outils
PHP offre trois niveaux de contrôle sur le flux de données : l'output buffer, les générateurs, et les streams natifs. Bien les distinguer évite 90 % des problèmes de mémoire.
En PHP, "streamer" peut vouloir dire trois choses très différentes selon le niveau où tu te places :
| Niveau | Mécanisme | Question résolue |
|---|---|---|
| Sortie HTTP | Output buffer (ob_*) |
Quand PHP envoie-t-il les octets au client ? |
| Données applicatives | Générateurs (yield) |
Comment produire une séquence sans tout stocker ? |
| I/O fichiers & réseau | Streams PHP (fopen, wrappers) |
Comment lire/écrire sans charger en mémoire ? |
Ces trois niveaux s'emboîtent : un générateur peut lire un stream PHP et pousser les résultats dans l'output buffer. Bien les distinguer, c'est savoir exactement où agir quand la mémoire explose ou que la latence s'emballe.
Ensemble de techniques permettant de traiter et transmettre des données fragment par fragment, sans jamais avoir l'intégralité en mémoire. Applicable à la génération de réponses HTTP, au traitement de fichiers volumineux, et à la production de séquences de données.
DB, fichier, API
yield
php://output
ob_flush + flush
L'output buffer — contrôle total sur la sortie
PHP accumule ta sortie avant de l'envoyer. ob_start(), ob_flush(), flush() : maîtrise ce mécanisme pour streamer ou capturer le rendu.
Quand tu appelles echo, les données ne partent pas immédiatement vers le client. PHP les accumule dans un output buffer — une zone mémoire intermédiaire. Ce comportement est configurable et exploitable.
Tampon interne de PHP qui accumule toute sortie générée par
echo, print, var_dump, etc. Le contenu n'est envoyé au client que quand le buffer est vidé (flushed) ou que le script se termine.
Les fonctions fondamentales :
// Démarrer un buffer (empêche tout envoi immédiat) ob_start(); echo "Ce texte est mis en tampon, pas encore envoyé"; // Récupérer le contenu du buffer sans l'envoyer (utile pour les templates) $html = ob_get_clean(); // Vider ET envoyer le buffer actuel vers le niveau supérieur ob_flush(); // Niveau de bufferisation actuel (0 = aucun buffer actif) ob_get_level();
Les buffers sont empilables — chaque ob_start() crée un nouveau niveau :
ob_start(); // niveau 1 ob_start(); // niveau 2 echo "hello"; ob_get_level(); // → 2 ob_end_flush(); // vide niveau 2 → niveau 1 ob_end_flush(); // vide niveau 1 → sortie réseau
header() après avoir envoyé du contenu provoque "headers already sent". Si ob_start() est actif, les headers peuvent encore être modifiés — le buffer n'a pas encore quitté PHP.
Pour le streaming en temps réel, il faut vider le buffer à chaque chunk :
// Option 1 : flush implicite après chaque echo ob_implicit_flush(true); // Option 2 : vider manuellement à chaque étape while ($chunk = $source->next()) { echo $chunk; if (ob_get_level() > 0) { ob_flush(); } flush(); // flush système (Nginx/FrankenPHP) }
ob_flush() vide le buffer PHP vers le buffer système. flush() vide le buffer système vers le réseau. Pour streamer vers le client, tu as besoin des deux.
Cas d'usage classique — capturer le rendu d'un template PHP :
function render(string $template, array $vars): string { ob_start(); extract($vars); include $template; return ob_get_clean(); // récupère et supprime le buffer }
Générateurs — données à la demande avec yield
yield transforme une fonction en source de données paresseuse. Produis des millions d'éléments sans jamais les avoir tous en mémoire simultanément.
Imagine une liste de 10 millions de lignes à traiter. Approche naïve : charger tout en mémoire via un array, puis itérer. Avec un générateur PHP, tu n'as jamais plus d'un élément en mémoire à la fois.
Fonction contenant
yield qui retourne un objet Generator. À chaque itération, la fonction reprend là où elle s'était arrêtée et produit la valeur suivante — sans recalculer les précédentes ni les stocker.
Comparaison directe :
// Version tableau — tout en mémoire d'un coup function rangeArray(int $n): array { $result = []; for ($i = 0; $i < $n; $i++) { $result[] = $i * $i; } return $result; // N entiers alloués d'un seul coup } // Version générateur — un élément à la fois function rangeGen(int $n): Generator { for ($i = 0; $i < $n; $i++) { yield $i => $i * $i; // produit clé => valeur } } // mémoire constante quelle que soit la valeur de N
Les générateurs implémentent Iterator — foreach fonctionne nativement :
foreach (rangeGen(1_000_000) as $i => $v) { process($v); // traite un élément puis l'oublie }
Un générateur peut aussi recevoir des valeurs via send() :
function accumulator(): Generator { $total = 0; while (true) { $value = yield $total; // expose $total, reçoit $value if ($value === null) break; $total += $value; } } $acc = accumulator(); $acc->current(); // démarre → 0 $acc->send(10); // → 10 $acc->send(5); // → 15
Exemple concret — lire un CSV volumineux ligne par ligne :
function readCsv(string $path): Generator { $fh = fopen($path, 'r'); try { while (($row = fgetcsv($fh)) !== false) { yield $row; } } finally { fclose($fh); // fermé même si le consommateur arrête tôt } } foreach (readCsv('/data/orders.csv') as $row) { Order::fromRow($row)->save(); }
Traversable). Permet de composer des pipelines de générateurs sans imbriquer les foreach.
Compare la consommation mémoire d'un tableau vs un générateur pour produire N carrés. Déplace le curseur et clique sur Simuler.
Streams PHP — lecture, écriture et transformation
fopen() n'est que la surface : PHP expose un système de streams complet avec wrappers, contextes et filtres en chaîne.
PHP dispose d'un système de streams unifié qui abstrait tous les types d'I/O — fichiers locaux, HTTP, FTP, données en mémoire — derrière la même interface fopen / fread / fwrite.
Abstraction d'une ressource séquentielle accessible en lecture et/ou écriture. Tout stream est identifié par un wrapper (schéma d'URL) :
file://, http://, php://, compress.zlib://, etc. La même API fread/fwrite fonctionne pour tous.
Les wrappers les plus utiles :
| Wrapper | Usage |
|---|---|
php://input |
Corps brut de la requête HTTP (POST, PUT) en lecture seule |
php://output |
Sortie standard — écrire dedans équivaut à echo |
php://memory |
Buffer en RAM — construire un fichier temporaire sans disque |
php://temp |
RAM jusqu'à 2 Mo, bascule automatiquement sur disque au-delà |
compress.zlib:// |
Compression/décompression gzip à la volée |
Copier un stream vers un autre sans passer par la mémoire :
// Envoyer un fichier au client — mémoire constante (~8 Ko) $src = fopen('/var/storage/report.pdf', 'rb'); $dest = fopen('php://output', 'wb'); stream_copy_to_stream($src, $dest); fclose($src); fclose($dest);
Les filtres de stream permettent de transformer les données à la volée, sans buffer intermédiaire :
// Lire un .gz et le décompresser à la volée $fh = fopen('compress.zlib://archive.gz', 'r'); while (!feof($fh)) { echo fread($fh, 8192); } fclose($fh); // Ajouter un filtre sur un stream ouvert $fh = fopen('data.txt', 'r'); stream_filter_append($fh, 'string.toupper'); // tout ce que retourne fread() sera automatiquement en majuscules
Créer un wrapper personnalisé pour abstraire n'importe quelle source :
class S3StreamWrapper { private $handle; public function stream_open(string $path, string $mode): bool { // parse s3://bucket/key, ouvre la connexion S3 $this->handle = S3Client::create()->getObject($path); return true; } public function stream_read(int $count): string { return $this->handle->read($count); } } stream_wrapper_register('s3', S3StreamWrapper::class); // Désormais fopen/fread/stream_copy_to_stream fonctionnent avec s3:// $fh = fopen('s3://mon-bucket/rapport.pdf', 'r');
stream_get_contents() charge tout en mémoire (équivalent de file_get_contents). Pour de gros fichiers, itère avec fread($fh, 8192) pour maintenir une empreinte mémoire constante.
stream_context_create() permet de passer des options au wrapper (timeout, headers HTTP, certificats SSL). Indispensable pour personnaliser fopen('https://...') sans passer par cURL.
Synthèse — composer les trois niveaux
Output buffer, générateurs, streams : chacun a son rôle. Ensemble, ils forment des pipelines zéro-copie de la source à l'octet réseau.
Trois outils, trois niveaux d'abstraction. Le choix dépend de ce que tu manipules.
| Situation | Outil | Pourquoi |
|---|---|---|
| Contrôler quand PHP envoie la réponse | ob_flush + flush |
Agit sur le tampon de sortie HTTP |
| Produire une séquence volumineuse (DB, calcul) | Générateur yield |
Mémoire O(1), compatible foreach |
| Lire / copier un fichier volumineux | stream_copy_to_stream ou fread en boucle |
Mémoire constante, pas de chargement total |
| Transformer des données à la volée (encode, compress) | Filtres de stream (stream_filter_append) |
Transformation sans buffer intermédiaire |
| Abstraire une source externe (S3, API, base NoSQL) | Stream wrapper personnalisé | Intégration transparente avec l'écosystème fopen |
Un générateur lit un stream PHP ligne par ligne, transforme via un filtre, et pousse dans l'output buffer avec
flush() à chaque chunk. Résultat : empreinte mémoire constante de la source à l'octet réseau.
// Exporter 1 M de lignes CSV en streaming HTTP function streamCsvExport(Generator $rows): void { header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="export.csv"'); header('X-Accel-Buffering: no'); $out = fopen('php://output', 'w'); foreach ($rows as $row) { fputcsv($out, $row); flush(); } fclose($out); }
Points clés à retenir :
- Output buffer :
ob_get_level()pour savoir si un buffer est actif.ob_implicit_flush(true)pour le streaming automatique. Deux flush sont nécessaires :ob_flush()puisflush(). - Générateurs : toujours fermer les ressources dans un bloc
finally— le consommateur peut arrêter l'itération avant la fin. - Streams : préfère
stream_copy_to_streamàfreaden boucle quand tu n'as pas besoin de transformer — c'est implémenté en C et plus rapide. - Composition : générateurs + streams + output buffer ne s'excluent pas — combine-les pour des pipelines efficaces.