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:
| Tag | Compilador | Vira |
|---|---|---|
<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étodo | Quando 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 |
{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)
| Chave | Default | Descrição |
|---|---|---|
paper | 'A4' | A4, A5, Letter, Legal… |
orientation | 'portrait' | portrait | landscape |
default_font | 'DejaVu Sans' | Fonte com suporte a acentuação |
isRemoteEnabled | false | Segurança: off por padrão — imagens http(s):// são ignoradas. Só ligue se precisar buscar imagem remota |
chroot | storage_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).
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":
| Atributo | Unidade | Onde aparece |
|---|---|---|
margins (page) | mm | <mad-doc-page> |
height-mm | mm | spacer, header-band, footer-band |
width-mm | mm | image, dynamic-image, barcode, signature |
size-mm | mm | qrcode |
font-size / size | pt | page, text, variable-field, page-number |
level | 1–4 (não é unidade física — mapeia internamente para 24/18/14/12pt) | heading |
thickness, margin-y | px | horizontal-line |
cell-padding | px | data-table |
padding | pt | header-band, footer-band |
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
- Controller:
MadDocPdf::fromView('<view>', ['record' => $rec, 'totals' => new \ArrayObject()]). - View: raiz
<mad-doc-page>; nunca aninhe outra page dentro. - Campos aninhados: sempre dot notation (
cliente.nome) emfield/fórmula/mask de data-table e variable-field. - Linhas via model:
:filter="fn($q) => $q->where(...)->limit(...)"(closure Eloquent), nunca um critério pré-montado. - Landscape:
orientation="landscape"no<mad-doc-page>e['orientation' => 'landscape']no$optsdefromView. - Imagens remotas: bloqueadas por padrão (
isRemoteEnabled=false). Prefira data URI/path local; só libere via$optsse precisar mesmo de HTTP(S). - Nada de
view()cru: sóMadDocPdf::fromView(ouMadBlade::render+fromHtmlmanualmente).