Stream con PHP

Gli stream sono uno dei meccanismi fondamentali di PHP per la gestione dell'I/O. Introdotti formalmente con PHP 4.3, gli stream offrono un'interfaccia unificata per leggere e scrivere dati da sorgenti eterogenee: file locali, connessioni di rete, buffer in memoria, risorse compresse e molto altro. Comprendere a fondo il sistema degli stream significa padroneggiare uno strumento potente e flessibile, applicabile in contesti che vanno ben oltre la semplice lettura di file.

Cosa sono gli stream

Uno stream in PHP e' una risorsa che rappresenta un flusso di dati sequenziale. Ogni stream e' identificato da un URI nella forma wrapper://target, dove il wrapper definisce il protocollo o il tipo di sorgente, e il target specifica la risorsa concreta. Ad esempio, file:///var/www/html/index.php e' uno stream basato sul wrapper file, mentre https://example.com/data.json usa il wrapper https.

Il concetto centrale e' che, indipendentemente dalla sorgente, tutte le operazioni avvengono tramite la stessa interfaccia: funzioni come fread(), fwrite(), fgets() e feof() funzionano in modo identico su qualsiasi tipo di stream. Questa astrazione e' il valore principale del sistema.

Wrapper nativi di PHP

PHP include numerosi wrapper predefiniti, ciascuno progettato per un tipo specifico di risorsa.

Il wrapper file:// e' il piu' comune e viene usato implicitamente ogni volta che si apre un file senza specificare il protocollo. I due esempi seguenti sono equivalenti:

<?php

// Apertura implicita tramite percorso assoluto
$handle = fopen('/var/www/html/data.txt', 'r');

// Apertura esplicita con wrapper file://
$handle = fopen('file:///var/www/html/data.txt', 'r');

Il wrapper php:// espone risorse interne dell'interprete. Le piu' importanti sono:

  • php://stdin, php://stdout, php://stderr: standard I/O del processo.
  • php://input: corpo grezzo della richiesta HTTP, accessibile in sola lettura.
  • php://output: buffer di output corrente, accessibile in sola scrittura.
  • php://memory e php://temp: buffer temporanei in memoria (o su disco per temp quando la dimensione supera una soglia).
  • php://filter: permette di applicare filtri a un altro stream.
<?php

// Lettura del body di una richiesta POST in formato grezzo
$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);

// Scrittura in un buffer in memoria
$memStream = fopen('php://memory', 'r+');
fwrite($memStream, 'Dati temporanei');
rewind($memStream);
$content = stream_get_contents($memStream);
fclose($memStream);

echo $content; // Stampa: Dati temporanei

Il wrapper data:// consente di incorporare dati direttamente nell'URI, seguendo il formato RFC 2397:

<?php

// Lettura di dati inline codificati in base64
$stream = fopen('data://text/plain;base64,' . base64_encode('Ciao, stream!'), 'r');
echo stream_get_contents($stream); // Stampa: Ciao, stream!
fclose($stream);

I wrapper http:// e https:// permettono di aprire risorse remote tramite HTTP. Sono abilitati dalla direttiva allow_url_fopen nel file php.ini:

<?php

// Recupero del contenuto di una pagina remota
$content = file_get_contents('https://api.example.com/v1/users');
$users = json_decode($content, true);

Il wrapper compress.zlib:// e compress.bzip2:// gestiscono la lettura e scrittura di file compressi in modo trasparente:

<?php

// Scrittura in un file compresso con gzip
$gzStream = fopen('compress.zlib://output.gz', 'w');
fwrite($gzStream, 'Contenuto che verra compresso automaticamente');
fclose($gzStream);

// Lettura trasparente di un file bzip2
$bzStream = fopen('compress.bzip2://archive.bz2', 'r');
while (!feof($bzStream)) {
    echo fgets($bzStream);
}
fclose($bzStream);

Contesti di stream (stream context)

Un contesto di stream e' una struttura dati che permette di personalizzare il comportamento di uno stream o di un wrapper. Si crea con stream_context_create() e si passa come parametro opzionale alle funzioni di I/O.

Il caso d'uso piu' frequente e' la configurazione di richieste HTTP, ad esempio per impostare metodo, intestazioni e corpo:

<?php

// Configurazione di una richiesta HTTP POST con contesto
$requestBody = json_encode(['username' => 'mario', 'password' => 'segreto']);

$contextOptions = [
    'http' => [
        'method'  => 'POST',
        'header'  => implode("\r\n", [
            'Content-Type: application/json',
            'Accept: application/json',
            'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...',
        ]),
        'content' => $requestBody,
        'timeout' => 10,
        'ignore_errors' => true, // Legge il body anche in caso di errore HTTP
    ],
];

$context = stream_context_create($contextOptions);
$response = file_get_contents('https://api.example.com/auth/login', false, $context);

// Lettura degli header di risposta
$responseHeaders = $http_response_header; // Variabile magica popolata da file_get_contents

I contesti possono includere opzioni per piu' wrapper contemporaneamente. Ad esempio, si puo' configurare sia il comportamento SSL che quello HTTP:

<?php

// Configurazione avanzata con opzioni SSL e HTTP
$contextOptions = [
    'ssl' => [
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
        'allow_self_signed' => false,
    ],
    'http' => [
        'method'          => 'GET',
        'follow_location' => 1,
        'max_redirects'   => 5,
    ],
];

$context = stream_context_create($contextOptions);
$data = file_get_contents('https://secure.example.com/data', false, $context);

Filtri di stream

I filtri di stream permettono di trasformare i dati mentre scorrono attraverso uno stream. PHP include filtri nativi e consente di registrare filtri personalizzati.

I filtri si applicano con stream_filter_append() o stream_filter_prepend(), oppure direttamente tramite il wrapper php://filter:

<?php

// Lettura di un file con decodifica base64 applicata come filtro
$encodedFile = 'php://filter/read=convert.base64-decode/resource=encoded_data.b64';
$decodedContent = file_get_contents($encodedFile);

// Scrittura con filtro di codifica base64
$outputFile = 'php://filter/write=convert.base64-encode/resource=output_encoded.txt';
file_put_contents($outputFile, 'Testo da codificare');

I filtri possono essere concatenati separandoli con il carattere pipe |:

<?php

// Catena di filtri: prima decompressione zlib, poi decodifica base64
$filteredStream = 'php://filter/read=zlib.inflate|convert.base64-decode/resource=file.dat';
$result = file_get_contents($filteredStream);

I filtri nativi disponibili includono string.rot13, string.toupper, string.tolower, string.strip_tags, convert.base64-encode, convert.base64-decode, zlib.deflate, zlib.inflate, bzip2.compress e bzip2.decompress.

Filtri personalizzati

Per creare un filtro personalizzato si estende la classe php_user_filter e si implementa il metodo filter(). Il filtro va poi registrato con stream_filter_register():

<?php

// Filtro che converte tutto il testo in maiuscolo
class UpperCaseFilter extends php_user_filter
{
    public function filter($in, $out, &$consumed, bool $closing): int
    {
        // Itera su ogni bucket nel bucket brigade in ingresso
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = strtoupper($bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }

        return PSFS_PASS_ON;
    }
}

// Registrazione del filtro con un nome personalizzato
stream_filter_register('uppercase', UpperCaseFilter::class);

// Utilizzo del filtro su uno stream
$handle = fopen('php://memory', 'r+');
stream_filter_append($handle, 'uppercase');

fwrite($handle, 'questo testo diventa maiuscolo');
rewind($handle);
echo stream_get_contents($handle); // QUESTO TESTO DIVENTA MAIUSCOLO

fclose($handle);

Wrapper personalizzati

PHP consente di registrare wrapper personalizzati implementando una classe con metodi specifici e registrandola tramite stream_wrapper_register(). Questa funzionalita' e' utile per astrarre sorgenti di dati non standard, come database, cache distribuiti o sistemi di archiviazione cloud.

La classe del wrapper deve implementare i metodi corrispondenti alle operazioni di stream che si vogliono supportare. L'interfaccia non e' formale (non esiste una interface PHP da implementare), ma i nomi dei metodi sono predefiniti:

<?php

// Wrapper personalizzato che legge e scrive da un array in memoria
class ArrayStreamWrapper
{
    // Contiene i dati simulando un filesystem in memoria
    private static array $storage = [];

    private string $path;
    private int $position;

    public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
    {
        // Estrae il percorso rimuovendo il prefisso del wrapper
        $this->path = str_replace('array://', '', $path);
        $this->position = 0;

        if ($mode === 'w' || $mode === 'w+') {
            // Inizializza il buffer per la scrittura
            self::$storage[$this->path] = '';
        } elseif (!isset(self::$storage[$this->path])) {
            return false;
        }

        return true;
    }

    public function stream_read(int $count): string|false
    {
        $data = substr(self::$storage[$this->path], $this->position, $count);
        $this->position += strlen($data);
        return $data;
    }

    public function stream_write(string $data): int
    {
        $written = strlen($data);
        self::$storage[$this->path] = substr(self::$storage[$this->path], 0, $this->position);
        self::$storage[$this->path] .= $data;
        $this->position += $written;
        return $written;
    }

    public function stream_eof(): bool
    {
        return $this->position >= strlen(self::$storage[$this->path]);
    }

    public function stream_tell(): int
    {
        return $this->position;
    }

    public function stream_seek(int $offset, int $whence = SEEK_SET): bool
    {
        $length = strlen(self::$storage[$this->path]);

        // Calcola la nuova posizione in base alla modalita' di seek
        $newPosition = match ($whence) {
            SEEK_SET => $offset,
            SEEK_CUR => $this->position + $offset,
            SEEK_END => $length + $offset,
            default  => -1,
        };

        if ($newPosition < 0) {
            return false;
        }

        $this->position = $newPosition;
        return true;
    }

    public function stream_stat(): array
    {
        $size = isset(self::$storage[$this->path]) ? strlen(self::$storage[$this->path]) : 0;
        return ['size' => $size];
    }
}

// Registrazione del wrapper con il prefisso "array"
stream_wrapper_register('array', ArrayStreamWrapper::class);

// Utilizzo del wrapper personalizzato come un normale stream
$handle = fopen('array://buffer/test', 'w+');
fwrite($handle, 'Dati scritti nel wrapper personalizzato');
rewind($handle);
echo fread($handle, 1024); // Dati scritti nel wrapper personalizzato
fclose($handle);

Lettura e scrittura non bloccante

Per default, le operazioni di I/O su stream sono bloccanti: fread() aspetta che i dati siano disponibili prima di restituire il controllo. In applicazioni che gestiscono piu' connessioni simultaneamente questo comportamento e' inaccettabile. PHP offre due strumenti per la gestione non bloccante: stream_set_blocking() e stream_select().

Con stream_set_blocking($stream, false) lo stream passa in modalita' non bloccante: le operazioni di lettura e scrittura restituiscono immediatamente anche se non ci sono dati disponibili.

stream_select() e' l'equivalente PHP della chiamata di sistema select(): blocca il processo fino a quando almeno uno degli stream passati e' pronto per la lettura, la scrittura o ha generato un'eccezione:

<?php

// Gestione di piu' connessioni socket con stream_select
$server = stream_socket_server('tcp://0.0.0.0:8080', $errorCode, $errorMessage);

if (!$server) {
    throw new RuntimeException("Impossibile avviare il server: $errorMessage");
}

stream_set_blocking($server, false);

$clients = [];

while (true) {
    // Costruisce l'array degli stream da monitorare
    $readStreams = array_merge([$server], $clients);
    $writeStreams = null;
    $exceptStreams = null;

    // Attende fino a 5 secondi che uno stream sia pronto
    $changed = stream_select($readStreams, $writeStreams, $exceptStreams, 5);

    if ($changed === false) {
        break; // Errore nella select
    }

    // Verifica se il server ha nuove connessioni in attesa
    if (in_array($server, $readStreams, true)) {
        $client = stream_socket_accept($server, 0);
        if ($client) {
            stream_set_blocking($client, false);
            $clients[(int) $client] = $client;
        }
        // Rimuove il server dall'array dei pronti
        unset($readStreams[array_search($server, $readStreams, true)]);
    }

    // Legge dai client pronti
    foreach ($readStreams as $client) {
        $data = fread($client, 1024);

        if ($data === false || $data === '') {
            // Il client ha chiuso la connessione
            fclose($client);
            unset($clients[(int) $client]);
            continue;
        }

        // Risponde al client con un echo
        fwrite($client, "Echo: $data");
    }
}

fclose($server);

Metadati e informazioni sugli stream

La funzione stream_get_meta_data() restituisce un array associativo con informazioni sullo stato interno di uno stream: se ha raggiunto il timeout, se e' in modalita' bloccante, il tipo di wrapper, il nome dell'URI e altre proprieta':

<?php

// Ispezione dei metadati di uno stream HTTP
$context = stream_context_create(['http' => ['timeout' => 5]]);
$handle = fopen('https://httpbin.org/get', 'r', false, $context);

if ($handle) {
    $meta = stream_get_meta_data($handle);

    /*
     * Esempio di output di $meta:
     * [
     *   'timed_out'    => false,
     *   'blocked'      => true,
     *   'eof'          => false,
     *   'wrapper_type' => 'http',
     *   'stream_type'  => 'tcp_socket/ssl',
     *   'mode'         => 'r',
     *   'unread_bytes' => 0,
     *   'seekable'     => false,
     *   'uri'          => 'https://httpbin.org/get',
     *   'wrapper_data' => [...] // Header HTTP di risposta
     * ]
     */

    if ($meta['timed_out']) {
        throw new RuntimeException('Lo stream ha superato il timeout');
    }

    echo stream_get_contents($handle);
    fclose($handle);
}

La funzione fstat() restituisce informazioni di tipo filesystem sullo stream (dimensione, timestamp, permessi), compatibili con quelle di stat():

<?php

// Lettura delle informazioni statistiche di un file tramite stream
$handle = fopen('/var/log/syslog', 'r');
$stats = fstat($handle);

echo "Dimensione: {$stats['size']} byte\n";
echo "Ultima modifica: " . date('Y-m-d H:i:s', $stats['mtime']) . "\n";

fclose($handle);

Copia e piping tra stream

La funzione stream_copy_to_stream() copia i dati da uno stream sorgente a uno stream destinazione in modo efficiente, senza caricare l'intero contenuto in memoria. Accetta parametri opzionali per specificare la quantita' massima di byte da copiare e la posizione di partenza:

<?php

// Copia efficiente di un file di grandi dimensioni
$source = fopen('/var/data/large_file.csv', 'r');
$destination = fopen('/var/backup/large_file_backup.csv', 'w');

// Copia tutti i dati dalla sorgente alla destinazione
$bytesCopied = stream_copy_to_stream($source, $destination);

echo "Copiati $bytesCopied byte.\n";

fclose($source);
fclose($destination);
<?php

// Streaming diretto di un file verso l'output HTTP
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="download.zip"');

$source = fopen('/var/files/archive.zip', 'rb');
$output = fopen('php://output', 'wb');

// Invia il file in blocchi di 8 KB senza caricare tutto in memoria
stream_copy_to_stream($source, $output, -1, 0);

fclose($source);
fclose($output);

Stream e socket

Le funzioni stream_socket_client() e stream_socket_server() forniscono un'interfaccia basata su stream per le connessioni di rete, supportando TCP, UDP e socket Unix:

<?php

// Connessione TCP con handshake TLS
$socket = stream_socket_client(
    'tls://smtp.example.com:465',
    $errorCode,
    $errorMessage,
    30,
    STREAM_CLIENT_CONNECT,
    stream_context_create([
        'ssl' => [
            'verify_peer'      => true,
            'verify_peer_name' => true,
        ]
    ])
);

if (!$socket) {
    throw new RuntimeException("Connessione fallita ($errorCode): $errorMessage");
}

// Legge il banner del server SMTP
$banner = fgets($socket);
echo "Server: $banner";

// Invia il comando EHLO
fwrite($socket, "EHLO localhost\r\n");
$response = fgets($socket);
echo "Risposta: $response";

fclose($socket);
<?php

// Server UDP che riceve datagrammi
$server = stream_socket_server(
    'udp://0.0.0.0:9999',
    $errorCode,
    $errorMessage,
    STREAM_SERVER_BIND
);

if (!$server) {
    throw new RuntimeException("Avvio server fallito: $errorMessage");
}

echo "In ascolto su UDP 9999...\n";

while (true) {
    // Riceve un datagramma (massimo 65535 byte) e ne recupera il mittente
    $data = stream_socket_recvfrom($server, 65535, 0, $peer);

    if ($data !== false && $data !== '') {
        echo "Da $peer: $data\n";
        // Risponde al mittente
        stream_socket_sendto($server, "ACK: $data", 0, $peer);
    }
}

Lettura riga per riga e chunked

Per file di grandi dimensioni e' fondamentale leggere i dati in blocchi anziche' caricare tutto in memoria con file_get_contents(). Le funzioni fgets(), fread() e fgetcsv() si prestano a questo pattern:

<?php

// Elaborazione riga per riga di un file CSV da decine di milioni di righe
function processCsvStream(string $filePath, callable $rowCallback): void
{
    $handle = fopen($filePath, 'r');

    if ($handle === false) {
        throw new RuntimeException("Impossibile aprire il file: $filePath");
    }

    // Salta la riga di intestazione
    fgetcsv($handle);

    $lineNumber = 0;

    while (($row = fgetcsv($handle, 4096, ',')) !== false) {
        $lineNumber++;
        $rowCallback($row, $lineNumber);
    }

    fclose($handle);
}

// Utilizzo: importa utenti da un CSV senza esaurire la memoria
processCsvStream('/var/data/users.csv', function (array $row, int $line): void {
    [$id, $name, $email] = $row;
    // Qui si inserirebbe la riga nel database
    echo "Riga $line: $name ($email)\n";
});
<?php

// Lettura di un file binario in blocchi da 4 KB
function hashFileStream(string $filePath, string $algorithm = 'sha256'): string
{
    $handle = fopen($filePath, 'rb');

    if ($handle === false) {
        throw new RuntimeException("File non apribile: $filePath");
    }

    $hashContext = hash_init($algorithm);

    // Legge e aggiorna il contesto hash blocco per blocco
    while (!feof($handle)) {
        $chunk = fread($handle, 4096);
        hash_update($hashContext, $chunk);
    }

    fclose($handle);

    return hash_final($hashContext);
}

$digest = hashFileStream('/var/data/firmware.bin');
echo "SHA-256: $digest\n";

Server-Sent Events con stream

Gli stream PHP si prestano naturalmente all'implementazione di Server-Sent Events (SSE), un protocollo che permette al server di inviare eventi al browser in modo unidirezionale su una connessione HTTP persistente:

<?php

// Endpoint SSE: invia aggiornamenti al client ogni secondo
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // Disabilita il buffering di Nginx

// Disabilita il timeout di esecuzione e il buffering di output
set_time_limit(0);
ob_implicit_flush(true);

$output = fopen('php://output', 'w');

$counter = 0;

while (true) {
    $counter++;

    // Formattazione secondo il protocollo SSE
    $event = "id: $counter\n";
    $event .= "event: update\n";
    $event .= "data: " . json_encode(['tick' => $counter, 'time' => time()]) . "\n\n";

    fwrite($output, $event);

    // Verifica che il client sia ancora connesso
    if (connection_aborted()) {
        break;
    }

    sleep(1);
}

fclose($output);

Gestione degli errori con stream

Le funzioni di stream PHP tendono a restituire false in caso di errore e a emettere warning. E' buona pratica sopprimere i warning con @ e gestire esplicitamente gli errori, oppure usare un error handler personalizzato:

<?php

// Apertura sicura di uno stream con gestione esplicita degli errori
function openStreamSafely(string $uri, string $mode): mixed
{
    // Cattura eventuali warning come eccezioni
    set_error_handler(function (int $errno, string $errstr) use ($uri): never {
        restore_error_handler();
        throw new RuntimeException("Impossibile aprire lo stream '$uri': $errstr", $errno);
    });

    try {
        $handle = fopen($uri, $mode);
    } finally {
        restore_error_handler();
    }

    if ($handle === false) {
        throw new RuntimeException("fopen ha restituito false per: $uri");
    }

    return $handle;
}

// Esempio di utilizzo con gestione dell'eccezione
try {
    $handle = openStreamSafely('/percorso/inesistente/file.txt', 'r');
    echo stream_get_contents($handle);
    fclose($handle);
} catch (RuntimeException $e) {
    // Gestione dell'errore senza propagare warning
    error_log($e->getMessage());
}

Considerazioni sulle prestazioni

L'uso corretto degli stream ha impatto diretto sull'utilizzo di memoria e sulla latenza. Alcune linee guida pratiche:

Preferire la lettura a blocchi (fread con buffer esplicito) a file_get_contents() per file superiori a pochi megabyte. Usare stream_copy_to_stream() per trasferire dati tra stream, evitando variabili intermedie. Impostare timeout realistici sui socket di rete con stream_set_timeout(). Valutare l'uso di php://temp al posto di file temporanei su disco per buffer transitori di dimensione moderata.

<?php

// Confronto tra approccio a buffer intero e approccio a stream
// Approccio sbagliato per file grandi: carica tutto in RAM
function processFileBad(string $path): int
{
    $content = file_get_contents($path); // Potenzialmente centinaia di MB in memoria
    return substr_count($content, "\n");
}

// Approccio corretto: lettura a blocchi con conteggio incrementale
function processFileGood(string $path): int
{
    $handle = fopen($path, 'r');
    $lineCount = 0;

    // Legge 8 KB alla volta
    while (!feof($handle)) {
        $chunk = fread($handle, 8192);
        $lineCount += substr_count($chunk, "\n");
    }

    fclose($handle);
    return $lineCount;
}

Conclusione

Il sistema degli stream di PHP e' una delle sue architetture piu' eleganti e meno esplorate. Dalla lettura trasparente di risorse remote alla creazione di wrapper completamente personalizzati, passando per filtri componibili e socket non bloccanti, gli stream offrono un modello di I/O coerente e potente. Investire nella comprensione di questo sistema significa scrivere codice piu' efficiente in termini di memoria, piu' flessibile nella gestione delle sorgenti dati e piu' robusto nella gestione degli errori.

Torna su