Exemplos práticos
Pedido, contrato, etiqueta, recibo.
Quatro documentos completos, do controller à view — cubra estes como ponto de partida e adapte modelos/campos pro seu domínio. Referência de cada componente em Componentes mad-doc-*; pipeline geral em Visão geral do MadDoc.
Pedido de venda — header/footer fixo, tabela com totais, barcode, QR
O exemplo mais completo: cabeçalho e rodapé fixos por página, tabela de itens agrupada por
categoria com fórmula calculada e totais publicados em $totals, código de
barras + QR Code, e uma segunda página com termos e assinaturas.
Controller
namespace App\Control\Docs;
use App\Models\PedidoVenda;
use Mad\Doc\MadDocPdf;
class PedidoVendaDocument
{
/** GET /docs/PedidoVendaDocument/{id} */
public function show($id)
{
$record = PedidoVenda::with(['cliente', 'vendedor'])->findOrFail($id);
$empresa = ['nome' => 'MAD Sistemas Ltda', 'cnpj' => '00.000.000/0001-00'];
$totals = new \ArrayObject();
$pdf = MadDocPdf::fromView('docs.pedido-venda', [
'record' => $record,
'empresa' => $empresa,
'totals' => $totals,
]);
return response($pdf, 200)
->header('Content-Type', 'application/pdf')
->header(
'Content-Disposition',
'inline; filename="pedido-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT) . '.pdf"'
);
}
}
View — resources/views/docs/pedido-venda.blade.php
@php
$totals = $totals ?? new \ArrayObject();
$itensCrit = fn ($q) => $q->where('pedido_id', $record->id)->orderBy('id');
@endphp
<mad-doc-page size="A4" orientation="portrait" margins="26,15,18,15"
font="DejaVu Sans" font-size="10">
<mad-doc-header-band height-mm="20" align="left" background="#1e3a8a" padding="5">
<table style="width:100%;border-collapse:collapse;color:#fff;font-size:9pt;">
<tr>
<td style="vertical-align:middle;">
<strong>{{ $empresa['nome'] }}</strong><br>
<span style="font-size:8pt;">CNPJ {{ $empresa['cnpj'] }}</span>
</td>
<td style="text-align:right;vertical-align:middle;">
<strong>Pedido #{{ str_pad((string) $record->id, 6, '0', STR_PAD_LEFT) }}</strong><br>
<span style="font-size:8pt;">{{ $record->dt_pedido?->format('d/m/Y') }}</span>
</td>
</tr>
</table>
</mad-doc-header-band>
<mad-doc-footer-band height-mm="11" align="center" background="#f8fafc" padding="3">
<table style="width:100%;border-collapse:collapse;font-size:8pt;color:#64748b;">
<tr>
<td style="text-align:left;">{{ $empresa['nome'] }}</td>
<td style="text-align:center;">
<mad-doc-page-number format="Página {page}" size="8" />
</td>
<td style="text-align:right;">Gerado em {{ now()->format('d/m/Y H:i') }}</td>
</tr>
</table>
</mad-doc-footer-band>
MAD__BLADE_COMMENT__1__
<mad-doc-spacer height-mm="22" />
<mad-doc-heading level="1" align="center" color="#1e3a8a">Pedido de Venda</mad-doc-heading>
<mad-doc-horizontal-line thickness="2" color="#1e3a8a" margin-y="4" />
MAD__BLADE_COMMENT__2__
<table style="width:100%;border-collapse:collapse;margin-bottom:8pt;"><tr>
<td style="width:60%;vertical-align:top;padding-right:8px;">
<mad-doc-variable-field :value="$record->cliente->nome" prefix="Cliente: " bold="true" size="11" />
<mad-doc-variable-field :value="$record->cliente->email" prefix="E-mail: " size="9" />
</td>
<td style="width:40%;vertical-align:top;">
<mad-doc-variable-field :value="$record->dt_pedido" format="date" prefix="Data: " align="right" bold="true" size="11" />
<mad-doc-variable-field :value="$record->vendedor->nome" prefix="Vendedor: " align="right" size="9" />
</td>
</tr></table>
MAD__BLADE_COMMENT__3__
<mad-doc-data-table
model="PedidoVendaItem" :filter="$itensCrit" :totals="$totals"
group-by="categoria"
header-bg="#1e3a8a" zebra="true" borders="true" font-size="9" cell-padding="5">
<mad-doc-data-table-column label="Produto" field="produto.nome" align="left" />
<mad-doc-data-table-column label="Qtd" field="quantidade" align="right" formatter="integer"
total="sum" total-name="total_qtd" width="60px" />
<mad-doc-data-table-column label="Vlr Unit." field="valor_unit" align="right" formatter="money" width="90px" />
<mad-doc-data-table-column label="Subtotal"
formula="{quantidade}*{valor_unit}" field-name="subtotal"
align="right" formatter="money"
total="sum" total-name="valor_total" width="100px" />
</mad-doc-data-table>
<mad-doc-variable-field :value="$totals['valor_total'] ?? ''" format="text"
prefix="TOTAL DO PEDIDO: " align="right" size="14" bold="true" />
<mad-doc-spacer height-mm="3" />
<mad-doc-heading level="3" color="#334155">Observações</mad-doc-heading>
<mad-doc-text size="9" color="#475569">{{ $record->obs ?: 'Sem observações adicionais.' }}</mad-doc-text>
<mad-doc-spacer height-mm="4" />
<table style="width:100%;border-collapse:collapse;"><tr>
<td style="width:50%;text-align:center;vertical-align:middle;">
<mad-doc-barcode :data="'PV' . str_pad((string) $record->id, 10, '0', STR_PAD_LEFT)"
format="C128" width-mm="80" height-mm="18" show-text="true" align="center" />
</td>
<td style="width:50%;text-align:center;vertical-align:middle;">
<mad-doc-qrcode :data="route('pedidos.show', $record->id)"
size-mm="28" ec-level="M" align="center" />
</td>
</tr></table>
MAD__BLADE_COMMENT__4__
<mad-doc-page-break />
<mad-doc-spacer height-mm="22" /> MAD__BLADE_COMMENT__5__
<mad-doc-heading level="2" color="#1e3a8a">Termos e Condições</mad-doc-heading>
<mad-doc-text align="justify" size="9" color="#334155">
Este pedido será processado conforme as condições acordadas. Prazo de entrega
e formas de pagamento estão sujeitos à confirmação de disponibilidade de estoque.
</mad-doc-text>
<mad-doc-spacer height-mm="36" />
<table style="width:100%;border-collapse:collapse;"><tr>
<td style="width:50%;text-align:center;">
<mad-doc-signature width-mm="80" align="center" :name="$record->cliente->nome" role="Cliente" />
</td>
<td style="width:50%;text-align:center;">
<mad-doc-signature width-mm="80" align="center" :name="$record->vendedor->nome" role="Vendedor" />
</td>
</tr></table>
</mad-doc-page>
margins top é 26 e o spacer é 22?
O header-band tem height-mm="20" + padding="5" (≈ 1,8mm
convertidos de pt). A margem superior da page precisa ser maior que isso (26mm dá
folga), e o spacer logo no início do corpo precisa cobrir pelo menos os 20mm da
banda — 22mm aqui dá uma margem de segurança. Ver
unidades mm vs pt.
{page} sozinho de propósito
format="Página {page} de {total}" também funciona e o
{total} sai exato — MadDocPdf::fromHtml()
detecta o span mad-pages e faz two-pass (renderiza uma vez pra contar as
páginas, depois de novo com o número literal). O preço é dobrar o custo de
render; num pedido de 2 páginas é irrelevante, num relatório de centenas
pesa — por isso o exemplo fica só no {page}.
Recibo — documento de 1 página sem tabela
O caso mais simples: nenhum header/footer fixo, nenhuma tabela — só
mad-doc-variable-field e mad-doc-signature. Bom ponto de partida
pra qualquer documento "1 registro, sem lista".
Controller
namespace App\Control\Docs;
use App\Models\Recebimento;
use Mad\Doc\MadDocPdf;
class ReciboDocument
{
/** GET /docs/ReciboDocument/{id} */
public function show($id)
{
$record = Recebimento::with('cliente')->findOrFail($id);
$pdf = MadDocPdf::fromView('docs.recibo', ['record' => $record]);
return response($pdf, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'inline; filename="recibo-' . $id . '.pdf"');
}
}
View — resources/views/docs/recibo.blade.php
<mad-doc-page size="A4" orientation="portrait" margins="30,25,30,25" font-size="11">
<mad-doc-heading level="1" align="center" color="#1e3a8a">Recibo</mad-doc-heading>
<mad-doc-spacer height-mm="6" />
<mad-doc-text size="11" color="#111">
Recebi de <strong>{{ $record->cliente->nome }}</strong> a quantia de:
</mad-doc-text>
<mad-doc-variable-field :value="$record->valor" format="money" prefix="R$ "
align="center" size="22" bold="true" />
<mad-doc-text align="justify" size="10" color="#334155">
Referente a: {{ $record->referente_a }}.
</mad-doc-text>
<mad-doc-spacer height-mm="10" />
<table style="width:100%;border-collapse:collapse;"><tr>
<td style="width:50%;"><strong>Forma de pagamento:</strong> {{ $record->forma_pagamento }}</td>
<td style="width:50%;text-align:right;">
<mad-doc-variable-field :value="$record->dt_recebimento" format="date" prefix="Data: " align="right" />
</td>
</tr></table>
<mad-doc-spacer height-mm="30" />
<div style="text-align:center;">
<mad-doc-signature width-mm="90" align="center" :name="$record->cliente->nome" role="Recebedor" />
</div>
</mad-doc-page>
Contrato — repeater de cláusulas, watermark, testemunhas
Cláusulas não são tabulares (cada uma é um parágrafo livre), então o documento usa
mad-doc-repeater em vez de mad-doc-data-table. O
watermark-text="MINUTA" marca visualmente que é uma versão de rascunho — troque
pra string vazia (ou remova o atributo) na versão final assinada.
Controller
namespace App\Control\Docs;
use App\Models\Contrato;
use Mad\Doc\MadDocPdf;
class ContratoDocument
{
/** GET /docs/ContratoDocument/{id} */
public function show($id)
{
$record = Contrato::with(['contratante', 'contratada', 'clausulas'])->findOrFail($id);
$totals = new \ArrayObject();
$pdf = MadDocPdf::fromView('docs.contrato', [
'record' => $record,
'totals' => $totals,
]);
return response($pdf, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'inline; filename="contrato-' . $id . '.pdf"');
}
}
View — resources/views/docs/contrato.blade.php
@php
$totals = $totals ?? new \ArrayObject();
$clausulasCrit = fn ($q) => $q->where('contrato_id', $record->id)->orderBy('numero');
@endphp
<mad-doc-page size="A4" orientation="portrait" margins="20,18,20,18"
watermark-text="MINUTA" watermark-opacity="0.08" watermark-angle="-30">
<mad-doc-heading level="1" align="center">Contrato de Prestação de Serviços</mad-doc-heading>
<mad-doc-text align="center" size="9" color="#6b7280">Contrato Nº {{ $record->id }}</mad-doc-text>
<mad-doc-horizontal-line margin-y="8" />
<mad-doc-text align="justify" size="10">
Pelo presente instrumento, de um lado <strong>{{ $record->contratante->nome }}</strong>
("CONTRATANTE") e, de outro, <strong>{{ $record->contratada->nome }}</strong>
("CONTRATADA"), têm entre si justo e contratado o que segue.
</mad-doc-text>
<mad-doc-spacer height-mm="4" />
MAD__BLADE_COMMENT__6__
<mad-doc-repeater
model="ContratoClausula" :filter="$clausulasCrit"
:aggregates="['total_clausulas' => ['op' => 'count']]"
:empty="'<p>Nenhuma cláusula cadastrada.</p>'">
<mad-doc-text align="justify" size="9" color="#1f2937">
<strong>Cláusula {{ $record->numero }}ª.</strong> {{ $record->texto }}
</mad-doc-text>
<mad-doc-spacer height-mm="2" />
</mad-doc-repeater>
<mad-doc-spacer height-mm="2" />
<mad-doc-text size="8" color="#9ca3af">
Total de {{ $totals['total_clausulas'] ?? 0 }} cláusulas.
</mad-doc-text>
MAD__BLADE_COMMENT__7__
<mad-doc-page-break />
<mad-doc-text align="justify" size="9">
E por estarem assim justas e contratadas, firmam o presente em duas vias de igual teor.
</mad-doc-text>
<mad-doc-spacer height-mm="30" />
<table style="width:100%;border-collapse:collapse;"><tr>
<td style="width:50%;text-align:center;">
<mad-doc-signature width-mm="80" align="center" :name="$record->contratante->nome" role="Contratante" />
</td>
<td style="width:50%;text-align:center;">
<mad-doc-signature width-mm="80" align="center" :name="$record->contratada->nome" role="Contratada" />
</td>
</tr></table>
<mad-doc-spacer height-mm="14" />
<table style="width:100%;border-collapse:collapse;"><tr>
<td style="width:50%;text-align:center;">
<mad-doc-signature width-mm="70" align="center" role="Testemunha 1" />
</td>
<td style="width:50%;text-align:center;">
<mad-doc-signature width-mm="70" align="center" role="Testemunha 2" />
</td>
</tr></table>
</mad-doc-page>
:aggregates com op: count não precisa de field
['total_clausulas' => ['op' => 'count']] conta as linhas do
repeater independente de qualquer campo — é o único agregado onde field
é opcional.
Etiqueta de produto — layout compacto, código de barras + QR
Para produtos físicos: nome, código de barras EAN-13, QR e preço em destaque, num formato pequeno o bastante pra colar numa embalagem.
mad-doc-page só aceita size A4, A5, Letter ou Legal — não
há suporte a dimensões customizadas (ex: 100×50mm de impressora térmica de rolo).
O exemplo abaixo usa A5 paisagem como o menor formato fixo disponível, pensado pra
impressão em folha comum e recorte manual, ou para gerar 1 PDF por etiqueta e
combinar várias numa ferramenta de impostação externa. Se o caso de uso exige rolo
térmico de verdade, isso fica fora do escopo do MadDoc atual.
Controller
namespace App\Control\Docs;
use App\Models\Produto;
use Mad\Doc\MadDocPdf;
class EtiquetaProdutoDocument
{
/** GET /docs/EtiquetaProdutoDocument/{id} */
public function show($id)
{
$record = Produto::findOrFail($id);
$pdf = MadDocPdf::fromView('docs.etiqueta-produto', [
'record' => $record,
], [
'paper' => 'A5',
'orientation' => 'landscape',
]);
return response($pdf, 200)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'inline; filename="etiqueta-' . $id . '.pdf"');
}
}
O paper/orientation em $opts precisa casar com size/orientation do mad-doc-page (ver landscape nos dois lugares):
// paper/orientation em $opts (DOMPDF) precisam casar com size/orientation
// do mad-doc-page — A5 é o menor formato fixo suportado.
View — resources/views/docs/etiqueta-produto.blade.php
<mad-doc-page size="A5" orientation="landscape" margins="8,8,8,8" font-size="10">
<mad-doc-heading level="2" align="center" color="#111827">{{ $record->nome }}</mad-doc-heading>
<table style="width:100%;border-collapse:collapse;"><tr>
<td style="width:55%;vertical-align:middle;text-align:center;">
<mad-doc-barcode :data="$record->codigo_barras" format="EAN13"
width-mm="65" height-mm="20" show-text="true" align="center" />
</td>
<td style="width:45%;vertical-align:middle;text-align:center;">
<mad-doc-qrcode :data="$record->codigo_barras" size-mm="26" ec-level="M" align="center" />
</td>
</tr></table>
<mad-doc-spacer height-mm="4" />
<mad-doc-variable-field :value="$record->preco" format="money" prefix="R$ "
align="center" size="24" bold="true" />
</mad-doc-page>