Using Browsershot for PDF generation
Yesterday I was trying to tweak the invoice design for Addrow, a SaaS application I made with Laravel. I have basically two templates for the invoice, one for web display and a PDF-version.
I tried to use one template for both versions, but using Dompdf I found myself having to tweak quite a lot to get the PDF anywhere near the HTML-version so ended up with two separate versions.
As I was trying to tweak the design, I decided to look for alternatives to Dompdf and try to get to a situation where I only needed to maintain one template file. I then stumbled upon this tweet by David Hemphill.
Generating PDFs with @vuejs components: I have a public invoice "print" view, styled with @tailwindcss, and served by a @vuejs SPA. I'm using spatie/browsershot to hit the page and generate a PDF download response of that public page: pic.twitter.com/yIXXCk7Nge
— DAVID-19 (@davidhemphill) February 19, 2019
Using Spatie's Browsershot Package
And that sort of blew my mind. David is using Spatie's Browsershot package to utilize the headless version of Chrome and generate a PDF download that way. A massive advantage is the template reusability and the PDF looks pretty darn close to the web-version. I can also use TailwindCSS to style the invoice. Pretty cool!
The package requires you to have Node and the Puppeteer library installed. But that is pretty easy to install. The package readme file, even includes instructions how to install this on a Forge provisioned server which is exactly my use case.
What does the code look like?
You can get the idea from David's screenshot, but I thought it would be nice to share some more code to give an indication of how I implemented this. I have added a pdf
method on my Invoice class like this:
public function pdf() { $content = view('templates.invoice', ['invoice' => $this])->render(); return Browsershot::html($content) ->margins(18, 18, 24, 18) ->format('A4') ->showBackground() ->pdf(); }
The generated HTML is just a Blade rendered template. Then, in my InvoicesController
I can do the following to stream the PDF to the browser:
public function pdf($id) { $invoice = Invoice::findOrFail($id); $this->authorize('view', $invoice); return response()->stream(function () use ($invoice) { echo $invoice->pdf(); }, 200, ['Content-Type' => 'application/pdf']); }
To add this PDF as an email attachment, I use the attachData
method on the Mailable
class.
public function build() { return $this->from(config('mail.from.address')) ->replyTo($this->invoice->profile->email) ->markdown('mail.invoice') ->subject(__("Your invoice is ready")) ->attachData($this->invoice->pdf(), __('Invoice :number', ['number' => $this->invoice->invoice_number]) . '.pdf'); }
Conclusion
In the end I am very happy with this refactor. I can now use one template and use TailwindCSS for styling. Thanks to Spatie for providing this package and David for mentioning this on Twitter. Also, thanks to Dompdf since I've used this for years and in a lot cases it might very well still be the way to go.