Invoice PDF generation with Browsershot

A balloon is controlling a browser with a remote with an antenna on it. The browser contains some pdf's.
Image by Annie Ruygt

Invoicing is boring and manually writing up invoices even more so, that’s why you should automate the entire process in your app. And why not run it on our servers while you’re at it? With Fly.io, you can get your Laravel app running globally in minutes!

Invoicing is a hugely important part of any business. By having your Laravel app generate invoices automatically, you can make the life of your users a lot easier and we all know that means they’ll hang around longer and be more keen to use your apps.

In this article, I’ll show you how to use Spatie’s Browsershot package to generate invoice PDFs automatically. Read on to find out how!

Making the invoice view

Before we talk about generating PDF’s or how Browsershot is set up, we’ll need something to show on our invoice. So I set up a fictional company called The Invoicing Company™️ with the sole purpose of sending out invoices, a solid business plan if you ask me. Anyway, here’s how the invoice looks:

To generate the pdf, we’ll need a web page that only displays the pdf itself. So, I set up a view that shows an A4-sized div like this: <div style="width: 210mm; height: 297mm; padding: 25mm 19mm"></div> . Once that’s set up, you can let your inner web designer run free.

Fly.io ❤️ Laravel

Fly your servers close to your users—and keep your apps running buttery smooth. Deploy globally on Fly in minutes!

Deploy your Laravel app!

Setting up Browsershot

For turning HTML into a PDF, I’m using Spatie’s Browsershot package. It has great documentation which made it a joy to use. I had to make some changes to the Dockerfile that’s shipped with the fly launch command as well, this is only needed for the worker and not for the web app itself:

# Other Dockerfile stuff here...

# Packages like Laravel Nova may have added assets to the public directory
# or maybe some custom assets were added manually! Either way, we merge
# in the assets we generated above rather than overwrite them
COPY --from=node_modules_go_brrr /app/public /var/www/html/public-npm
RUN rsync -ar /var/www/html/public-npm/ /var/www/html/public/ \
    && rm -rf /var/www/html/public-npm \
    && chown -R www-data:www-data /var/www/html/public

+ # ADDED: Browsershot does need npm and nodejs. So we'll install them anyway in the base container.
+     # I also added the chromium dependencies, since they don't get installed correctly.
+ RUN curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \
+     && apt-get install -y nodejs gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget libgbm-dev libxshmfence-dev \
+     && npm install -g npm \
+     && npm install --unsafe-perm puppeteer

EXPOSE 8080

ENTRYPOINT ["/entrypoint"]

Making the job

Since it’ll take some time and we don’t want to make our users wait around, we’ll be using queued jobs to generate the pdf. Here’s how I set it up: I made a Job that generates the pdf, and then sends a post request to a signed url to upload the invoice to the correct Order. I’m keeping it simple, you can use S3 here or something similar you like. The reason I added this post request is because our worker will be running in a different VM than our web app, so it can’t reach the web app’s file system. So here’s how the job’s handle method looks:

// Add a private property $order here and set it in the __construct() method.

/**
 * Execute the job.
 *
 * @return void
 */
public function handle()
{
    try {
        Browsershot::html(view('invoice.invoice', ['order' => $this->order])->render())
            ->noSandbox()
            ->waitUntilNetworkIdle()
            ->format('A4')
            ->showBackground()
            ->savePdf("temp.pdf");

        $postRoute = URL::signedRoute('orderinvoices.store', ['order' => $this->order]);
        Http::attach('invoice', file_get_contents('temp.pdf'), 'invoice.pdf')
            ->post($postRoute)
            ->throw();
    }
    catch (\Exception $exception )
    {
        Log::error($exception);
    }
}

Some things to notice here: The noSandbox() method is needed to avoid errors with the headless Chrome browser used by Puppeteer. Because I created the HTML code we pass along myself, I’m not too worried about the security risks. You can read more about Chrome sandboxing here.

What’s next?

By now, we’ve covered everything needed to create a job that generates PDFs with anything you want on them. So what do we do now? My humble suggestion is that you check out my article on cost-effective queue workers. I explain how to set ‘em up and more importantly, how running your queue workers on Fly.io can save you money. See you there!