Projeto: galeria com upload
LidoNesta aula, você aprenderá a construir uma galeria de imagens com upload seguro em PHP, cobrindo desde a validação e armazenamento de arquivos até a listagem e exibição das imagens. O projeto inclui boas práticas de segurança, como verificação de tipos MIME, geração de nomes aleatórios e proteção contra uploads maliciosos.
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
- PHP Manual - File Uploads
- PHP Manual - move_uploaded_file
- PHP Manual - finfo_file
- OWASP - Unrestricted File Upload
- MDN - <input type="file">
- Bootstrap 5.3 - Collapse
Exercícios
- Exercício 1: Modifique a função
uploadSeguropara aceitar também arquivos SVG, mas com uma validação de conteúdo para evitar scripts. Como você faria essa validação? - 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.
- Exercício 3: Implemente uma paginação simples na listagem, exibindo 12 imagens por página. Use o parâmetro
pagena URL. - 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.
- 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.
if ($mime === 'image/svg+xml') {
$conteudo = file_get_contents($file['tmp_name']);
if (preg_match('/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));$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.$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';
}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.