Docs›Geração de PDF (MadDoc)›Exemplos práticos
Geração de PDF (MadDoc)

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>
Por que 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.

O rodapé usa {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.

Não existe formato de etiqueta contínua (rolo térmico)

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>

Próximos