💾 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

  1. Por que Generators?
  2. Trick #1: Processamento de Logs Gigantes
  3. Trick #2: Parser de CSV com Transformação
  4. 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?

  1. Limite de memória do PHP: Padrão é 128MB-256MB
  2. Slow down do sistema: Swap disk quando excede RAM
  3. 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:

  1. Abre o arquivo (handle)
  2. Lê 1 linha
  3. Processa
  4. Descarta
  5. Repete

Análise de Complexidade

| Método | Memória | Tempo | Arquivo 10GB | |--------|---------|-------|--------------| | `file()` | $O(n)$ | $O(n)$ | ❌ 10GB RAM | | Generator | $O(1)$ | $O(n)$ | ✅ ~1MB RAM |

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


Generators: Processando o infinito com memória finita! 💾🚀