Generar un PDF PROFESIONAL en Laravel o cualquier aplicación en PHP (CodeIgniter)
Una de las funcionalidades más útiles que puedes implementar en tus aplicaciones, sobre todo como las mías de venta de libros, es la de poder generar documentos PDF a partir de contenido HTML, este contenido HTML son mis libros, pero en tu caso, puedes ser cualquier cosa.
Sin embargo, si lo quieres hacer de la manera completa, como hice yo, para generar este contenido depende de estilos personalizados, interacciones en JavaScript para darle el formato que se necesita entre otros.
En este artículo te muestro cómo resolví ese problema combinando render en navegador y generación final de PDF desde el backend en Laravel usando DomPDF.
El problema
Quería generar un PDF desde una serie de bloques de contenido (llamémoslos "secciones de libro") que se visualizan y editan en una aplicación educativa. Cada sección está compuesta por HTML que se renderiza en el navegador con sus estilos y scripts. El objetivo era que ese mismo contenido se pudiera exportar como PDF con la apariencia final, respetando los estilos, estructuras y orden de las secciones.
La dificultad principal estaba en que si simplemente renderizaba una vista Blade y la pasaba a DomPDF, perdía muchas de las ventajas que podemos tener cuando mediante JS se da formato a el contenido HTML, en mi caso, el plugin de highlights para darle formato al código, por lo tanto, tuve que implementar una lógica adicional para renderizar el contenido HTML y guardarlo ya renderizado, ya listo y aplicado el estilo queda bellísimo.
Dividir el proceso en tres etapas
La clave para logar lo comentado, fue dividir el proceso en tres etapas muy claras:
1. Render del contenido en el navegador
Cada sección del libro es visualizada en el navegador. Este render permite aplicar lógica JavaScript, estilos dinámicos, componentes interactivos y todo lo que no se puede renderizar directamente desde el backend.
En esta etapa aprovecho todo el poder del navegador para dejar cada sección con su aspecto visual final. Una vez que se ve tal como quiero en pantalla, tomo el contenido HTML del contenedor principal y lo envío al backend en Laravel para guardarlo.
html.blade.php
@foreach ($book->sections()->where('orden', '>=', 1)->get() as $key => $s)
<section class="page-break book-chapter" id="{{ $s->id }}"
data-chapter="chapter{{ $key + 1 }}.xhtml">
<h1><span class="underline">{{ $book->language == 'spanish' ? 'Capítulo' : 'Chapter' }}
{{ $key + 1 }}</span>: {{ $s->title }}</h1>
{!! preg_replace('/\s*(width|height)="\d*"/i', '', $s->content) !!}
</section>
@endforeach
2. Guardado de secciones (book sections)
Cada bloque de HTML que representa una sección se guarda en base de datos. A cada sección le asigno un orden, para poder luego reconstruir el documento HTML completo en el orden correcto.
En esta etapa no intento generar el PDF aún. Solo me enfoco en asegurar que el contenido final —ya transformado y listo— quede guardado como HTML estático. Esto me permite desacoplar completamente el proceso de visualización del de exportación.
html.blade.php
// guarda el contenido del libro, cada seccion
document.documentElement.querySelectorAll(".book-chapter").forEach((s) => {
axios.post("{{ route('web-book-generate-section-save', 55) }}".replace(55, s.id), {
//"document": `${s.outerHTML}`
"document": `<html>${document.documentElement.querySelector("head").outerHTML}<body>${s.outerHTML}${document.documentElement.querySelector("footer").outerHTML}</body></html>`
})
// console.log(`${s.outerHTML}</body></html>`)
})
Además, tener las secciones guardadas por separado me permite hacer ajustes, debug o reordenar el contenido sin afectar otras partes del flujo.
3. Ensamblado y generación del PDF
Luego, con todo el contenido listo, uso el paquete de DomPDF en Laravel (aunque es un paquete para PHP), y se genera el archivo descargable.
$html = "<html>";
// obtiene el BODY/foreach de TODOS los capitulos
foreach ($this->book->sections()->where('orden', '>', 0)->orderBy('orden')->get() as $s) {
$sectionHtml = $s->content_render;
// Limpiar etiquetas <html>, <head>, <body> usando DOMDocument
$doc = new \DOMDocument();
@$doc->loadHTML(mb_convert_encoding($sectionHtml, 'HTML-ENTITIES', 'UTF-8'));
$body = $doc->getElementsByTagName('body')->item(0);
$content = $body ? $doc->saveHTML($body) : $sectionHtml;
// Elimina <body> tags dejando solo el contenido interior
$content = preg_replace('/<\/?body[^>]*>/', '', $content);
$html .= $content;
}
// cierra el doc HTML
$html .= '</body></html>';
//**** REEMPLAZOS del HTML
// DOOM PDF neceita que la url sea absoluta
$html = str_replace("/images/example/libros/", URL('/') . "/images/example/libros/", $html);
// elimino la referencia a chapterX.xhtml del formato epub
$html = preg_replace('/chapter\d+\.xhtml#/', '#', $html);
// dd($html );
$pdf = Pdf::loadHTML($html)
->set_option("isRemoteEnabled", true)
->set_option('isHtml5ParserEnabled', true)
->set_option('isPhpEnabled', true)
->setPaper('letter', 'portrait');
Este último paso, ya le das el formato que quieras, yo lo uso para reemplazar referencias relativas a absolutas para las imágenes, remover el la etiqueta HEAD y HTML de cada uno de las secciones, que las registro como un documento HTML completo, entre otros para finalmente exportarlo tal cual puedes ver en el código anterior.
Beneficios de este enfoque
Este flujo me dio varias ventajas claras:
- Puedo usar el navegador para renderizar contenido con JavaScript antes de guardarlo.
- Se respetan los estilos y estructuras reales que ve el usuario.
- COMPLETAMENTE personalizable en el estilo.
- No dependo de un motor de PDF para interpretar CSS complejo.
- El PDF es fiel a lo que ve el usuario, ya que se basa en HTML ya procesado.
Es fácil debuggear, ya que cada sección puede revisarse de forma individual.