Simple PDF Rendering with Laravel

PDF generation is a core feature when our app contains invoicing or services that require a downloadable version of an information schema. Like in many other situations, a simple solution would do it instead of a full-featured package. Let’s see how to render PDFs easily.

Getting Started

Yes, yes we know…there is the package called laravel-dompdf, but you know what, personally we don’t like to integrate a package immediately if we have to solve something. We believe it’s better to try to cover your needs on your own first if it does not require too much extra energy. So let’s move on.

But of course, we can’t be dependency free here; we need to obtain a package called dompdf. We can build our custom wrapper around that, instead of using a ready-to-use solution. It’s an impressive package that provides an excellent solution for the needs we have. If your app uses Cashier, you should know about that. So run the composer require dompdf/dompdf command to push it to your composer dependencies.

As a next step, we need to create a configuration for setting up the PDF generation itself. Just add a dompdf.php file to your config folder, and that’s it we can access it instantly. You can find all the settings in its repository, but to keep it simple we used the following ones:

// config/dompdf.php

<?php

return [

    'chroot' => realpath(base_path()),
    'fontDir' => storage_path('fonts/'),
    'fontCache' => storage_path('fonts/'),
    'defaultMediaType' => 'screen',
    'defaultPaperSize' => 'a4',
    'dpi' => 96,
    'defaultFont' => 'serif',
    'fontHeightRatio' => 1.1,

];
The laravel-dompdf package gave useful tips about configuration, so basically, we just copied some of those and optimized for our needs.

Writing the Custom PDF Service

Before we start let’s think about what do we need to do here. What are the needs we need to handle?

We want to offer to the user to download or just to view the PDF we generated. Of course, we need to bring some PDF template since there is no point to provide static stuff for all the users. So we need a blade template what handles the data, what we render and pass the result to the dompdf’s generator.

Let’s see the code of our custom PDF service, and then we explain the methods. So since it’s a service, we should create the file in the app/Services folder, called Pdf.php.

<?php 

namespace App\Services; 

use App\Invoice;
use Dompdf\Dompdf; 
use Illuminate\Support\Facades\View; 

class Pdf extends Dompdf 
{ 
    /** 
     * Create a new pdf instance. 
     * 
     * @param  array $config 
     * @return void 
     */ 
    public function __construct(array $config = []) 
    { 
        parent::__construct($config); 
    }

    /** 
     * Determine id the use wants to download or view. 
     * 
     * @return string 
     */ 
    public function action() 
    { 
        return request()->has('download') ? 'attachment' : 'inline';
    }


    /**
     * Render the PDF.
     *
     * @param  \App\Invoice  $invoice
     * @return string
     */
    public function generate(Invoice $invoice)
    {
        $this->loadHtml(
            View::make('your.blade.template', compact('invoice'))->render()
        );

        $this->render();

        return $this->output();
    }
}

As you can see, it’s a very narrow class. Since we extend the original dompdf, we don’t need too many things, but some extensions to make our life easier.

In the constructor, we accept the configuration and just pass it to the parent’s constructor. This is where we take the settings what we defined in the dompdf config file.

Also, it would be nice if we could determine if the user wants to download the PDF or just wants to view it. We could add a parameter to the URL they click on if they’re going to download the document. In the action() method, we check if the request contains a query string key (with or without any value), called download. If yes, the action will be attachment. Otherwise, it will be inline. We will pass this to the response’s header in our controller.

Now take a look at the tricky part, the generate() method. By default, dompdf offers a loadHtml() method, what accepts the HTML markup in a string version and convert it to a PDF. But somehow, we need to pass the data to our blade file and return with a string version of it instead of a view response. Fortunately, we can use the View facade here. We just make a view and pass the parameters like when we use the view() helper. We pass the blade template we want to use and the data we want to pass to the blade template. We are using a fake model here, you can pass anything you want! There is nothing left, but to render the view, what generates the HTML what we can pass to the loadHtml() method. When it’s done we can render and return with the output. Both methods are inherited from the dompdf parent class.

Let’s move on and bind our service to the service container!

Binding the Service to the Container

Anytime we want to extend our app with a service, the best way to do via service providers. We can generate a service provider with the make:provider command. Since we have a PDF service, we call our provider PdfServiceProvider.

See the code first, and then we explain what is happening:

<?php

namespace App\Providers;

use App\Services\Pdf;
use Illuminate\Support\ServiceProvider;

class PdfServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(Pdf::class, function ($app) {
            return new Pdf($app['config']['dompdf']);
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [Pdf::class];
    }
}

In the register method, we bind the PDF service into the application. That means when we use the DI container to resolve the service, we will get an instance of it with the configuration we defined automatically. Nothing special here, but it’s incredible to make it once and use it anywhere without configuring over and over.

In the provides method, we just return with the services we provide in our service provider. In this case, we have only the PDF service.

Also, you may notice the $defer property. If a provider is only registering bindings, we should set it to true. There is a performance reason behind it.

All right, we are ready to use our PDF service, let’s make a controller for it and see how does it work.

A Simple PDF Controller

Create a controller called Pdf controller and make it very simple. Also, prepare the route for it in the web routes:

Route::get('invoice/{invoice}', 'PdfController');
You may not know, but you can leave the method in your route definitions. That means you need to use the __invoke() method in your controllers, what gets triggered when the user hits the route.

Now generate the controller with the make:controller command and name it to PdfController. The controller should look like this:

<?php

namespace App\Http\Controllers;

use App\Invoice;
use App\Services\Pdf;

class PdfController extends Controller
{
    /**
     * The dompdf instance.
     *
     * @var \App\Services\Pdf
     */
    protected $pdf;

    /**
     * Create a new controller instance.
     *
     * @param  \App\Services\Pdf  $pdf
     * @return void
     */
    public function __construct(Pdf $pdf)
    {
        $this->middleware('auth');

        $this->pdf = $pdf;
    }

    /**
     * Generate the PDF to inspect or download.
     *
     * @param  \App\Invoice  $invoice
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Invoice $invoice)
    {
        return response($this->pdf->generate($invoice), 200)->withHeaders([
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => "{$this->pdf->action()}; filename='invoice-{$invoice->id}.pdf'",
        ]);
    }
}

In the constructor, we can inject the PDF service from the container. Awesome right? It’s automatically configured nice and clean approach. Alos, if we want we can define middlewares even if we are using the __invoke() approach.

In the invoke method, we can use the route-model binding like at any other controller method. So what’s happening here exactly? We return a response that contains the pdf we generated by our service. Then we attach two headers to the response. First, we define the content type, there is nothing to explain it. Then we need to define if we want to download or view the file and also, the filename itself. The service will automatically determine if the action and also we can name the files dynamically based on the given invoice model instance.

For example, if we hit the URL, https://your-site.com/invoice/678, we will view the invoice-678.pdf in the browser. If we put the download parameter after the URL like, https://your-site.com/invoice/678?download, we will download the invoice instantly.

Summary

As you can see, this is a simple and clean way to generate PDF files even for inspecting them or downloading them. With a little reading on the topic, we could avoid to use 3rd party packages and bring our own solution. We learned some new, practiced a lot and solved what we needed, nothing more or nothing less.