PDF Rendering with WKHTML and Laravel

Formerly we wrote an article about DOMPDF and Laravel. That solution is working well when you need to render a small PDF. But what if you have a multi-page document that you need to render? You can use WKHTML and integrate it with Laravel.

Warming Up

First of all, if you want a quick and ready solution, you may use this package. But in case, you would not use all the functionality what the package offers, or you don’t want another dependency that brings another dependency, you may create a simple integration with the base package, called Snappy.

Like the previous post, our approach will be very similar. Creating the service and the provider where we bind the service to the container. Then we bring the basic config and prepare the controller where the PDF can be rendered to view or download.

The PDF Service

The first thing to do is to install the WKHTML binary and Snappy package. We can do both with composer. It depends on your machine, which WKHTML you have to pull in. Here you can find an indication. For me, the proper command was composer require h4cc/wkhtmltopdf-amd64.

Then we need to install Snappy, also via composer: composer require knplabs/knp-snappy. Now, all the dependecies are ready, we can prepare the PDF service.

namespace App\Services;

use App\Invoice;
use Knp\Snappy\Pdf as Snappy;
use Illuminate\Support\Facades\View;

class Pdf extends Snappy
{
    /**
     * Initialize a new pdf instance.
     *
     * @param  array  $config
     * @return void
     */
    public function __construct(array $config = [])
    {
        parent::__construct($config['binary'], $config['generator']);
    }

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

It’s very similar to the DOMPDF approach. Here also, we pass an invoice to the renderer, but of course, you can totally pass anything you need here.

Registering the Service in Provider

We can make the new provider by running the php artisan make:provider PdfServiceProvider.

namespace App\Providers;

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

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

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [Pdf::class];
    }
}
Please note, since Laravel 5.8, the $defer property is deprecated. Deferred providers must implement the DeferrableProvider contract.

So nothing extra here. We bind the service to the container and automatically pass the configuration to the instance. It means, whenever we use the automatic resolution from the container, the configuration is automatically injected and we don’t need to bother with that. Talking of configuration, let’s see the config file.

The Basic Configuration

The basic config should look like this. We specify the binary path, but also, you may pass the path from the .env file.

// config/pdf.php

<?php

return [

    'binary' => env('WKHTML_PATH', realpath(h4cc\WKHTMLToPDF\WKHTMLToPDF::PATH)),

    'generator' => [
        'images' => true,
        'no-images' => false,
        'encoding' => 'utf-8',
        'disable-smart-shrinking' => true,
        'page-size' => 'A4',
        'margin-top' => 0,
        'margin-left' => 0,
        'margin-right' => 0,
        'margin-bottom' => 0,
    ],

];

The PDF Controller

The controller is basically the same as the DOMPDF’s controller. First let’s generate the controller with the php artisan make:controller PdfController command. Then define the route for the controller in the web routes file.

Route::get('invoice/{invoice}', 'PdfController');

Now, let’s see the controller itself:

namespace App\Http\Controllers;

use App\Invoice;
use App\Services\Pdf;
use Illuminate\Http\Request;

class PdfController extends Controller
{
    /**
     * The pdf 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  \Illuminate\Http\Request  $request
     * @param  \App\Invoice  $invoice
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request, Invoice $invoice)
    {
        return response($this->pdf->render($invoice), 200)->withHeaders([
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => ($request->has('download') ? 'attachment' : 'inline') . "; filename='invoice-{$invoice->id}.pdf'",
        ]);
    }
}

So, what’s going on? When the user hits the invoice route, we render the PDF. If the query string contains the download key, the PDF will be downloaded, otherwise, it will be inline and readable from the browser.

Closing up

This approach is suggested when you have a bigger document (let’s say 10+ pages) that you need to render. Also, if your setup is not proper, you may have some errors, that is not so easy to debug. For example, make sure you have a compatible libssl installed on your machine. Also, make sure the binaries are runnable.

Special thanks for the following recource(s): Icon made by Eucalyp from www.flaticon.com