Nesta aula, vamos construir uma galeria de imagens completa em PHP, com foco em upload seguro, armazenamento adequado, listagem e exibição das imagens. Este projeto é um dos mais comuns em aplicações web, e dominá-lo é essencial para qualquer desenvolvedor PHP. Você aprenderá não apenas a implementar as funcionalidades, mas também a proteger sua aplicação contra vulnerabilidades comuns, como upload de arquivos maliciosos e path traversal.

Vamos desenvolver passo a passo, começando pela estrutura de diretórios, passando pelo formulário de upload, o processamento no servidor, e finalizando com a exibição das imagens em uma grade responsiva. Ao final, você terá um projeto funcional que pode ser expandido com recursos como miniaturas, paginação e autenticação.

Upload seguro

O upload de arquivos é uma funcionalidade crítica que, se mal implementada, pode comprometer todo o servidor. A segurança deve ser a prioridade número um. Vamos abordar as principais medidas de segurança: validação do tipo de arquivo, limitação do tamanho, verificação do conteúdo real (não apenas a extensão), e prevenção contra scripts maliciosos.

Em PHP, o upload é tratado via superglobal $_FILES. O primeiro passo é verificar se o arquivo foi enviado sem erros. Em seguida, devemos validar o tipo MIME usando finfo (File Information) para detectar o tipo real do arquivo, pois a extensão pode ser falsificada. Também é essencial gerar um nome único para o arquivo, para evitar sobrescritas e ataques de path traversal. Vamos ver um exemplo de função de upload seguro:

function uploadSeguro(array $file, string $destino): bool {
    // Verifica se houve erro no upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return false;
    }

    // Verifica o tamanho máximo (2MB)
    if ($file['size'] > 2 * 1024 * 1024) {
        return false;
    }

    // Verifica o tipo MIME real
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file['tmp_name']);
    $allowed = ['image/jpeg', 'image/png', 'image/gif'];
    if (!in_array($mime, $allowed)) {
        return false;
    }

    // Gera um nome único
    $extensao = pathinfo($file['name'], PATHINFO_EXTENSION);
    $novoNome = bin2hex(random_bytes(16)) . '.' . $extensao;
    $caminho = $destino . '/' . $novoNome;

    // Move o arquivo
    return move_uploaded_file($file['tmp_name'], $caminho);
}

Além disso, é importante configurar o php.ini com upload_max_filesize e post_max_size adequados. Nunca confie no nome original do arquivo para armazenamento, pois pode conter caracteres perigosos. Sempre use um nome gerado aleatoriamente. Também evite armazenar arquivos em diretórios públicos executáveis; se possível, coloque-os fora da raiz do servidor e sirva via um script intermediário.

Armazenamento

O armazenamento das imagens deve ser organizado e seguro. No nosso projeto, criaremos uma pasta uploads dentro do diretório público, mas com permissões restritas. Para evitar execução de PHP dentro da pasta de uploads, podemos adicionar um arquivo .htaccess que desabilita a execução de scripts, ou melhor, armazenar fora da raiz pública e servir via um script PHP que valida o acesso.

Vamos adotar uma abordagem prática: criar uma estrutura de diretórios com subpastas por data (ex: uploads/2025/02/) para melhor organização. Isso facilita a manutenção e evita um número excessivo de arquivos em um único diretório. No nosso script de upload, vamos incluir a criação automática da pasta, caso não exista.

$destino = __DIR__ . '/uploads/' . date('Y/m');
if (!is_dir($destino)) {
    mkdir($destino, 0755, true);
}

if (uploadSeguro($_FILES['imagem'], $destino)) {
    echo "Upload realizado com sucesso!";
} else {
    echo "Falha no upload.";
}

Também é recomendável armazenar metadados no banco de dados (como nome original, tamanho, data) para facilitar a listagem e futuras funcionalidades. No entanto, para este projeto, podemos listar diretamente os arquivos do diretório, o que é suficiente para demonstração. Se optar por banco de dados, lembre-se de usar prepared statements para evitar SQL injection.

Listagem

A listagem das imagens consiste em ler o diretório de uploads e exibir as imagens em uma grade. Precisamos percorrer recursivamente as subpastas (se houver) e coletar os caminhos das imagens. Vamos criar uma função que retorna um array com os caminhos de todas as imagens, ordenadas por data de modificação (mais recentes primeiro).

Usaremos a classe DirectoryIterator ou RecursiveDirectoryIterator para varrer os diretórios. É importante filtrar apenas arquivos de imagem (extensões permitidas). Vamos também incluir uma paginação simples, para não sobrecarregar a página com muitas imagens.

function listarImagens(string $dir): array {
    $imagens = [];
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir)
    );

    foreach ($iterator as $file) {
        if ($file->isFile()) {
            $ext = strtolower($file->getExtension());
            if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif'])) {
                $imagens[] = $file->getPathname();
            }
        }
    }

    // Ordena por data de modificação (mais recente primeiro)
    usort($imagens, fn($a, $b) => filemtime($b) - filemtime($a));
    return $imagens;
}

$imagens = listarImagens(__DIR__ . '/uploads');

Na página de listagem, exibiremos as imagens em uma grade usando CSS Grid ou Flexbox. Cada imagem pode ser clicável para abrir em um modal ou em uma página separada. Vamos usar o Bootstrap para estilizar a grade.

Exibição

A exibição das imagens deve ser feita de forma segura e performática. Como as imagens estão no diretório público, podemos simplesmente usar a tag <img> com o caminho relativo. No entanto, se optarmos por armazenar fora da raiz pública, precisaremos de um script intermediário que leia o arquivo e envie com o header correto.

Aqui, vamos manter as imagens em public/uploads e exibi-las diretamente. Para evitar quebra de layout, definiremos um tamanho fixo para as miniaturas, usando CSS. Também podemos adicionar um atributo loading="lazy" para melhorar o desempenho.

foreach ($imagens as $imagem) {
    // Caminho relativo para a tag img
    $caminhoRelativo = 'uploads/' . basename(dirname($imagem)) . '/' . basename($imagem);
    echo "<div class='col-md-3 mb-3'>";
    echo "<img src='$caminhoRelativo' alt='Imagem' class='img-fluid rounded' loading='lazy'>";
    echo "</div>";
}

Para um projeto mais robusto, poderíamos gerar miniaturas com a biblioteca GD ou Imagick, mas isso foge ao escopo desta aula. No entanto, é uma ótima prática para otimizar o carregamento. Vamos incluir uma seção de boas práticas no final.

Boas práticas e observações finais

Além das medidas de segurança já mencionadas, é importante considerar:

  • Valide também o nome do arquivo original, removendo caracteres especiais e espaços.
  • Use htmlspecialchars() ao exibir qualquer dado vindo do usuário para evitar XSS.
  • Configure um limite de upload no formulário e no servidor.
  • Considere usar serviços de armazenamento em nuvem (S3, etc.) para escalabilidade.
  • Monitore os logs de erro para identificar tentativas de upload malicioso.

Este projeto é a base para muitos outros, como redes sociais, portfólios e sistemas de gerenciamento de conteúdo. Com as técnicas aprendidas, você pode expandir adicionando autenticação, permissões e processamento de imagens.

Referências

Exercícios

  1. Exercício 1: Modifique a função uploadSeguro para aceitar também arquivos SVG, mas com uma validação de conteúdo para evitar scripts. Como você faria essa validação?
  2. ✓ Resposta: Para validar SVG, além de verificar o MIME type (image/svg+xml), devemos verificar se o conteúdo não contém tags de script ou eventos como onload. Podemos usar DOMDocument para parsear o XML e remover elementos perigosos, ou simplesmente rejeitar se encontrar '<script>' ou 'on'. Exemplo:
    if ($mime === 'image/svg+xml') {
        $conteudo = file_get_contents($file['tmp_name']);
        if (preg_match('/
  3. Exercício 2: Crie uma função que retorne o tamanho formatado (ex: 1.5 MB) a partir do tamanho em bytes. Use-a para exibir o tamanho na listagem.
  4. ✓ Resposta:
    function formatarTamanho(int $bytes): string {
        $unidades = ['B', 'KB', 'MB', 'GB'];
        $i = 0;
        while ($bytes >= 1024 && $i < count($unidades) - 1) {
            $bytes /= 1024;
            $i++;
        }
        return round($bytes, 2) . ' ' . $unidades[$i];
    }
    // Uso: echo formatarTamanho(filesize($arquivo));
  5. Exercício 3: Implemente uma paginação simples na listagem, exibindo 12 imagens por página. Use o parâmetro page na URL.
  6. ✓ Resposta:
    $porPagina = 12;
    $paginaAtual = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
    $totalImagens = count($imagens);
    $totalPaginas = ceil($totalImagens / $porPagina);
    $offset = ($paginaAtual - 1) * $porPagina;
    $imagensPagina = array_slice($imagens, $offset, $porPagina);
    // Exibir links de paginação: anterior, números, próximo.
  7. Exercício 4: Escreva um script PHP que sirva uma imagem a partir de um diretório protegido, enviando os headers corretos e verificando se o arquivo existe.
  8. ✓ Resposta:
    $arquivo = $_GET['arquivo'] ?? '';
    $caminho = __DIR__ . '/uploads/' . basename($arquivo); // evitar path traversal
    if (file_exists($caminho)) {
        $mime = mime_content_type($caminho);
        header('Content-Type: ' . $mime);
        header('Content-Length: ' . filesize($caminho));
        readfile($caminho);
        exit;
    } else {
        http_response_code(404);
        echo 'Arquivo não encontrado';
    }
  9. Exercício 5: Adicione uma função para deletar uma imagem, garantindo que ela pertença ao diretório de uploads e que o usuário tenha permissão.
  10. ✓ Resposta:
    function deletarImagem(string $caminho): bool {
        $caminhoCompleto = __DIR__ . '/uploads/' . basename($caminho);
        if (file_exists($caminhoCompleto)) {
            return unlink($caminhoCompleto);
        }
        return false;
    }
    // Verificar permissões antes de chamar.