Docs›Formulários›Upload de arquivos
Formulários

Upload de arquivos

Discos mad_uploads/mad_tmp (Storage/Flysystem, MAD_UPLOAD_DISK), storage disk|db, mode comma|table + metadados, file-name.

O MAD oferece upload automático via 5 componentes de campo: <mad-file-field>, <mad-multi-file-field>, <mad-image-field>, <mad-signature-field> e <mad-avatar-field>. Configure storage no Blade, chame $form->save() no controller — o framework cuida de mover arquivos, gerar nome único, sanitizar o nome original, persistir os paths e relacionar com o registro.

Zero código de arquivo no controller

Nunca chame move_uploaded_file() ou manipule $_FILES manualmente. Declare storage + folder no Blade, chame $form->save($record), pronto — inclusive sanitização de nome, bloqueio de extensões executáveis e remoção do arquivo antigo na troca.

Modos de storage

ValorOnde grava
storage="disk"Disco de Filesystem do Laravel (Storage/Flysystem). Default mad_uploads — root storage/app/mad, fora de public/. A coluna do banco guarda a chave relativa (ex: uploads/contratos/x.pdf)
storage="db"BLOB base64 na própria coluna do banco, via prepared statement. Útil para arquivos pequenos ou ambientes sem filesystem persistente

Discos — 100% Storage/Flysystem

Nenhum caminho de escrita usa mais funções nativas de arquivo: todo I/O passa pela facade Storage do Laravel, por dois pontos únicos do framework.

ClasseDiscoRootPara quê
Mad\Service\MadUploadStorage mad_uploads (MadUploadStorage::LOCAL_DISK) storage/app/mad Uploads persistidos: campos storage="disk", documentos do GED, anexos de chat, modelos de importação
Mad\Service\MadScratchStorage mad_tmp (MadScratchStorage::DISK, fixo) storage/app/mad-tmp Scratch transiente: staging de $_FILES (mad_uploads/), import/, exports da grade (output/), blob/ do loadBlob()

O disco dos uploads persistidos é configurável — config('mad.uploads.disk'), alimentado pela env MAD_UPLOAD_DISK. Apontando para s3 (ou MinIO, qualquer driver Flysystem) o código é o MESMO e as chaves gravadas no banco continuam idênticas:

MAD_UPLOAD_DISK=s3
O scratch NUNCA segue o MAD_UPLOAD_DISK

mad_tmp é sempre local, por design: é arquivo de trabalho por-request/por-nó — mandar staging pro bucket só somaria latência e lixo remoto. E o arquivo servido nunca sai por URL direta de storage: o download passa sempre pelo app (rota autenticada/assinada, mad_download_url()), então o bucket pode — e deve — ser privado.

Sem fallback legado

outputDir e MadUploaderService foram removidos, e não existe fallback para o layout antigo de pastas na raiz do projeto: dados gravados por versões pré-Storage precisam de migração manual para dentro do disco mad_uploads.

Arquivo único — <mad-file-field>

Disk

<mad-file-field name="contrato" label="Contrato"
    storage="disk" folder="uploads/contratos"
    name-column="contrato_nome"
    accept=".pdf,.doc,.docx" max-size="10MB" />
// Controller — ZERO código de arquivo
$this->form->save($record);

// Resultado automático:
// $record->contrato      = 'uploads/contratos/<hash>_contrato.pdf'
// $record->contrato_nome = 'contrato.pdf'

DB (BLOB)

<mad-file-field name="conteudo" label="Arquivo"
    storage="db" name-column="nome_arquivo" />
// onEdit — extrai o BLOB para um arquivo temporário
public function onEdit(int $id): void
{
    $registro = Registro::findOrFail($id);
    $this->form->loadBlob($registro, 'conteudo', 'nome_arquivo');
    $this->form->fill($registro);
}

// onSave — o base64 vai para o banco automaticamente
$this->form->save($registro);

Múltiplos arquivos — <mad-multi-file-field>

Mode: comma (CSV numa coluna só)

<mad-multi-file-field name="anexos" label="Anexos"
    storage="disk" folder="uploads/docs" mode="comma"
    accept=".pdf,.doc" :max-files="5" />
$this->form->save($record);
// $record->anexos = 'uploads/docs/abc.pdf,uploads/docs/xyz.doc'

Mode: table (registro por arquivo, 1:N)

<mad-multi-file-field name="arquivos" label="Arquivos"
    storage="disk" folder="uploads/pedido" mode="table"
    model="PedidoAnexo" foreign-key="pedido_id"
    path-column="file_path" name-column="file_name"
    accept=".pdf,image/*" :max-files="10" />
$this->form->save($pedido);
// Cria N registros automaticamente em PedidoAnexo:
// PedidoAnexo { pedido_id: $pedido->id, file_path: 'uploads/pedido/xxx.pdf', file_name: 'doc.pdf' }
Prop obrigatória no mode="table"Descrição
modelClasse Eloquent da tabela filha
foreign-keyColuna FK que referencia o registro pai
path-columnColuna que guarda o caminho (disk) ou BLOB (db)
name-columnColuna que guarda o nome do arquivo

Metadados do arquivo no mode="table"

Além de FK/path/name, o INSERT da tabela filha preenche metadados — sem isso uma tabela de anexo com original_name/size/ mime_type/disk NOT NULL estourava SQLSTATE[23000]. Você declara a coluna explicitamente ou deixa a convenção resolver:

PropColuna por convençãoConteúdo
original-name-columnoriginal_nameNome ORIGINAL, sem sanitizar
size-columnsizeTamanho em bytes
mime-columnmime_typeMIME detectado
disk-columndiskDisco REAL usado (MadUploadStorage::diskName()) — não config('filesystems.default')
A convenção nunca inventa coluna

Sem prop explícita, o framework só preenche coluna que existe na tabela filha (Schema::getColumnListing, cache por classe) e que ainda está vazia — nunca cria coluna nem sobrescreve valor que você mesmo setou.

Estratégia do nome no disco — file-name

ValorNome gravado
prefix (default)<hash>_nome-sanitizado.pdf — hash de 8 bytes aleatórios
unique<hash>.pdf — descarta o nome original no disco
originalnome-sanitizado.pdf — colide se dois registros subirem o mesmo nome
record<pk>_nome-sanitizado.pdf — cai no hash quando o registro ainda não tem PK

O nome original (sem sanitizar) continua disponível na coluna de name-column/original-name-column — o nome no disco é sempre sanitizado e extensão executável é bloqueada.

Imagem — <mad-image-field> (crop, rotate, câmera)

<mad-image-field name="foto" label="Foto do produto"
    storage="disk" folder="uploads/fotos" name-column="foto_nome"
    crop aspect-ratio="1:1"
    accept="image/png,image/jpeg,image/webp"
    max-size="5MB" />
Cropper embutido

O componente abre um cropper interativo quando o usuário escolhe um arquivo. aspect-ratio aceita "1:1", "16:9", "4:3" ou livre (omitir o atributo). camera habilita captura direto da webcam/câmera do dispositivo.

Assinatura — <mad-signature-field>

<mad-signature-field name="assinatura" label="Assinatura"
    storage="disk" folder="uploads/assinaturas" name-column="assinatura_nome" />

Avatar — <mad-avatar-field>

Layout circular compacto para foto de perfil:

<mad-avatar-field name="avatar" label="Foto"
    storage="disk" folder="uploads/avatars"
    name-column="avatar_nome"
    accept="image/*" max-size="2MB" />

Validação

As mesmas regras nativas do illuminate/validation aplicam aos campos de arquivo — ver Validação para a referência completa:

public static function rules($id = null): array
{
    return [
        'foto'   => 'nullable|image|max:5120',   // 5MB
        'doc'    => 'required|file|max:10240',   // 10MB
        'planta' => 'nullable|file|max:51200',   // 50MB
    ];
}

Convivência com campos normais

<mad-form submit="onSave">
    <mad-image-field name="foto" label="Foto"
        storage="disk" folder="uploads/produtos"
        crop aspect-ratio="4:3" />

    <mad-input-field name="nome" label="Nome" required />

    <mad-multi-file-field name="anexos" label="Documentos"
        storage="disk" folder="uploads/produtos/docs" mode="table"
        model="ProdutoAnexo" foreign-key="produto_id"
        path-column="file_path" name-column="file_name"
        accept=".pdf,.doc,.docx" :max-files="5" />

    <mad-btn perm-action="onSave" type="submit" variant="primary" icon="save">Salvar</mad-btn>
</mad-form>

Um único $this->form->save($produto) persiste tudo: campos, foto, anexos.

NUNCA fazer

// ERRADO
move_uploaded_file($_FILES['anexo']['tmp_name'], $dest);
$record->anexo = $dest;
$record->save();

// CERTO — declarar storage no Blade, deixar o form->save() cuidar de tudo
// Blade: <mad-file-field name="anexo" storage="disk" folder="uploads" ... />
$this->form->save($record);

Próximos