Skip to content

Tudo para WordPress, desenvolvimento web — e não só

🚀 Carregar vários ficheiros com HTML e PHP

🚀 Carregar vários ficheiros com HTML e PHP

Um utilizador precisa de carregar cinco imagens, mas o formulário só aceita uma. Ou um cliente pede carregamentos de documentos em lote com um título associado a cada ficheiro. Isto não é fantasia: é uma tarefa padrão que o PHP resolve em trinta linhas.

O problema é que a maioria dos tutoriais mostra carregamentos de um único ficheiro. O carregamento múltiplo funciona de forma diferente: um array de ficheiros em vez de um, mapeamento para campos adicionais, proteção contra sobrescrita. Abaixo encontra uma implementação funcional, do zero até um trecho de código pronto a usar.

💡 Visão geral rápida:

  • Crie um formulário HTML com enctype="multipart/form-data" e um campo file com o atributo multiple.
  • Processe o array $_FILES em PHP: percorra os índices e utilize move_uploaded_file() para cada ficheiro.
  • Adicione validação de extensão e tamanho antes de gravar em disco.
  • Para associar "ficheiro + título", utilize arrays paralelos title[] e fileUpload[].

Um vídeo rápido que aborda o mesmo tópico, desde o formulário HTML até gravar ficheiros numa base de dados:

Passo 1: Formulário HTML para carregar vários ficheiros

Primeiro, construa o formulário. Pontos-chave: o atributo enctype definido como multipart/form-data e um campo de entrada com multiple. Sem o enctype, o servidor simplesmente não receberá ficheiros; isto não é um erro do PHP, mas da especificação HTTP.

Dê ao campo o nome de um array: fileUpload[]. Os parênteses retos no final indicam ao PHP que o servidor está a receber uma lista, e não um único ficheiro.

1<form action="" method="post" enctype="multipart/form-data">
2 <label>Select files:</label>
3 <input type="file" name="fileUpload[]" multiple>
4 <input type="submit" name="Submit" value="Upload">
5</form>

O formulário acima permite selecionar qualquer número de ficheiros de uma só vez na janela de seleção. O navegador irá empacotá-los num pedido multipart e enviá-los para o servidor. Não é necessário JavaScript nesta fase.

Passo 2: Processamento PHP do array de ficheiros

Quando o formulário é submetido ao servidor, o PHP preenche o array superglobal $_FILES. Mas a estrutura difere dos carregamentos de ficheiro único: as chaves name / tmp_name / size / error tornam-se arrays onde o índice corresponde ao número sequencial do ficheiro.

Percorremos os índices e guardamos cada ficheiro usando move_uploaded_file():

1<?php
2
3$target_dir = 'uploads/';
4
5if (isset($_FILES['fileUpload']['name'])) {
6
7 $total_files = count($_FILES['fileUpload']['name']);
8
9 for ($key = 0; $key < $total_files; $key++) {
10
11 // Skip empty fields (no file selected)
12 if (isset($_FILES['fileUpload']['name'][$key])
13 && $_FILES['fileUpload']['size'][$key] > 0) {
14
15 $original_filename = $_FILES['fileUpload']['name'][$key];
16 $target = $target_dir . basename($original_filename);
17 $tmp = $_FILES['fileUpload']['tmp_name'][$key];
18
19 move_uploaded_file($tmp, $target);
20 }
21 }
22}

Adicione este código ao functions.php do seu tema filho ou ao plugin Code Snippets, e o formulário do passo 1 começará a aceitar ficheiros.

Note que basename() remove o caminho do nome do ficheiro. Isto protege contra ataques de path traversal: um atacante poderia enviar ../../wp-config.php como nome. O basename() deixa apenas wp-config.php, mas a próxima camada de defesa deve continuar a ser a validação da extensão.

Passo 3: Verificar o tipo e o tamanho do ficheiro

Não pode aceitar "tal como vem" tudo o que o navegador envia. Proteção mínima: uma lista branca de extensões e um limite de tamanho.

Verificação da extensão usando pathinfo() com a flag PATHINFO_EXTENSION. Converta para minúsculas e compare com as extensões permitidas:

1$allowed_ext = array('jpg', 'jpeg', 'png', 'gif', 'bmp', 'pdf', 'doc', 'docx');
2$ext = strtolower(pathinfo($_FILES['fileUpload']['name'][$key], PATHINFO_EXTENSION));
3
4if (!in_array($ext, $allowed_ext)) {
5 $errors[$key] = 'Invalid file type: ' . $ext;
6 continue;
7}

A verificação do tamanho é ainda mais simples; o valor size em bytes já está em $_FILES:

1$max_file_size = 5 * 1024 * 1024; // 5 MB
2
3if ($_FILES['fileUpload']['size'][$key] > $max_file_size) {
4 $errors[$key] = 'File exceeds the allowed size (5 MB)';
5 continue;
6}

Coloque ambas as verificações antes de move_uploaded_file() no mesmo ciclo. Os ficheiros que falharem a validação não chegarão ao disco. Recolha os erros num array e, após o processamento, mostre ao utilizador uma lista indicando qual o ficheiro que falhou e porquê. Sem isto, os utilizadores verão apenas um resultado vazio e irão embora, assumindo que o formulário está avariado. Para cada ficheiro rejeitado, indique o nome e o motivo, como "foto.png: tipo inválido" ou "arquivo.zip: tamanho excedido".

Passo 4: Renomear ficheiros durante o carregamento

Se dois utilizadores carregarem photo.jpg, o segundo ficheiro irá sobrescrever o primeiro. A solução é gerar um nome único ao guardar. A abordagem mais simples: substituir espaços por underscores e adicionar time().

1if (isset($_FILES['fileUpload']['name'][$key])
2 && $_FILES['fileUpload']['size'][$key] > 0) {
3
4 $original_filename = $_FILES['fileUpload']['name'][$key];
5
6 // Extension separately
7 $ext = pathinfo($original_filename, PATHINFO_EXTENSION);
8
9 // Name without extension
10 $filename_without_ext = basename($original_filename, '.' . $ext);
11
12 // New name: no spaces + timestamp
13 $new_filename = str_replace(' ', '_', $filename_without_ext)
14 . '_' . time() . '.' . $ext;
15
16 move_uploaded_file(
17 $_FILES['fileUpload']['tmp_name'][$key],
18 $target_dir . $new_filename
19 );
20}

O timestamp time() fornece unicidade ao segundo. Em projetos de alto tráfego, substitua-o por uniqid() ou UUID: dois pedidos simultâneos no mesmo segundo são suficientes para causar uma colisão e perder um dos ficheiros. Além disso, time() revela a data de carregamento no nome do ficheiro, o que por vezes é indesejável. Um UUID do ramsey/uuid elimina completamente ambos os problemas.

Passo 5: Carregamento com campos adicionais para cada ficheiro

Por vezes, precisa de anexar um título, descrição ou categoria a cada ficheiro. Em vez de um par "título + ficheiro", cria vários, dando aos campos o nome de arrays:

1<form action="" method="post" enctype="multipart/form-data">
2
3 <input type="text" name="title[]" placeholder="Title">
4 <input type="file" name="fileUpload[]">
5
6 <input type="text" name="title[]" placeholder="Title">
7 <input type="file" name="fileUpload[]">
8
9 <input type="text" name="title[]" placeholder="Title">
10 <input type="file" name="fileUpload[]">
11
12 <input type="submit" name="Submit" value="Upload">
13
14</form>
Formulário de carregamento com campos de título adicionais

Do lado do servidor, $_POST['title'] e $_FILES['fileUpload']['name'] são arrays paralelos. O índice 0 num corresponde ao índice 0 no outro. O processamento usa o mesmo ciclo, mas agora com associação de título:

1$total_files = count($_FILES['fileUpload']['name']);
2
3for ($key = 0; $key < $total_files; $key++) {
4
5 if ($_FILES['fileUpload']['size'][$key] === 0) {
6 continue; // empty field — skip
7 }
8
9 $title = isset($_POST['title'][$key]) ? $_POST['title'][$key] : '';
10 $tmp = $_FILES['fileUpload']['tmp_name'][$key];
11 $fname = basename($_FILES['fileUpload']['name'][$key]);
12 $target = $target_dir . time() . '_' . $fname;
13
14 if (move_uploaded_file($tmp, $target)) {
15 // Save $title and $target to the database
16 // or into an array for further processing
17 }
18}

Certifique-se de que o número de campos title[] corresponde ao número de campos fileUpload[] no formulário. Se o utilizador adicionar campos dinamicamente via JavaScript, todos os índices devem ser consecutivos, sem falhas. O PHP não colapsa automaticamente arrays esparsos, pelo que $_POST['title'][5] pode existir enquanto $_FILES['fileUpload']['name'][5] não.

Na prática, esta abordagem cobre uma boa metade das tarefas de clientes: galerias de imagens com legendas, carregamentos de documentos com etiquetas, submissões de currículos com portefólios. O mesmo princípio de arrays paralelos funciona para descrição, categoria, ordem e quaisquer outros campos.

⁉️🤔 Perguntas frequentes

Porque é que o ficheiro não aparece no $_FILES apesar de o formulário ter sido submetido?

Nove em cada dez vezes, falta o enctype="multipart/form-data" na tag <form>. Sem ele, o navegador envia os dados como texto simples e o PHP não preenche $_FILES. Verifique também post_max_size e upload_max_filesize no php.ini; exceder qualquer um deles resulta num array vazio, sem erros visíveis.

Como limito o tamanho total de carregamento para um único pedido?

A diretiva post_max_size no php.ini trunca todo o corpo do pedido POST, incluindo ficheiros e campos de texto. O valor padrão, de acordo com a documentação do PHP, é de 8 MB. Defina-o mais alto do que upload_max_filesize, com uma margem para a codificação multipart (cerca de 20%). Para carregamentos em massa, aumente ambos os parâmetros: upload_max_filesize = 20M e post_max_size = 25M.

Os ficheiros podem ser carregados de forma assíncrona via JavaScript?

Sim. Use FormData e fetch com method: 'POST'. Adicione campos fileUpload[] usando formData.append('fileUpload[]', file) para cada ficheiro selecionado. Não defina o cabeçalho Content-Type manualmente; o navegador definirá automaticamente multipart/form-data com o boundary correto. Para feedback visual, use uma barra de progresso via XMLHttpRequest.upload.onprogress, uma vez que fetch não expõe o progresso diretamente.

E quanto à segurança: um atacante carrega um script PHP em vez de uma imagem?

A verificação de extensão do passo 3 é a primeira linha de defesa. Segunda: armazene os ficheiros carregados FORA da pasta raiz do site ou num diretório com um .htaccess que desative a execução (php_flag engine off). Terceira: renomeie os ficheiros ao guardar (passo 4); mesmo que shell.php passe o filtro de extensão, torna-se shell_1718300000.php, o que não é perigoso sem permissões de execução. Quarta: verifique o tipo MIME não a partir de $_FILES['type'] (enviado pelo navegador, facilmente falsificável), mas via finfo (Fileinfo integrado do PHP), que lê a assinatura do ficheiro.

O que esta abordagem proporciona: o quadro completo

Construiu um sistema de carregamento que lida com três cenários de imediato: ficheiros em lote com um botão, carregamento único com validação e emparelhamento "ficheiro + metadados" através de arrays paralelos. O código tem cerca de trinta linhas para cada passo, nada mais.

Se estiver a trabalhar em WordPress, coloque o manipulador no functions.php do seu tema filho ou no plugin Code Snippets. Em PHP puro, basta colocar o script de processamento no destino da ação do formulário.

O código deste artigo cobre a maioria das tarefas típicas de carregamento. Os cenários restantes (carregamento em partes para ficheiros grandes, carregamento direto para armazenamento compatível com S3 e arrastar e largar com pré-visualização) serão abordados em artigos separados. Escreva nos comentários qual deles é relevante para o seu projeto, e daremos prioridade a esse.