Docs›Geração de PDF (MadDoc)›Visão geral do MadDoc
Geração de PDF (MadDoc)

Visão geral do MadDoc

fromView, stream, save, unidades em mm.

O MadDoc é o pipeline de geração de PDF do MAD framework: você escreve uma view Blade declarativa com componentes <mad-doc-*> (<mad-doc-page>, <mad-doc-data-table>, <mad-doc-barcode>...), e Mad\Doc\MadDocPdf entrega os bytes do PDF prontos via DOMPDF. Carregamento de dados é 100% Eloquent — sem dependência de ORM legado.

Arquitetura

Dois compiladores rodam dentro de MadBlade::compileString(), ANTES do Blade padrão, convertendo as tags declarativas em Blade/PHP real:

TagCompiladorVira
<mad-doc-data-table> + <mad-doc-data-table-column> MadDocTableCompiler <x-doc-data-table :rows :columns>
<mad-doc-repeater> MadDocRepeaterCompiler @foreach + acumuladores → $totals

As demais tags <mad-doc-xxx> são apenas alias → <x-doc-xxx> (componentes Blade anônimos reais). O fluxo completo:

Controller ──► MadDocPdf::fromView('doc', $data)
                     │
                     ▼
            MadBlade::render('doc', $data)        (preprocessa <mad-doc-*>)
                     │  HTML completo (<!DOCTYPE…>)
                     ▼
            MadDocPdf::fromHtml($html, $opts)      (DOMPDF → bytes PDF)
fromView usa MadBlade, nunca view()

O helper view() do Laravel não roda o preprocessamento das tags <mad-doc-*> — elas saem literais no HTML, e o PDF sai quebrado. Use sempre MadDocPdf::fromView() (que chama Mad\View\MadBlade::render() por dentro):

// ERRADO - view() do Laravel nao preprocessa <mad-doc-*>, emite as tags literais no PDF
$html = view('docs.pedido-venda', ['record' => $record])->render();
$pdf  = MadDocPdf::fromHtml($html);

// CERTO - MadDocPdf::fromView usa Mad\View\MadBlade::render() por dentro
$pdf = MadDocPdf::fromView('docs.pedido-venda', ['record' => $record]);

API — Mad\Doc\MadDocPdf

MétodoQuando usar
fromHtml(string $html, array $opts = []): string HTML já renderizado (escape hatch — você mesmo montou a string)
fromView(string $view, array $data = [], array $opts = []): string Caminho padrão — renderiza a view via MadBlade e gera o PDF
Two-pass automático quando o documento usa {total}

counter(pages) do CSS não funciona no Dompdf 3.x. Então fromHtml() checa se o HTML contém o span <span class="mad-pages"></span> (emitido pelo token {total} do <mad-doc-page-number>): se contém, renderiza uma vez só pra contar as páginas (get_page_count(), mínimo 1), troca o span pelo número literal e renderiza de novo. {total} sai exato — ao custo de duas renders do documento. Sem o span, render única.

Ambos retornam bytes crus do PDF (string). Não existe stream() nem save() na classe — exibir inline, forçar download ou gravar em disco são responsabilidade do controller sobre esses bytes (ver § Stream vs salvar em disco).

Opções ($opts, repassadas ao DOMPDF)

ChaveDefaultDescrição
paper'A4'A4, A5, Letter, Legal…
orientation'portrait'portrait | landscape
default_font'DejaVu Sans'Fonte com suporte a acentuação
isRemoteEnabledfalseSegurança: off por padrão — imagens http(s):// são ignoradas. Só ligue se precisar buscar imagem remota
chrootstorage_path('app')Restringe de onde o DOMPDF lê imagens do disco
font_dir—Diretório de fontes customizadas

Essas seis chaves são as únicas lidas — qualquer outra opção do DOMPDF passada em $opts é ignorada silenciosamente. Duas ficam travadas no wrapper e não são configuráveis: isHtml5ParserEnabled = true e isPhpEnabled = false (nada de <script type="text/php"> dentro do documento).

Landscape precisa ser configurado nos dois lugares

paper/orientation em $opts controlam o tamanho REAL do papel no DOMPDF. O size/orientation do <mad-doc-page> controlam o CSS @page. Pra landscape sair correto, passe nos dois:

$pdf = MadDocPdf::fromView('docs.relatorio-amplo', [
    'record' => $record,
], [
    'orientation' => 'landscape',   // <- controla o papel REAL no DOMPDF
]);
<mad-doc-page size="A4" orientation="landscape">
    MAD__BLADE_COMMENT__2__
</mad-doc-page>

Controller padrão

Sem ORM legado — Eloquent gerencia a conexão. A view referencia $record (registro mestre) e $totals (bag compartilhado, ver Componentes mad-doc-*):

namespace App\Control\Docs;

use Mad\Doc\MadDocPdf;

class PedidoVendaDocument
{
    /** GET /docs/PedidoVendaDocument/{id} */
    public function show($id)
    {
        $record = \App\Models\PedidoVenda::findOrFail($id);

        // Bag compartilhado: data-table e repeater publicam totais nomeados
        // aqui. ArrayObject -> passado por referencia (mutacoes propagam
        // para fora do componente).
        $totals = new \ArrayObject();

        $pdf = MadDocPdf::fromView('docs.pedido-venda', [
            'record' => $record,
            'totals' => $totals,
        ]);

        return response($pdf, 200)
            ->header('Content-Type', 'application/pdf')
            ->header('Content-Disposition', 'inline; filename="pedido-' . $id . '.pdf"')
            ->header('Cache-Control', 'no-store');
    }
}

View — sempre dentro de <mad-doc-page>

Toda view MadDoc tem <mad-doc-page> como raiz — ele emite o <!DOCTYPE html> completo e os demais blocos vivem dentro, herdando fonte e cor por cascata CSS. Se a view vai usar totais (data-table com total-name, repeater com :aggregates), inicialize $totals logo no topo — o controller já manda um, mas a view fica independente se for chamada sem ele em algum teste:

@php $totals = $totals ?? new \ArrayObject(); @endphp
<mad-doc-page size="A4" orientation="portrait" margins="20,15,20,15">
    <mad-doc-heading level="1">Pedido #{!! \Mad\Doc\MadDocRuntime::inline($record->id) !!}</mad-doc-heading>
    MAD__BLADE_COMMENT__1__
</mad-doc-page>

Campos aninhados em colunas/fórmulas usam dot notation sempre (cliente.nome), nunca -> — ver Componentes mad-doc-* para a referência completa de cada tag.

Stream vs salvar em disco

fromView/fromHtml devolvem só os bytes — o que fazer com eles é decisão do controller, usando a API normal do Laravel:

// Visualizar embutido no navegador
return response($pdf, 200)
    ->header('Content-Type', 'application/pdf')
    ->header('Content-Disposition', 'inline; filename="pedido.pdf"');
// Forcar download
return response($pdf, 200)
    ->header('Content-Type', 'application/pdf')
    ->header('Content-Disposition', 'attachment; filename="pedido.pdf"');
use Illuminate\Support\Facades\Storage;

// Salvar no disco configurado (storage/app/... por padrao)
Storage::disk('local')->put('pedidos/pedido-' . $id . '.pdf', $pdf);

// Ou path absoluto direto
file_put_contents(storage_path('app/pedidos/pedido-' . $id . '.pdf'), $pdf);

Unidades: mm vs pt

Tamanhos físicos de layout (margens, dimensões de imagem/código/assinatura) são sempre em milímetros; tamanhos de fonte e traços finos são em pontos (pt) ou pixels (px) CSS. Misturar as duas escalas é a causa mais comum de "sumiu atrás do header fixo":

AtributoUnidadeOnde aparece
margins (page)mm<mad-doc-page>
height-mmmmspacer, header-band, footer-band
width-mmmmimage, dynamic-image, barcode, signature
size-mmmmqrcode
font-size / sizeptpage, text, variable-field, page-number
level1–4 (não é unidade física — mapeia internamente para 24/18/14/12pt)heading
thickness, margin-ypxhorizontal-line
cell-paddingpxdata-table
paddingptheader-band, footer-band
Header/footer fixo: o spacer precisa cobrir a banda inteira

header-band/footer-band usam position:fixed e sobrepõem o conteúdo. margins.top/margins.bottom da page (em mm) precisam ser maiores que height-mm da banda; e logo no início do corpo (e depois de cada <mad-doc-page-break />) coloque um <mad-doc-spacer height-mm="…" /> com altura igual ou maior que a banda — senão o texto abre por baixo dela. Detalhes completos em Componentes mad-doc-*.

Checklist rápido

  1. Controller: MadDocPdf::fromView('<view>', ['record' => $rec, 'totals' => new \ArrayObject()]).
  2. View: raiz <mad-doc-page>; nunca aninhe outra page dentro.
  3. Campos aninhados: sempre dot notation (cliente.nome) em field/fórmula/mask de data-table e variable-field.
  4. Linhas via model: :filter="fn($q) => $q->where(...)->limit(...)" (closure Eloquent), nunca um critério pré-montado.
  5. Landscape: orientation="landscape" no <mad-doc-page> e ['orientation' => 'landscape'] no $opts de fromView.
  6. Imagens remotas: bloqueadas por padrão (isRemoteEnabled=false). Prefira data URI/path local; só libere via $opts se precisar mesmo de HTTP(S).
  7. Nada de view() cru: só MadDocPdf::fromView (ou MadBlade::render + fromHtml manualmente).

Próximos