← Tous les cours / PHP Streaming — Flux de données en PHP Cours
00 — VUE D'ENSEMBLE

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 :

NiveauMécanismeQuestion 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.

Streaming en PHP
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.
Source
DB, fichier, API
Générateur
yield
Stream
php://output
Output buffer
ob_flush + flush
Client
💡 Ce cours vs "Streaming HTTP" : le cours Streaming HTTP couvre le protocole (chunked encoding, SSE, BinaryFileResponse). Ce cours-ci couvre la mécanique PHP sous-jacente — ce qui se passe avant que les octets quittent PHP.
01 — OUTPUT BUFFERING

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.

Output Buffer
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
⚠️ Piège classique : appeler 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)
}
💡 Deux flush différents : 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
}
02 — GÉNÉRATEURS

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.

Générateur PHP
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 Iteratorforeach 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();
}
🔑 yield from : délègue à un autre itérable (tableau, générateur, objet 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.

03 — STREAMS PHP

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.

Stream PHP
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 :

WrapperUsage
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 vs fread : 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.
⚠️ Contextes de stream : stream_context_create() permet de passer des options au wrapper (timeout, headers HTTP, certificats SSL). Indispensable pour personnaliser fopen('https://...') sans passer par cURL.
04 — SYNTHÈSE

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
Pipeline complet zéro-copie
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 :