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.
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
| Valor | Onde 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.
| Classe | Disco | Root | Para 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
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.
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 |
|---|---|
model | Classe Eloquent da tabela filha |
foreign-key | Coluna FK que referencia o registro pai |
path-column | Coluna que guarda o caminho (disk) ou BLOB (db) |
name-column | Coluna 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:
| Prop | Coluna por convenção | Conteúdo |
|---|---|---|
original-name-column | original_name | Nome ORIGINAL, sem sanitizar |
size-column | size | Tamanho em bytes |
mime-column | mime_type | MIME detectado |
disk-column | disk | Disco REAL usado (MadUploadStorage::diskName()) — não config('filesystems.default') |
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
| Valor | Nome gravado |
|---|---|
prefix (default) | <hash>_nome-sanitizado.pdf — hash de 8 bytes aleatórios |
unique | <hash>.pdf — descarta o nome original no disco |
original | nome-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" />
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);