Trabalhando com arquivos: leitura, escrita e permissões

1. Fundamentos da manipulação de arquivos em PHP

1.1. O sistema de arquivos no PHP: funções nativas vs. wrappers de fluxo

O PHP oferece duas abordagens principais para manipular arquivos: funções nativas do sistema de arquivos e wrappers de fluxo (streams). As funções nativas como fopen, fread e fwrite operam diretamente no sistema de arquivos local, enquanto os wrappers de stream permitem acessar recursos como memória, compressão ou até URLs remotas de forma transparente.

// Função nativa
$handle = fopen('arquivo.txt', 'r');
$conteudo = fread($handle, 1024);
fclose($handle);

// Wrapper de stream
$stream = fopen('php://memory', 'r+');
fwrite($stream, 'dados temporários');
rewind($stream);
echo stream_get_contents($stream);

1.2. Modos de abertura de arquivos (fopen)

O segundo parâmetro de fopen define o modo de acesso. Os modos mais comuns são:

  • 'r' — leitura apenas, ponteiro no início
  • 'r+' — leitura e escrita, ponteiro no início
  • 'w' — escrita apenas, cria ou sobrescreve
  • 'w+' — leitura e escrita, cria ou sobrescreve
  • 'a' — escrita apenas, append (ponteiro no final)
  • 'a+' — leitura e escrita, append
  • 'x' — escrita exclusiva, falha se o arquivo existir
  • 'b' — modo binário (combinado com outros, ex.: 'rb')
// Leitura simples
$arquivo = fopen('dados.txt', 'r');

// Escrita com sobrescrita
$arquivo = fopen('novo.txt', 'w');

// Append (adicionar ao final)
$arquivo = fopen('log.txt', 'a');

// Modo binário para arquivos não-texto
$imagem = fopen('foto.jpg', 'rb');

1.3. Verificação de existência e estado do arquivo

Antes de qualquer operação, é prudente verificar se o arquivo existe e se as permissões são adequadas.

$caminho = '/tmp/meu_arquivo.txt';

if (file_exists($caminho)) {
    if (is_file($caminho)) {
        if (is_readable($caminho)) {
            echo "Arquivo existe e pode ser lido.";
        }
        if (is_writable($caminho)) {
            echo "Arquivo existe e pode ser escrito.";
        }
    }
} else {
    echo "Arquivo não encontrado.";
}

2. Leitura de arquivos: estratégias e boas práticas

2.1. Leitura completa: file_get_contents, file e readfile

Para arquivos pequenos (até alguns MB), a leitura completa é a abordagem mais simples e eficiente.

// Lê todo o conteúdo como string
$conteudo = file_get_contents('config.php');

// Lê como array de linhas
$linhas = file('usuarios.csv');
foreach ($linhas as $linha) {
    echo htmlspecialchars($linha) . "<br>";
}

// Envia o conteúdo diretamente para a saída (útil para downloads)
readfile('relatorio.pdf');

2.2. Leitura linha a linha com fgets e fgetcsv para arquivos grandes

Arquivos grandes (logs, CSVs enormes) devem ser processados linha a linha para evitar consumo excessivo de memória.

$handle = fopen('logs_servidor.txt', 'r');
if ($handle) {
    while (($linha = fgets($handle)) !== false) {
        // Processa cada linha
        if (str_contains($linha, 'ERROR')) {
            echo $linha;
        }
    }
    fclose($handle);
}

// Para arquivos CSV
$csv = fopen('clientes.csv', 'r');
while (($dados = fgetcsv($csv, 1000, ',')) !== false) {
    echo "Nome: {$dados[0]}, Email: {$dados[1]}<br>";
}
fclose($csv);

2.3. Leitura em partes com fread e controle de buffer

Para arquivos binários ou quando você precisa controlar exatamente quantos bytes ler.

$handle = fopen('video.mp4', 'rb');
$bufferSize = 8192; // 8KB

while (!feof($handle)) {
    $chunk = fread($handle, $bufferSize);
    // Processa o chunk (ex.: enviar para download)
    echo $chunk;
    ob_flush();
    flush();
}
fclose($handle);

3. Escrita e criação de arquivos

3.1. Escrita simples: file_put_contents e fwrite

file_put_contents é a função mais direta para escrever arquivos pequenos.

// Escreve (ou sobrescreve) um arquivo
file_put_contents('saida.txt', "Conteúdo do arquivo\n");

// Com flags: FILE_APPEND para adicionar ao final
file_put_contents('log.txt', "Novo log entry\n", FILE_APPEND);

// Usando fwrite para mais controle
$handle = fopen('personalizado.txt', 'w');
fwrite($handle, "Linha 1\n");
fwrite($handle, "Linha 2\n");
fclose($handle);

3.2. Escrita incremental e append com bloqueio de arquivo (flock)

Em ambientes concorrentes, use flock para evitar corrupção de dados.

$handle = fopen('contador.txt', 'c+');
if (flock($handle, LOCK_EX)) { // Bloqueio exclusivo
    $contador = (int) fread($handle, 1024);
    $contador++;
    ftruncate($handle, 0);
    rewind($handle);
    fwrite($handle, (string) $contador);
    flock($handle, LOCK_UN); // Libera bloqueio
}
fclose($handle);

3.3. Criação de diretórios e arquivos temporários

// Criar diretórios recursivamente
mkdir('/tmp/app/logs/2024', 0755, true);

// Arquivo temporário com nome único
$tmp = tmpfile();
fwrite($tmp, 'dados temporários');
$meta = stream_get_meta_data($tmp);
echo "Arquivo temporário: " . $meta['uri'];

// Ou obter caminho do diretório temporário
$caminho = sys_get_temp_dir() . '/meu_arquivo_' . uniqid() . '.tmp';
file_put_contents($caminho, 'conteúdo');

4. Manipulação avançada com ponteiros e streams

4.1. Controle de ponteiro: fseek, ftell, rewind

$handle = fopen('arquivo.txt', 'r+');
echo "Posição atual: " . ftell($handle) . "\n"; // 0

fseek($handle, 10); // Move para o byte 10
echo "Nova posição: " . ftell($handle) . "\n";

rewind($handle); // Volta ao início
echo "Após rewind: " . ftell($handle) . "\n"; // 0

fclose($handle);

4.2. Trabalhando com wrappers de stream

Os wrappers php://memory e php://temp são úteis para processar dados sem criar arquivos físicos.

// Usando memória para dados temporários
$stream = fopen('php://memory', 'r+');
fwrite($stream, serialize(['nome' => 'João', 'idade' => 30]));
rewind($stream);
$dados = unserialize(stream_get_contents($stream));
fclose($stream);

// php://temp usa memória até um limite, depois usa disco
$stream = fopen('php://temp/maxmemory:1048576', 'r+');

// Aplicando filtros (ex.: compressão)
stream_filter_append($stream, 'zlib.deflate', STREAM_FILTER_WRITE);
fwrite($stream, 'dados a serem comprimidos');

4.3. Leitura e escrita de arquivos remotos via wrappers HTTP/FTP

// Ler conteúdo de uma URL (requer allow_url_fopen ativo)
$conteudo = file_get_contents('https://api.exemplo.com/dados.json');

// Upload via FTP
$ftp = fopen('ftp://usuario:senha@servidor/pasta/arquivo.txt', 'w');
fwrite($ftp, "conteúdo do arquivo remoto");
fclose($ftp);

5. Gerenciamento de permissões e metadados

5.1. Alteração de permissões com chmod

// Modo octal (0755 = rwxr-xr-x)
chmod('/var/www/html/config.php', 0644);

// Verificar permissões atuais
$perms = fileperms('arquivo.txt');
if ($perms & 0x004) {
    echo "Proprietário pode ler";
}

5.2. Propriedade de arquivos: chown, chgrp

// Alterar proprietário (requer privilégios de root)
chown('arquivo.txt', 'www-data');
chgrp('arquivo.txt', 'www-data');

// Em ambientes compartilhados, essas funções podem falhar

5.3. Metadados: fileatime, filemtime, filesize e stat

$caminho = 'documento.pdf';

echo "Último acesso: " . date('d/m/Y H:i:s', fileatime($caminho)) . "\n";
echo "Última modificação: " . date('d/m/Y H:i:s', filemtime($caminho)) . "\n";
echo "Tamanho: " . filesize($caminho) . " bytes\n";

// Informações completas com stat
$info = stat($caminho);
echo "Inode: " . $info['ino'] . "\n";
echo "Permissões: " . decoct($info['mode']) . "\n";

6. Tratamento de erros e exceções na manipulação de arquivos

6.1. Uso de try-catch com exceções

function lerArquivoSeguro(string $caminho): string {
    if (!file_exists($caminho)) {
        throw new \RuntimeException("Arquivo não encontrado: $caminho");
    }

    $conteudo = @file_get_contents($caminho);
    if ($conteudo === false) {
        throw new \RuntimeException("Erro ao ler o arquivo: $caminho");
    }

    return $conteudo;
}

try {
    $dados = lerArquivoSeguro('/etc/config.php');
} catch (\RuntimeException $e) {
    error_log("Falha na leitura: " . $e->getMessage());
    // Fallback para valores padrão
    $dados = '{}';
}

6.2. Verificações defensivas antes de operações de I/O

function escreverSeguro(string $caminho, string $conteudo): bool {
    $dir = dirname($caminho);

    // Verifica se o diretório existe e é gravável
    if (!is_dir($dir)) {
        if (!mkdir($dir, 0755, true)) {
            return false;
        }
    }

    if (!is_writable($dir)) {
        return false;
    }

    // Escrita atômica: escreve em arquivo temporário e renomeia
    $tmp = tempnam($dir, 'tmp_');
    if (file_put_contents($tmp, $conteudo) === false) {
        return false;
    }

    return rename($tmp, $caminho);
}

6.3. Logging de erros e fallback

function processarUpload(array $arquivo): ?string {
    try {
        if ($arquivo['error'] !== UPLOAD_ERR_OK) {
            throw new \RuntimeException("Erro no upload: código {$arquivo['error']}");
        }

        $destino = '/uploads/' . basename($arquivo['name']);
        if (!move_uploaded_file($arquivo['tmp_name'], $destino)) {
            throw new \RuntimeException("Falha ao mover arquivo");
        }

        return $destino;
    } catch (\RuntimeException $e) {
        error_log("Upload falhou: " . $e->getMessage());
        return null;
    }
}

7. Casos práticos e segurança

7.1. Upload seguro de arquivos

function uploadSeguro(array $arquivo): string {
    $extensoesPermitidas = ['jpg', 'png', 'pdf', 'txt'];
    $tamanhoMaximo = 5 * 1024 * 1024; // 5MB

    $extensao = strtolower(pathinfo($arquivo['name'], PATHINFO_EXTENSION));

    if (!in_array($extensao, $extensoesPermitidas)) {
        throw new \InvalidArgumentException("Extensão não permitida: $extensao");
    }

    if ($arquivo['size'] > $tamanhoMaximo) {
        throw new \InvalidArgumentException("Arquivo muito grande");
    }

    // Validar tipo MIME real
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mime = finfo_file($finfo, $arquivo['tmp_name']);
    finfo_close($finfo);

    if (!in_array($mime, ['image/jpeg', 'image/png', 'application/pdf', 'text/plain'])) {
        throw new \InvalidArgumentException("Tipo MIME inválido: $mime");
    }

    // Gerar nome único para evitar colisões
    $nomeUnico = uniqid() . '_' . bin2hex(random_bytes(8)) . '.' . $extensao;
    $destino = __DIR__ . '/uploads/' . $nomeUnico;

    if (!move_uploaded_file($arquivo['tmp_name'], $destino)) {
        throw new \RuntimeException("Falha ao salvar arquivo");
    }

    return $destino;
}

7.2. Prevenção de path traversal e injeção de caminhos

function caminhoSeguro(string $base, string $caminhoUsuario): string {
    // Remove qualquer componente de diretório pai
    $caminhoLimpo = basename($caminhoUsuario);

    // Resolve caminho absoluto
    $caminhoReal = realpath($base . '/' . $caminhoLimpo);

    // Verifica se está dentro do diretório base
    $baseReal = realpath($base);
    if ($caminhoReal === false || strpos($caminhoReal, $baseReal) !== 0) {
        throw new \InvalidArgumentException("Caminho inválido ou fora do diretório permitido");
    }

    return $caminhoReal;
}

// Uso
try {
    $arquivo = caminhoSeguro('/var/www/uploads', $_GET['arquivo']);
    readfile($arquivo);
} catch (\InvalidArgumentException $e) {
    http_response_code(403);
    echo "Acesso negado";
}

7.3. Limpeza de arquivos temporários e boas práticas de concorrência

function processarLoteComLock(string $arquivo, callable $processador): void {
    $handle = fopen($arquivo, 'c+');

    if (!flock($handle, LOCK_EX)) {
        throw new \RuntimeException("Não foi possível obter bloqueio");
    }

    try {
        $conteudo = stream_get_contents($handle);
        $resultado = $processador($conteudo);

        ftruncate($handle, 0);
        rewind($handle);
        fwrite($handle, $resultado);
    } finally {
        flock($handle, LOCK_UN);
        fclose($handle);
    }
}

// Limpeza automática de arquivos temporários
register_shutdown_function(function () {
    $tmpDir = sys_get_temp_dir();
    $padrao = $tmpDir . '/tmp_*';
    foreach (glob($padrao) as $arquivo) {
        if (filemtime($arquivo) < time() - 3600) { // Mais de 1 hora
            unlink($arquivo);
        }
    }
});

Referências

  • PHP Manual: Filesystem Functions — Documentação oficial completa de todas as funções de sistema de arquivos no PHP.
  • PHP Manual: Stream Wrappers — Lista completa de wrappers de stream suportados pelo PHP, incluindo php://memory, php://temp e wrappers HTTP/FTP.
  • PHP The Right Way: File Handling — Guia de boas práticas para manipulação de arquivos em PHP, com exemplos de segurança e eficiência.
  • [OWASP: File Upload Cheat Sheet](https://cheats

atsheetseries/AJAX_Security_Cheat_Sheet) — Referência essencial para implementar upload seguro de arquivos e prevenir vulnerabilidades comuns.
- PHP Security Guide: Filesystem Security — Aborda riscos de path traversal, permissões inadequadas e outras ameaças relacionadas ao sistema de arquivos.

Conclusão

A manipulação de arquivos em PHP é uma habilidade fundamental para qualquer desenvolvedor web. Desde operações simples como leitura e escrita até tarefas complexas como upload seguro e gerenciamento de concorrência, dominar as funções nativas e os wrappers de stream permite construir aplicações robustas e seguras.

Ao longo deste artigo, exploramos:

  • Fundamentos: modos de abertura, verificação de existência e estado dos arquivos.
  • Leitura: estratégias para arquivos pequenos e grandes, com foco em desempenho e uso de memória.
  • Escrita: criação incremental, append, bloqueio de arquivo e manipulação de diretórios.
  • Streams e ponteiros: controle fino sobre o fluxo de dados e uso de wrappers especiais.
  • Permissões e metadados: alteração de permissões, propriedade e consulta de informações do arquivo.
  • Tratamento de erros: uso de exceções, verificações defensivas e logging para operações seguras.
  • Segurança prática: upload seguro, prevenção de path traversal e limpeza de arquivos temporários.

Lembre-se sempre de aplicar verificações defensivas, validar entradas do usuário e utilizar bloqueios de arquivo em operações concorrentes. Com essas práticas, você estará preparado para lidar com qualquer desafio de I/O no ecossistema PHP.