The recommended approach is to use the CakePHP plugin CakePdf for this.

With that plugin it is "a piece of cake" to output HTML views as PDF or render them to files for download/emailing etc.

You can obviously just use an approach just like this article describes.

But this makes the code quite dependent on the actual rendering library used isn’t very DRY when there is more than a single page that needs PDF rendering.

Note: This article is about generating PDF files from HTML templates. If you already have PDF files you just want to output or serve as download, then this article is not for you. See serving-views-as-files-in-cake2 then.

Demo

A live demo can be found in my sandbox app. It also helps to compare the different engines.

Installation

First you need to decide on what engine you want to go with.

WkHtmlToPdf is the fastest, but it also depends on CLI and the binaries being installed properly. So it might work differently or not at all on different OS. But using binary files it should work out of the both on both UNIX and Windows. The latter just needs a single exe file drag-and-dropped on your system. wkhtmltopdf.org/downloads.html contains such a ready-to-use binary.

You can then link it in your Configure set-up for CakePdf:

Configure::write('CakePdf.binary', YOUR_PATH_TO . 'wkhtmltopdf\wkhtmltopdf.exe');

For UNIX those precombiled binaries on that download page might not work and a simple apt-get install wkhtmltopdf usually won’t do the trick. You need to manually compile it in this case. But on most systems it should be fine. Don’t forget to adjust the path to the binary here, as well, if it is not the default one:

Configure::write('CakePdf.binary', YOUR_PATH_TO . 'wkhtmltopdf/bin/wkhtmltopdf');

I usually go with DomPdf as it is quite reliable, and (as PHP code) even though slower than a native approach will work flawlessly with all OS and setups without additional dependencies. If speed really is an issue you might want to reconsider using the server internal libraries. But for me generating invoices and such never is time-critical and usually done asynchronous using background tasks via shells 🙂

For DomPdf one important setting when using images in your PDFs is to "enable remote":

define('DOMPDF_ENABLE_REMOTE', true);

Either way don’t forget the rest of the settings for CakePdf – it could look like this:

$config['CakePdf'] = array( 'engine' => 'CakePdf.DomPdf', 'options' => ..., 'margin' => ..., 'orientation' => 'portrait', );

The official documentation on the installation part tells you all you need to know. Check it out.

Advanced Setup

The recommended way is to use CakePlugin::load('CakePdf', array('bootstrap' => true, 'routes' => true)); which is supposed to take care of automatic View class switching.

If you don’t want to load the plugin bootstrap/routes and rather do it manually you need to automatically map all .pdf extension URLs to the CakePdf plugin PdfView ourselves:

public $components = array( 'RequestHandler' => array( 'viewClassMap' => array('pdf' => 'CakePdf.Pdf') ) );

This can also be useful if you want to overwite/extend the PdfView class for some reason ( MyCakePdf etc).

Skipping the plugin routes file is also useful, if you already have extension settings in your routes. You can then simply update your APP/Config/routes.php :

Router::parseExtensions(); Router::setExtensions(array('json', 'xml', 'rss', 'pdf')); // We just add pdf to the already defined ones

With both routes and bootstrap defined manually, CakePlugin::load('CakePdf'); suffices.

Usage

The official documentation already states that, so I won’t go into much detail. For the sake of completeness a short example:

public function view($id) { $this->pdfConfig = array( 'filename' => 'invoice', 'download' => (bool)$this->request->query('download') ); $invoice = $this->Invoice->find('first', array('conditions' => array('id' => $id))); $this->set(compact('invoice'); }

If you use my Tools plugin, you can just use $invoice = $this->Invoice->get($id) here.

Make sure you have a basic pdf layout in /View/Layouts/pdf/default.ctp and a view template for the PDF in /View/Invoices/pdf/view.ctp

Now if you allow access without extension ( /invoices/view/1 ) the action can render a normal HTML page with the invoice in a table like form.

If you link it as /invoices/view/1.pdf it will automatically switch the view class to PdfView and render it as PDF file:

$this->Html->link('View PDF', array( 'action' => 'view', $id, 'ext' => 'pdf' ));

Note the trick with download using the query string for it. If you link to it with ?download=1 it will trigger the download instead of just displaying it:

$this->Html->link('Download PDF', array( 'action' => 'view', $id, 'ext' => 'pdf', '?' => array('download' => 1) ));

This way the action can do both automatically.

Tips

URLs and Paths

For all engines to work with your URLs and images outputted by helpers and therefore with schema-less absolute URLs ( /controller/action/ and /img/foo.jpg ) this little modification in your AppHelper is quite useful:

/** * Overwrite to make URLs absolute for PDF content. * * @param mixed $url * @param bool $full * @return string */ public function url($url = null, $full = false) { if (!empty($this->request->params['ext']) && $this->request->params['ext'] === 'pdf') { $full = true; } return parent::url($url, $full); } /** * Overwrite to make paths for assets absolute so they can be found by the PDF engine. * * @param string $path * @param array $options * @return string */ public function assetUrl($path, $options = array()) { if (!empty($this->request->params['ext']) && $this->request->params['ext'] === 'pdf') { $options['fullBase'] = true; } return parent::assetUrl($path, $options); }

This will make them absolute including the full base (schema + domain) in PDF context.

Filename and downloading

If you don’t force downloading and display the PDF, make sure you read the "eastereggs" part of serving-views-as-files-in-cake2/ regarding an additional passed param here to actually make the downloaded file name what you want it to be.