Create structured and dynamic PDFs using mPDF and Drupal

Published on
9 mins read
––– views
thumbnail-image

In one of my projects recently, the client needed to generate PDFs depending on certain factors. So, specific values must be passed from the database or an API and rendered on the PDF.

In this post, I aim to explain how i did it. We'll create a custom module, a service to generate PDFs and the twig template which will serve as our html for the PDF.

mPDF

First, mPDF is a PHP Library that allows you to generate PDF files from HTML. It's a great library, and it's free. You can find more information about it here: https://mpdf.github.io/

Installing mPDF

Open up your command line; at the root of our Drupal installation, run the following command:

composer require mpdf/mpdf

This will install the mPDF library in our vendor folder.

Creating our custom module structure

By the end of this tutorial, the structure of our module will look like this:

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. We start by creating the info file.
pdf_generator.info.yml
name: PDF Generator
description: 'PDF Generator'
type: module
core_version_requirement: ^8 || ^9
package: Custom
  • If you navigate the modules page, you'll see our module listed there.
  • You can enable it via the cli drush en pdf_generator or the UI.

Creating the service

In this example, we'll create a Service that will handle the generation of the PDFs; this way, we can call the service from other modules.

  1. Since I already have an idea of what I want to do, I'll start by creating the service, then inject the dependencies and create the method.
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. Now, before we continue, we need to create the twig templates that will be used to render the PDF. So we'll create a folder called templates in the root of our module, and inside it, we'll create two twig files:
  • 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-first-page.html.twig
<html>
<head>
    <title>My PDF Template</title>
</head>
<body>
<div className="heading">
</div>

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

</body>
</html>

In my case, I'll pass one associative array, so I only need to register one variable in the render array. We need to create a .module file and add the following code:

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,
      ],
    ],
  ];
}

We don't want a default value for the variable, so we set its value to NULL.


As you can see, it's as simple as creating the HTML structure and adding the variable that will be passed to the template.

Now, let's get back to our service. As I've already said, I want to create a 2-page PDF. Different templates will be used to render the first and second pages. Let's get to it.

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');
  }
}

So what we've done here is the following:

  • We've created a method that will generate the PDF.
  • We've set up the configuration for the PDF. We've set the format to A5 and the orientation to Landscape.
  • We've created an instance of Mpdf and passed the configuration to it.
  • We've created an array with the options for the first page. We've set the margins to 7mm.
  • We've rendered and added the first-page template to the PDF.
  • We've created an array with the options for the second page. We've set the margins to 7mm.
  • We've rendered and added the second-page template to the PDF.
  • We've exported the PDF and forced the download.

Now, we need to pass our array to the templates. Here's how we do it:

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]);

Creating our .service.yml file

Our final step is to register our service and declare any dependencies used. We do this by creating a .services.yml file in the root of our module and adding the following code:

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

The arguments key is where we pass the dependencies of our service. In our case, we only need the twig service.

Calling the service

Now that we have our service ready, we can call it from anywhere in our code. In my case, I'll call it from a custom module that I've created. So, I'll create a controller and call the service from there.

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);
  }
}

And voila, we have our PDF generated. You can call the service anywhere in your code: a controller, a form, a block, a custom module, etc.

Let's say your client has confidential information he doesn't want to share. You can add a watermark to the PDF and a hash to the footer. Here's how you do it:

  1. First, we need to upload the image that will be used as a watermark. Let's create a folder in web/sites/default/files/pdf_generator and upload the image. In my case, I'll use a png image called watermark.png.
  2. Inject the file system interface in the constructor of our service.
  3. Get the path of the image and pass it to the SetWatermarkImage method of mPDF.
  4. Set the hash in the footer
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');
  }

}

Now that we've injected the file system interface, we need to modify our .services.yml file.


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

And That's it for today! I hope you found this helpful post. If you have any questions, please contact me on LinkedIn.

Happy coding!