Crea PDFs estructurados y dinámicos con mPDF y Drupal

Published on
Tiempo de lectura: 9 min
––– visitas
thumbnail-image

En uno de mis proyectos recientes, se necesitaba generar PDFs en función de ciertos factores. Varios valores específicos debian pasarse desde la base de datos o una API y renderizarse en el PDF. En este post, explicaré cómo lo hice. Crearemos un módulo personalizado, un servicio para generar PDFs y la plantilla twig que servirá como nuestro html para el PDF.

mPDF

mPDF es una biblioteca PHP que te permite generar archivos PDF desde HTML. Es bastante util y gratuita. Puedes encontrar más información sobre ella aquí: https://mpdf.github.io/

Instalamos mPDF

Abre la línea de comandos, en la raíz de nuestra instalación de Drupal, ejecuta el comando que se muestra a continuación, para instalar mPDF.

composer require mpdf/mpdf

Creamos nuestro módulo personalizado

Una vez finalizado el tutorial, la estructura de nuestro módulo se verá así:

pdf_generator
├── pdf_generator.info.yml
├── pdf_generator.module
├── src
│   └── Service
│       └── PdfGenerator.php
└── templates
    └── pdf-generator
        ├── template-first-page.html.twig
        └── template-second-page.html.twig
  1. Creamos el archivo info
pdf_generator.info.yml
name: PDF Generator
description: 'PDF Generator'
type: module
core_version_requirement: ^8 || ^9
package: Custom
  • Si vas a la página de módulos, verás nuestro módulo listado allí.
  • Puedes habilitarlo a través de la CLI drush en pdf_generator o la UI.

Creamos el servicio

En este ejemplo, crearemos un servicio que se encargará de la generación de los PDFs; de esta manera, podemos llamar a este mismo desde cualquier parte de nuestro código.

  1. Ya que tengo una idea en mente de lo que quiero hacer, empezaré creando el servicio, luego inyectaré las dependencias y crearé el método.
src/Service/PdfGenerator.php

class PDFGenerator {

  private Environment $twig;

  public function __construct(Environment $twig) {
    $this->twig = $twig;
  }

  /**
   * The main method that will generate the PDF.
   * @param array $arr An associative array that will be passed to the twig template.
   * @throws \Twig\Error\RuntimeError
   * @throws \Twig\Error\LoaderError
   * @throws \Mpdf\MpdfException
   * @throws \Twig\Error\SyntaxError
   */
  public function generatePDF($arr): void {

   $mpdfConfig = [
      'mode'        => 'utf-8',
      'format'      => 'A5',
      'orientation' => 'L',
    ];

    $mpdf = new Mpdf($mpdfConfig);
  }

}
  1. Ahora, antes de continuar, necesitamos crear las plantillas twig que se utilizarán para renderizar el PDF. Así que crearemos una carpeta llamada templates en la raíz de nuestro módulo, y dentro de ella, crearemos dos archivos twig:
  • template-first-page.html.twig
  • template-second-page.html.twig
templates/pdf-generator/template-first-page.html.twig
<html>
<head>
    <title>My PDF Template</title>
    <style>
        @page :first {
            margin:0;
        }
    </style>
</head>
<body>

<p>{{ arr.first_name }}</p>
<p style="padding-right: 400px">
{{ arr.last_name }}
</p>
<p><strong>{{ arr.city }}</strong></p>
</body>
</html>
templates/pdf-generator/template-second-page.html.twig
<html>
<head>
    <title>My PDF Template</title>
</head>
<body>
<div className="heading">
</div>

<h1>{{ arr.YOUR_VARIABLE }}</h1>

</body>
</html>

En mi caso, pasaré una array asociativa, por lo que solo necesito registrar una variable en la render array. Necesitamos crear un archivo .module y agregar el siguiente código:

pdf_generator.module
<?php

/**
 * Implements hook_theme().
 */
function pdf_generator_theme($existing, $type, $theme, $path) {
  return [
    'template_first_page'       => [
      'variables' => [
        'arr' => NULL,
      ],
    ],
    'template_second_page'      => [
      'variables' => [
        'arr  '         => NULL,
      ],
    ],
  ];
}
No queremos un valor por defecto, por eso ponemos NULL.

Como puedes ver, es tan simple como crear la estructura HTML y agregar la variable que se pasará a la plantilla.

Ahora, volvamos a nuestro servicio. Como ya he dicho, quiero crear un PDF de 2 páginas. Se utilizarán diferentes plantillas para renderizar la primera y la segunda página. Vamos a ello.

src/Service/PdfGenerator.php
class PDFGenerator {

  private Environment $twig;

  // Inject the Twig environment and File System Interface in the constructor
  public function __construct(Environment $twig) {
    $this->twig = $twig;
  }

  /**
   * Generate PDFs based on the provided data.
   *
   * @param array $arr Data for generating PDF.
   *
   * @throws \Twig\Error\RuntimeError
   * @throws \Twig\Error\LoaderError
   * @throws \Mpdf\MpdfException
   * @throws \Twig\Error\SyntaxError
   */
  public function generatePDF(array $arr): void {
    // Configuration for Mpdf
    $mpdfConfig = [
      'mode'        => 'utf-8',
      'format'      => 'A5',
      'orientation' => 'L',
    ];

    // Initialize Mpdf with the specified configuration
    $mpdf = new Mpdf($mpdfConfig);

    // First Page
    $firstPageOptions = [
      'margin-left'   => '7',
      'margin-right'  => '7',
      'margin-top'    => '8',
      'margin-bottom' => '7',
    ];

    // Render the first-page template and add it to the PDF
    $firstPageRenderedHtml = $this->twig->render('@pdf_generator/template-first-page.html.twig',[]);
    $mpdf->AddPageByArray($firstPageOptions);
    $mpdf->WriteHTML($firstPageRenderedHtml);

    // Second Page
    $secondPageOptions = [
      'margin-left'   => '7',
      'margin-right'  => '7',
      'margin-top'    => '7',
      'margin-bottom' => '7',
    ];

    // Render the second-page template and add it to the PDF
    $secondPageRenderedHtml = $this->twig->render('@pdf_generator/template-second-page.html.twig', []);

    $mpdf->WriteHTML($secondPageRenderedHtml);

    // Export the PDF and force download it
    $mpdf->Output('MyAwesomePDF.pdf', 'D');
  }
}

Hemos hecho lo siguiente:

  • Hemos creado un método que generará el PDF.
  • Hemos configurado el PDF. Hemos establecido el formato en A5 y la orientación en horizontal.
  • Hemos creado una instancia de Mpdf y le hemos pasado la configuración.
  • Hemos creado una array con las opciones para la primera página. Hemos establecido los márgenes en 7mm.
  • Hemos renderizado y añadido la plantilla de la primera página al PDF.
  • Hemos creado una array con las opciones para la segunda página. Hemos establecido los márgenes en 7mm.
  • Hemos renderizado y añadido la plantilla de la segunda página al PDF.
  • Hemos exportado el PDF y forzado la descarga.

Ahora necesitamos pasar nuestra array a las plantillas. Así es como lo hacemos:

src/Service/PdfGenerator.php

// Modify this line in the generatePDF method
$firstPageRenderedHtml = $this->twig->render('@pdf_generator/template-first-page.html.twig',['arr' => $arr]);

// Modify this line in the generatePDF method
$secondPageRenderedHtml = $this->twig->render('@pdf_generator/template-second-page.html.twig', ['arr' => $arr]);

Creamos el archivo .service.yml

Nuestra ultima tarea es registrar nuestro servicio y declarar cualquier dependencia que usemos. Para ello, creamos un archivo .services.yml en la raíz de nuestro módulo y añadimos el siguiente código:

pdf_generator.services.yml
services:
  pdf_generator.pdf_generator:
    class: Drupal\pdf_generator\Service\PDFGenerator
    arguments: [ '@twig' ]

La clave arguments es donde pasamos las dependencias de nuestro servicio. En nuestro caso, solo necesitamos el servicio twig.

Llamamos al servicio

Ahora que tenemos nuestro servicio listo, podemos llamarlo desde cualquier parte de nuestro código. En mi caso, lo llamaré desde un módulo personalizado que he creado. Así que crearé un controlador y llamaré al servicio desde allí.

src/Controller/GeneratorController.php
class GeneratorController extends ControllerBase {

  private PDFGenerator $pdfGenerator;

  public function __construct(PDFGenerator $pdfGenerator) {
    $this->pdfGenerator = $pdfGenerator;
  }

    /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container
  ) {
    return new static(
      $container->get('pdf_generator.pdf_generator'),
    );
  }

  public function mpdf() {
    $arr = [
      'first_name' => 'John',
      'last_name'  => 'Doe',
      'city'       => 'New York',
    ];

    $this->pdfGenerator->generatePDF($arr);
  }
}

Y voila, tenemos nuestro PDF generado. Puedes llamar al servicio en cualquier parte de tu código: un controlador, un formulario, un bloque, un módulo personalizado, etc.

Opcional - Añadir una marca de agua y un hash al PDF

Pongamos que nuestro cliente tiene información confidencial que no quiere compartir o quiere añadir una marca de agua al PDF y un hash al pie de página. Así es como lo hacemos:

  1. Subimos la imagen que se utilizará como marca de agua. Creemos una carpeta en web/sites/default/files/pdf_generator y subimos la imagen. En mi caso, usaré una imagen png llamada watermark.png.
  2. Inyectamos la interfaz del sistema de archivos en el constructor de nuestro servicio.
  3. Obtenemos la ruta de la imagen y la pasamos al método SetWatermarkImage de mPDF.
  4. Establecemos el hash en el pie de página.
src/Service/PdfGenerator.php
class PDFGenerator {

  private Environment $twig;

  private FileSystemInterface $fileSystem;


  public function __construct(Environment $twig, FileSystemInterface $fileSystem) {
    $this->twig = $twig;
    $this->fileSystem = $fileSystem;
  }

  /**
   * @throws \Twig\Error\RuntimeError
   * @throws \Twig\Error\LoaderError
   * @throws \Mpdf\MpdfException
   * @throws \Twig\Error\SyntaxError
   */
  public function generatePDF($arr): void {
    $mpdfConfig = [
      'mode'        => 'utf-8',
      'format'      => 'A5',
      'orientation' => 'L',
    ];

    $mpdf = new Mpdf($mpdfConfig);

    //    Second page
    $firsPageOptions = [
      'margin-left'   => '7',
      'margin-right'  => '7',
      'margin-top'    => '8',
      'margin-bottom' => '7',
    ];

    // First Page
    $firstPageRenderedHtml = $this->twig->render('@pdf_generator/template-first-page.html.twig', ['arr' => $arr]);
    $mpdf->AddPageByArray($firsPageOptions);
    $mpdf->WriteHTML($firstPageRenderedHtml);
    $mpdf->SetHTMLFooter($arr['hash']);

    // Second Page
    $secondPageOptions = [
      'margin-left'   => '7',
      'margin-right'  => '7',
      'margin-top'    => '7',
      'margin-bottom' => '7',
    ];
    $mpdf->AddPageByArray($secondPageOptions);
    $secondPageRenderedHtml = $this->twig->render('@pdf_generator/template-second-page.html.twig', ['arr' => $arr]);


    // Get the image path
    $imagePath = $this->fileSystem->realpath('sites/default/files/pdf-generator/watermark.png'); // /var/www/html/drupal/web/sites/default/files/pdf-generator/watermark.png

    // Set the WaterMark
    $mpdf->SetWatermarkImage($imagePath, 1, 'P');
    $mpdf->SetWatermarkImage($imagePath, 1, 'P', [0, 0]);
    $mpdf->showWatermarkImage = TRUE;
    $mpdf->watermarkImgBehind = TRUE;
    $mpdf->WriteHTML($secondPageRenderedHtml);
    $mpdf->SetHTMLFooter($arr['hash']);

    // Export the PDF
    $mpdf->Output('MyAwesomePDF.pdf', 'D');
  }

}

Ahora que hemos inyectado la interfaz del sistema de archivos, necesitamos modificar nuestro archivo .services.yml.


pdf_generator.services.yml
services:
  pdf_generator.pdf_generator:
    class: Drupal\pdf_generator\Service\PDFGenerator
    arguments: [ '@twig', '@file_system']

¡Esto es todo por hoy! Espero que hayas encontrado útil este post. Si tienes alguna pregunta, por favor, contáctame en LinkedIn.

Happy coding!