Streams em PHP: O Poder Oculto da Linguagem

12/10/202521 min de leitura

O Básico: Lendo Arquivos

Vamos começar com algo familiar. Todo mundo conhece esse código básico de leitura de arquivos:

$file = fopen('names.txt', 'r');
while ($line = fgets($file)) {
    echo $line;
}
fclose($file);

Simples, direto: abre um arquivo, lê linha por linha, fecha. Nada de novo aqui.

A Virada: Inception com Streams

Agora fica interessante. E se eu te disser que o fopen não trabalha apenas com arquivos locais? Ele trabalha com múltiplos protocolos.

Quando você escreve fopen('names.txt', 'r'), na verdade está fazendo fopen('file://names.txt', 'r'). O PHP abstrai toda a complexidade através de stream wrappers.

E o mais legal? Funciona com HTTP também! Você pode fazer requisições web usando as mesmas funções de arquivo:

$file = fopen('http://localhost:8001/names.php', 'r');
while ($line = fgets($file)) {
    echo $line;
}
fclose($file);

O PHP abstrai toda a complexidade do protocolo HTTP (headers, requests, responses) através desses wrappers mágicos. Você está literalmente fazendo uma requisição HTTP, mas usando funções de arquivo!

Filtros: Transformando Dados em Tempo Real

Aqui é onde as coisas ficam realmente legais. O PHP tem filtros embutidos que você pode aplicar nas streams. Por exemplo:

Filter Strip Tags

Remove todas as tags HTML de uma stream. Perfeito para quando você está no terminal e não quer ver um monte de tags bagunçando sua saída:

$file = fopen('php://filter/read=string.strip_tags/resource=names.txt', 'r');
fpassthru($file);

Filter ROT13

Rotaciona 13 posições no código ASCII de cada caractere. É tipo uma criptografia bem básica (bem pobre mesmo). O texto fica parecendo russo, mas é só ROT13:

$file = fopen('php://filter/read=string.rot13/resource=names.txt', 'r');
fpassthru($file);

PHP como Pipe no Terminal

Aqui vem a mágica: você pode usar o PHP como pipe no terminal, tipo o grep!

php fopen.php | php -r "stream_copy_to_stream(
    fopen('php://filter/read=string.strip_tags/resource=php://stdin', 'r'),
    fopen('php://stdout', 'w')
);"

O melhor? A função stream_copy_to_stream() copia de uma stream para outra sem usar memória do PHP. É praticamente zero de memória consumida. Eficiência pura!

Você pode encadear quantos filtros quiser:

php fopen.php | php -r "stream_copy_to_stream(
    fopen('php://filter/read=string.strip_tags|string.rot13/resource=php://stdin', 'r'),
    fopen('php://stdout', 'w')
);"

Agora removeu as tags E transformou em ROT13. Tudo em uma linha!

Criando Filtros Customizados

Aqui começam as bizarrices do PHP. Para criar filtros customizados, você precisa estender a classe php_user_filter (sim, com esse nome bizarro que não tem nada a ver com as outras classes do PHP).

Isso vem do PHP 4, quando o modelo de objetos ainda não estava bem definido. A implementação usa um esquema de "buckets" copiado do Apache:

class UpperFilter extends php_user_filter {
    public function filter($in, $out, &$consumed, $closing) {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = strtoupper($bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register('upper', 'UpperFilter');

$file = fopen('php://filter/read=string.strip_tags|upper/resource=php://stdin', 'r');
stream_copy_to_stream($file, fopen('php://stdout', 'w'));

Dei toda essa volta só para transformar um texto em maiúsculas, mas é o jeito mais simples de mostrar essa API.

Contextos: Controlando Requisições HTTP

Você pode criar contextos para controlar como as requisições são feitas. O contexto é aquele quarto parâmetro esquisito do fopen:

$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => 'Content-Type: application/json',
        'content' => json_encode(['data' => 'valor'])
    ]
]);

$result = file_get_contents('http://api.exemplo.com', false, $context);

Com isso você pode fazer requisições HTTP muito mais relevantes: mudar o método (POST, PUT, DELETE), passar headers customizados, enviar body... tudo isso!

Implementando o Comando yes em PHP

Alguém conhece o comando yes do terminal? Ele fica repetindo "y" infinitamente. É útil para automatizar comandos que pedem confirmação.

Vamos implementar nosso próprio yes criando um stream wrapper customizado:

class YesWrapper {
    // implements streamWrapper (não existe de verdade, só na documentação)
    
    public function stream_open($path, $mode, $options, &$opened_path) {
        return true; // sempre abre com sucesso
    }
    
    public function stream_read($count) {
        usleep(1000000); // 1 segundo de sleep
        return "y\n"; // sempre retorna y
    }
    
    public function stream_eof() {
        return false; // nunca termina, é infinito!
    }
    
    public function stream_stat() {
        return []; // informações do arquivo
    }
}

stream_wrapper_register('yes', 'YesWrapper');
fpassthru(fopen('yes://', 'r'));

Isso cria um arquivo virtual infinito que só tem "y"s! É como se você estivesse lendo de um arquivo que nunca acaba.

Mock de Arquivos com vfsStream (Respect/Test)

Aqui fica extremamente útil: imagine que você tem código que mexe com arquivos e quer testar sem criar arquivos reais. O Respect/Test tem uma feature incrível:

use org\bovigo\vfs\vfsStream;

vfsStream::setup();
vfsStream::create([
    'pandas.txt' => "Fulano\nCiclano\nBeutrano"
]);

// Agora você pode usar vfs://pandas.txt como se fosse um arquivo real!
$content = file_get_contents('vfs://pandas.txt');

// Pode gravar também
file_put_contents('vfs://pandas.txt', 'novos dados');

Você cria um sistema de arquivos virtual completamente em memória. Perfeito para testes unitários! Não precisa criar arquivos temporários, fazer cleanup, nada disso.

Detalhe importante: use clearstatcache() se precisar trocar entre o sistema real e o virtual, porque o PHP cacheia informações de arquivos.

Socket Servers: Criando Seu Próprio Servidor

Você não precisa apenas consumir streams. Você pode criar streams também! Vamos criar um socket server:

$server = stream_socket_server('tcp://localhost:1234', $errno, $errstr);

while ($client = stream_socket_accept($server, -1)) {
    $peer = stream_socket_get_name($client, true);
    echo "Conectado: $peer\n";
    
    while ($data = fgets($client)) {
        echo "Recebido: $data";
    }
    
    fclose($client);
}

Agora você tem um servidor TCP rodando! Pode conectar nele usando nc localhost 1234 (netcat) ou telnet.

O problema? Esse código aceita apenas um cliente por vez. Se um segundo cliente tentar conectar, ele fica esperando o primeiro terminar.

Event Loop com stream_select()

Aqui vem a mágica para suportar múltiplos clientes simultâneos:

$server = stream_socket_server('tcp://0.0.0.0:1234', $errno, $errstr);
$clients = [];

while (true) {
    $reads = array_merge([$server], $clients);
    $writes = $excepts = null;
    
    stream_select($reads, $writes, $excepts, 60);
    
    if (in_array($server, $reads)) {
        $client = stream_socket_accept($server, 0);
        $peer = stream_socket_get_name($client, true);
        echo "Conectado: $peer\n";
        $clients[] = $client;
        
        unset($reads[array_search($server, $reads)]);
    }
    
    foreach ($reads as $client) {
        $data = fgets($client);
        if (!$data) {
            echo "Cliente desconectou\n";
            fclose($client);
            unset($clients[array_search($client, $clients)]);
        } else {
            echo "Recebido: $data";
        }
    }
}

O stream_select() é um event loop. Ele monitora múltiplas streams ao mesmo tempo e te avisa quando alguma tem dados para ler.

Agora você pode ter vários clientes conectados simultaneamente! Um não bloqueia o outro.

O Que É stream_select()?

O stream_select() é basicamente um bind para a função select() do sistema operacional (system call em C). É bem low-level.

Não é thread, não é multi-processo. É um tipo de paralelismo diferente: você processa eventos de múltiplas conexões em um único processo. É o mesmo conceito usado por frameworks assíncronos.

Quem Usa Isso?

Você pode estar pensando: "legal, mas quem usa isso na vida real?"

  • Guzzle: biblioteca HTTP famosa, usa streams
  • ReactPHP: framework assíncrono, usa event loops com streams
  • Respect/Test: para mockar arquivos em testes

Streams são a base de muita coisa no PHP, só que ninguém fala sobre elas!

Três APIs Bizarras para a Mesma Coisa

O PHP tem três APIs diferentes que mexem com streams:

  1. Funções básicas: fopen(), fgets(), fread(), fclose() - a API tradicional de arquivos
  2. Filtros e Wrappers: php_user_filter, stream_wrapper_register() - vem do PHP 4, antes do modelo de objetos estar definido
  3. Sockets: stream_socket_server(), stream_select() - bindings diretos para system calls do C

Cada uma tem suas peculiaridades e bizarrices, mas todas trabalham com o mesmo conceito de streams.

Conclusão

Streams em PHP são poderosas, bizarras e completamente subutilizadas. Você pode:

  • Ler de HTTP como se fossem arquivos
  • Criar filtros customizados para transformar dados
  • Mockar arquivos para testes
  • Criar servidores TCP do zero
  • Fazer paralelismo com event loops
  • Processar dados com consumo zero de memória

É low-level, é estranho, mas funciona. E abre um mundo de possibilidades que você provavelmente nunca imaginou que existiam no PHP.

Agora vai lá experimentar! E quando der erro (porque vai dar), lembre-se: quanto mais gente assistindo, maior a chance de dar errado. É a lei do live coding! 😄

Gostou do post?

Deixe seu like e ajude a divulgar o conteúdo!