Generators para Processamento Eficiente de Memória
Truques práticos usando Generators para processar arquivos gigantes, logs, CSVs e streams sem explodir a memória
💾 Generators para Processamento Eficiente de Memória
Este guia mostra truques práticos para usar Generators (PHP 5.5+) para processar dados gigantes com memória constante - ideal para logs, CSVs, ETL e streaming.
💡 Compatibilidade: PHP 5.5+. Para conceitos básicos, veja Coroutines em PHP.
📚 Índice
- Por que Generators?
- Trick #1: Processamento de Logs Gigantes
- Trick #2: Parser de CSV com Transformação
- Resumo dos Tricks
🎯 Por que Generators?
O Problema Real
Imagine que você precisa processar um arquivo de log de 5GB. Com abordagens tradicionais:
// ❌ Isso vai EXPLODIR a memória!
$lines = file('huge.log'); // Tenta carregar 5GB na RAM
Por que isso é um problema?
- Limite de memória do PHP: Padrão é 128MB-256MB
- Slow down do sistema: Swap disk quando excede RAM
- Crash: "Allowed memory size exhausted"
A Solução: Generators
O conceito: Em vez de carregar TUDO, processa 1 item por vez.
Analogia: É como ler um livro página por página vs. tentar decorar o livro inteiro antes de começar.
Comparação de Memória
graph LR
A[Arquivo 2GB] -->|file| B[Array 2GB<br/>❌ Memória explode]
A -->|Generator| C[1 linha por vez<br/>✅ ~1KB memória]
style B fill:#501616
style C fill:#0b4315
Complexidade de Memória:
- Arrays tradicionais: $O(n)$ - cresce com tamanho do arquivo
- Generators: $O(1)$ - constante, sempre ~1KB
Quando usar Generators:
- ✅ Arquivos grandes (logs, CSVs, dumps)
- ✅ Processamento sequencial (linha por linha)
- ✅ Transformações de dados (ETL)
- ✅ Quando memória é limitada
Quando NÃO usar:
- ❌ Dados pequenos (\u003c10MB) - overhead desnecessário
- ❌ Acesso aleatório - generators são sequenciais
- ❌ Múltiplas passadas - cada yield é único
💡 Trick #1: Processamento de Logs Gigantes
Contexto do Problema
Você tem um servidor web com logs de acesso de 50GB. Precisa:
- Encontrar todos os erros 500
- Contar requisições por IP
- Estatísticas por código de status
Approach tradicional: Explodiria a memória Com Generators: Processa suavemente, 1 linha por vez
Por que isso funciona?
O Generator não carrega o arquivo. Ele apenas:
- Abre o arquivo (handle)
- Lê 1 linha
- Processa
- Descarta
- Repete
Análise de Complexidade
Implementação
<?php
class LogAnalyzer {
private string $logFile;
public function __construct(string $logFile) {
$this->logFile = $logFile;
}
/**
* Lê linhas do log preguiçosamente
*/
private function readLines(): Generator {
$handle = fopen($this->logFile, 'r');
if (!$handle) {
throw new Exception("Não foi possível abrir: {$this->logFile}");
}
$lineNum = 0;
while (($line = fgets($handle)) !== false) {
$lineNum++;
yield $lineNum => trim($line);
}
fclose($handle);
}
/**
* Filtra apenas linhas de erro
*/
public function errors(): Generator {
foreach ($this->readLines() as $lineNum => $line) {
// Apache/Nginx error pattern
if (preg_match('/\[(error|crit|alert|emerg)\]/i', $line)) {
yield $lineNum => $line;
}
}
}
/**
* Agrupa por código de status HTTP
*/
public function byStatusCode(): array {
$stats = [];
foreach ($this->readLines() as $line) {
// Extrai código de status (ex: "HTTP/1.1" 200)
if (preg_match('/" (\d{3}) /', $line, $matches)) {
$code = $matches[1];
$stats[$code] = ($stats[$code] ?? 0) + 1;
}
}
return $stats;
}
/**
* Encontra IPs com mais requisições
*/
public function topIPs(int $limit = 10): Generator {
$ipCounts = [];
// Conta IPs
foreach ($this->readLines() as $line) {
// IP no início da linha
if (preg_match('/^(\d+\.\d+\.\d+\.\d+)/', $line, $matches)) {
$ip = $matches[1];
$ipCounts[$ip] = ($ipCounts[$ip] ?? 0) + 1;
}
}
// Ordena e retorna top N
arsort($ipCounts);
$count = 0;
foreach ($ipCounts as $ip => $requests) {
if (++$count > $limit) break;
yield $ip => $requests;
}
}
}
// Cria arquivo de log de exemplo
$logContent = <<<LOG
192.168.1.100 - - [03/Dec/2025:10:15:23 +0000] "GET /index.html HTTP/1.1" 200 1234
192.168.1.101 - - [03/Dec/2025:10:15:24 +0000] "GET /api/users HTTP/1.1" 404 234
192.168.1.100 - - [03/Dec/2025:10:15:25 +0000] "POST /api/login HTTP/1.1" 200 456
192.168.1.102 - - [03/Dec/2025:10:15:26 +0000] "GET /admin HTTP/1.1" 403 123
[error] 2025/12/03 10:15:27 PHP Fatal error in /var/www/app.php
192.168.1.100 - - [03/Dec/2025:10:15:28 +0000] "GET /images/logo.png HTTP/1.1" 200 5678
LOG;
file_put_contents('test.log', $logContent);
$analyzer = new LogAnalyzer('test.log');
echo "=== Análise de Log ===\n\n";
// 1. Erros
echo "Erros encontrados:\n";
foreach ($analyzer->errors() as $lineNum => $error) {
echo "Linha $lineNum: " . substr($error, 0, 80) . "\n";
}
// 2. Status codes
echo "\nStatus codes:\n";
$codes = $analyzer->byStatusCode();
foreach ($codes as $code => $count) {
echo "$code: $count requisições\n";
}
// 3. Top IPs
echo "\nTop IPs:\n";
foreach ($analyzer->topIPs(3) as $ip => $count) {
echo "$ip: $count requisições\n";
}
unlink('test.log');
// Saída esperada
// === Análise de Log ===
//
// Erros encontrados:
// Linha 5: [error] 2025/12/03 10:15:27 PHP Fatal error in /var/www/app.php
//
// Status codes:
// 200: 3 requisições
// 404: 1 requisições
// 403: 1 requisições
//
// Top IPs:
// 192.168.1.100: 3 requisições
// 192.168.1.101: 1 requisições
// 192.168.1.102: 1 requisições
Vantagens desta Abordagem
Vs. Array Tradicional:
- ✅ Memória constante: 100GB arquivo = ~5MB RAM
- ✅ Lazy evaluation: Só processa quando necessário
- ✅ Componível:
readLines()é reutilizado
Vs. Ler linha por linha manualmente:
- ✅ Mais limpo: API de foreach simples
- ✅ Type-safe: Generator retorna tipo conhecido
- ✅ Testável: Fácil de mockar
Por que é melhor?
Imagine analisar logs de 1 ano (500GB):
- Array: Precisaria 500GB+ RAM = Impossível
- Generator: Usa ~10MB RAM = Funciona tranquilo
💡 Trick #2: Parser de CSV com Transformação
Contexto do Problema
Você recebeu um CSV de clientes com 10 milhões de linhas (2GB):
- Precisa validar emails
- Transformar dados
- Inserir no banco em lotes
Problema: Carregar tudo = crash Solução: Processamento funcional com generators
Por que Composição Funcional?
Generators permitem criar "pipelines" tipo Unix:
# No Unix
cat users.csv | grep ".com" | head -n 100
# Com Generators
$processor->rows()
->filter($emailValido)
->batch(100)
Cada operação apenas "decora" o generator anterior, sem carregar dados!
Implementação
<?php
class CSVProcessor {
private string $file;
private array $headers = [];
public function __construct(string $file) {
$this->file = $file;
}
/**
* Lê CSV linha por linha como array associativo
*/
public function rows(): Generator {
$handle = fopen($this->file, 'r');
// Primeira linha = headers
$this->headers = fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
// Combina headers com valores
yield array_combine($this->headers, $row);
}
fclose($handle);
}
/**
* Filtra por condição
*/
public function filter(callable $condition): Generator {
foreach ($this->rows() as $row) {
if ($condition($row)) {
yield $row;
}
}
}
/**
* Transforma cada linha
*/
public function map(callable $transformer): Generator {
foreach ($this->rows() as $row) {
yield $transformer($row);
}
}
/**
* Processa em lotes (batch)
*/
public function batch(int $size): Generator {
$batch = [];
foreach ($this->rows() as $row) {
$batch[] = $row;
if (count($batch) >= $size) {
yield $batch;
$batch = [];
}
}
// Último lote (pode ser menor)
if (!empty($batch)) {
yield $batch;
}
}
}
// Cria CSV de exemplo
$csv = <<<CSV
name,email,age,city
João Silva,joao@example.com,25,São Paulo
Maria Santos,maria@example.com,30,Rio de Janeiro
Pedro Oliveira,pedro@invalid,22,Belo Horizonte
Ana Costa,ana@example.com,28,Porto Alegre
Carlos Souza,carlos@example.com,35,Curitiba
CSV;
file_put_contents('users.csv', $csv);
$processor = new CSVProcessor('users.csv');
echo "=== Processamento de CSV ===\n\n";
// 1. Filtra apenas emails válidos
echo "Emails válidos:\n";
$validEmails = $processor->filter(function($row) {
return filter_var($row['email'], FILTER_VALIDATE_EMAIL);
});
foreach ($validEmails as $user) {
echo "- {$user['name']}: {$user['email']}\n";
}
// 2. Transforma dados
echo "\nUsuários maiores de 25:\n";
$adults = $processor->map(function($row) {
return [
'nome_completo' => strtoupper($row['name']),
'idade' => (int)$row['age'],
'email' => $row['email']
];
});
foreach ($adults as $user) {
if ($user['idade'] > 25) {
echo "- {$user['nome_completo']} ({$user['idade']} anos)\n";
}
}
// 3. Processa em lotes de 2
echo "\nProcessamento em lotes:\n";
$batchNum = 1;
foreach ($processor->batch(2) as $batch) {
echo "Lote $batchNum (" . count($batch) . " registros):\n";
foreach ($batch as $row) {
echo "- {$row['name']}\n";
}
$batchNum++;
}
unlink('users.csv');
// Saída esperada
// === Processamento de CSV ===
//
// Emails válidos:
// - João Silva: joao@example.com
// - Maria Santos: maria@example.com
// - Ana Costa: ana@example.com
// - Carlos Souza: carlos@example.com
//
// Usuários maiores de 25:
// - MARIA SANTOS (30 anos)
// - ANA COSTA (28 anos)
// - CARLOS SOUZA (35 anos)
//
// Processamento em lotes:
// Lote 1 (2 registros):
// - João Silva
// - Maria Santos
// Lote 2 (2 registros):
// - Pedro Oliveira
// - Ana Costa
// Lote 3 (1 registros):
// - Carlos Souza
Benefícios desta Abordagem
Composição Funcional:
- ✅
filter()+map()+batch()= Pipeline declarativo - ✅ Cada método retorna Generator = Chain infinitas
- ✅ Lazy = Nada executa até
foreach()
Uso Real:
Imagine importar 10M clientes:
// Processa 10M registros com ~10MB RAM!
foreach ($processor->batch(1000) as $batch) {
$db->insert($batch); // Insere 1000 por vez
}
Por que batch?
- ✅ INSERT em lote é 100x mais rápido
- ✅ Não trava memória
- ✅ Pode fazer commit parcial
Comparação:
| Método | 10M linhas | Memória | Tempo |
|---|---|---|---|
file() + insert 1-1 |
❌ Crash | - | - |
Generator + batch 1000 |
✅ OK | ~10MB | 5min |
Carregar tudo + insert all |
❌ Crash | - | - |
🎓 Resumo dos Tricks
| Trick | Cenário | Complexidade Memória | Ganho Real |
|---|---|---|---|
| Log Analyzer | Logs de 50GB+ | $O(1)$ | 1000x menos RAM |
| CSV Parser | CSVs de milhões de linhas | $O(1)$ | Sem limite de tamanho |
| Pipeline ETL | Transform/Load dados | $O(1)$ | Modular e testável |
Quando Usar Generators
✅ Use quando:
- Arquivos \u003e100MB
- Processamento sequencial
- Memória é limitada
- ETL/transformações de dados
- Stream processing
❌ NÃO use quando:
- Dados \u003c10MB (overhead não vale a pena)
- Precisa acesso aleatório (use array indexed)
- Múltiplas passadas nos mesmos dados (cache em array)
- Performance crítica de CPU (generators têm overhead)
O Segredo dos Generators
A mágica não é velocidade, é ESCALA:
- Generator de 10 linhas = mais lento que array
- Generator de 10 milhões = único que funciona
Princípio: Trade-off de um pouco de CPU por MUITA memória.
🔗 Recursos Relacionados
- Coroutines em PHP - Conceitos fundamentais
- Fibers em PHP - Coroutines modernas (PHP 8.1+)
- Fibers Async HTTP - HTTP assíncrono com Fibers
Generators: Processando o infinito com memória finita! 💾🚀