
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
- We start by creating the info file.
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.
- 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.
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);
}
}
- 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
<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>
<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:
<?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.
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:
// 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:
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.
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.
Optional - Adding a watermark and a footer
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:
- 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 calledwatermark.png.
- Inject the file system interface in the constructor of our service.
- Get the path of the image and pass it to the
SetWatermarkImage
method of mPDF. - Set the hash in the footer
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.
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!