Serving views as files in Cake2

Actually, its not that different in Cake1.3. But as I just played around with it in 2.0, I will stick to that version for examples.

How to start

Skip this, if you want to cut to the chase.

In your routes.php you need to add Router::parseExtensions(); (or only specific ones).
That tells cake that urls ending with ".xyz" will be served as files (either inline or as download attachment).

Setup

Don’t forget to add the RequestHandler to your Controller components list:

public $components = array('RequestHandler');

This will be an important part in the following auto-magic.

Let’s say you want to display an invoice as pdf. The normal url is /invoices/view/1.
Now, we set a link in the view to the file like so:

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

Since we use the RequestHandler Component cake automatically detects that this will be a pdf file, it will
a) set the correct header (application/pdf)
b) will try to find the specific pdf view in /View/Invoices/pdf/index.ctp and the pdf layout in /View/Layouts/pdf/default.ctp

That’ all 🙂

Download it right away

Files like pdf can be displayed inline. So the browser will usually not force you to download it.
If you want this, though, you need to call $this->response->download($filename); in your controller action.

Note: If your browser does not understand the file format (in this case pdf) it will probably trigger the download right away.

Browser bugs and easter eggs

Well, you could also call it a bug. But during my tryouts I found out that files served inline (Content-Disposition: inline; filename="…") will not use the given filename on save. They will be saved with the name in the url instead. In the above example it would be "1.pdf". I did some research: Thats a well known browser deficiency that nobody yet fixed. Or so it seems.
Ok, but nobody wants his invoice to be "1.pdf". So what can we do about it?

I found a pretty well working workaround:

$this->Html->link('View as PDF', array('action'=>'view', 'ext'=>'pdf', 1, 'invoice-2011-11-01_some_customer_tag'));

As you can see we simply add our filename to the url – after the id (!).

Since it is only "filename cosmetics" we dont need to add this second passed param to our method:

public function view($id = null) {}

It will be ignored in the action itself.

So the generated url is /invoices/view/1/invoice-2011-11-01_some_customer_tag.pdf and will result in a file saved as "invoice-2011-11-01_some_customer_tag.pdf".
Job done.

Let me know what you think.

Example for PDFs

A pretty quick example how to output your content as pdf using DOMPDF.

In your layout (default.ctp in /pdf/):

App::import('Vendor', 'dompdf/dompdf.php');
$dompdf = new DOMPDF();
$dompdf->load_html(utf8_decode($content_for_layout), Configure::read('App.encoding'));
$dompdf->render();
echo $dompdf->output();

For me it looked like that without utf8_decode the DOMPDF lib seems to be buggy – although it claims to support utf8.

If you have existing PDF files you just want to display or serve as download, you can directly interact with the response object:

$this->autoRender = false; // Tell CakePHP that we don't need any view rendering in this case
$this->response->body(file_get_contents($pathToPdfFile));
$this->response->type('pdf'); // Only necessary if the action is not accessed with `.pdf` extension

Example for ics (ical calendar) files

Just a very basic example using an IcalHelper from my Tools Plugin.

Please note: There is a pending ticket on this. Until then you either need to manually patch the ResponseClass (quick solution) by adding the missing mimetype 'ics' => 'text/calendar', or define it in your Controller via $this->response->type(array('ics' => 'text/calendar')); and set all paths manually… Hopefully the second issue for unknown mimetypes will be addressed soon, as well.

Controller code:

$reservation = ...;
$this->plugin = 'Tools'; //not necessary, only that I want to store the layout once (in the plugin)
$this->set(compact('reservation'));
$this->helpers[] = 'Tools.Ical';

Layout (in /Plugin/Tools/View/Layouts/ics/default.ctp):

<?php echo $content_for_layout; ?>

View (in /View/Reservations/ics/export.ctp):

$data = array(
    'start' => $this->Time->toAtom($reservation['Reservation']['time']),
    'end' => $this->Time->toAtom(strtotime($reservation['Reservation']['time'])+HOUR),
    'summary' => 'Reservation',
    'description' => $reservation['Reservation']['headcount'].' persons @ location foo',
    'organizer' => 'CEO',
    'class' => 'public',
    'timestamp' => '2010-10-08 22:23:34',
    'id' => 'reservation-'.$reservation['Reservation']['id'],
    'location' => $reservation['Restaurant']['address'],
);
$this->Ical->add($data);
echo $this->Ical->generate();

And all that is left again is to link this action accordingly:

echo $this->Html->link($this->Html->image('icons/calendar.gif', array('title'=>'Download reservation as ical-file')), array('action'=>'export', $reservation['Reservation']['id'], 'ext'=>'ical'), array('escape'=>false));

Generic example using file()

This will work with all kinds of data if already available as file (passthrough):

public function download() {
    $this->autoRender = false; // tell CakePHP that we don't need any view rendering in this case
    $this->response->file('/path/to/file/' . $fileNameWithExtension, array('download' => true, 'name' => 'my-filename.ext'));
}

file() also sets the correct type headers usually. In most cases that suffices.
If it doesn’t, you need to add this after file(), as well:

$this->response->type('ext');

And maybe even declare the extension if it is yet unkown to CakePHP.

Read more an file() here.

And this will work with all kind of "dynamically generated data", in our case an image:

public function display() {
    $this->autoRender = false; // tell CakePHP that we don't need any view rendering in this case
    $content = 'binary data of generated jpeg image (on the fly maybe even)';
    $this->response->body($content);
    $this->response->type('jpg');
}

Optionally, you can also use download() again to force the downloading instead of displaying it inline.

Try to avoid returning the response object or doing more with it than this. Especially calling send() on it is a very bad idea as you invoke it twice this way (The controller dispatching will automatically call this method at the end of the request) and end up with strange results.

Downloading records as CSV/JSON etc

In case you want your (paginated?) index view data to be downloadable, try also to use view extensions.

We link to the same action, but with the appropriate extension:

echo $this->Html->link('As CSV', array('action' => 'index', 'ext' => 'csv'));

So in our case for CSV, we can leverage CsvView to easily switch from normal HTML output to CSV output:

// Switch to CSV if desired
if (!empty($this->request['params']['ext'])) {
    $this->viewClass = 'CsvView.Csv';
    $this->set(compact('data', '_serialize'));
    return;
}

In case you want to only force download in some cases, you can leverage the same little query string trick as above:

if ($this->request->query('download')) {
    $this->response->download($this->request->params['action'] . '.' . $this->request->params['ext']);
}

And the link:

echo $this->Html->link('Download as CSV', array('action' => 'index', 'ext' => 'csv', '?' => array('download' => 1)));

So only with a link that contains .../action.csv?download=1 it will actually force-download it and usually just display the data in the browser first.

Same goes for JSON and other view class approaches.

See a live example in the sandbox.

Automatic view class switching

public $components = [
    'RequestHandler' => [
        'viewClassMap' => ['csv' => 'CsvView.Csv']
    ]
];

This will use the CsvView as soon as the extension is csv 🙂

2016-01 – CakePHP 3

For CakePHP 3.x the sandbox examples can be found at sandbox3.dereuromark.de/export. For CSV there is an own section at (sandbox3.dereuromark.de/sandbox/csv).

2018-09 – ICAL

For calendar and ICAl generating the code went to Calendar plugin.

2021-11 – CakePHP 4

For CakePHP 4.x the sandbox examples can be found at sandbox.dereuromark.de/export. For CSV there is an own section at (sandbox.dereuromark.de/sandbox/csv).

0.00 avg. rating (0% score) - 0 votes

34 Comments

  1. Hi Mark, thanks for the article, I am almost getting it to work but the code on the layout is not working it gives me an error for your code on the creation of the DOMPDF class.

    $dompdf = new DOMPDF();

    It gives me an error that says:

    "Fatal error: Class ‘DOMPDF’ not found"

    Any ideas?

  2. did you App::import correctly? Where is the class? .. exactly (lower/uppercase matters)

  3. I’m was problems importing the vendor too. Downloaded latest version of dompdf and extracted to /app/Vendor/dompdf.

    In layout I had:
    App::import(‘Vendor’, ‘dompdf/dompdf.php’);

    This failed silently as App::import simply returns false is file not found. I got the import working by rmoving the file extension, this left me with:
    App::import(‘Vendor’, ‘dompdf/dompdf’);

    Now I am getting an error500, but at least that’s some progress.

    P.S. I am using Cake 2.1

  4. My previous comment had a few typos in, sorry 😛

    I managed to get a PDF displaying by replacing the following:
    App::import(‘Vendor’, ‘dompdf/dompdf.php’);

    with:
    require_once(APP . ‘Vendor’ . DS . ‘dompdf’ . DS . ‘dompdf_config.inc.php’);

    I came to this conclusion after realising some of the checks in dompdf.php were throwing exceptions that Cake was trying and failing to intercept.

    I then went searching for how to manually include just the config file and ended up reading a StockOverflow response from Jose Lorenzo:
    http://stackoverflow.com/questions/8158129/loading-vendor-files-in-cakephp-2-0

    Hope this helps, or if you can think of anything wrong with this method please advise me.

  5. Mark,

    Setup is working locally (WAMP), but when uploaded to server getting open_basedir errors.

    I assume this is something to do with paths you can specify in dompdf_config.custom.inc.php and wondered if you could share your config settings so I can replicate?

    Thanks, Paul.

  6. Great tutorial. Still haven’t gotten the dompdf to render correctly, but i’m sure it’s a config issue. Just a side note though. I had to include

    public $components = array('RequestHandler');

    in my controller before it would start handling the requests correctly. After that, it was butter. Thanks Again!

  7. Thank you for the hint!
    I use it always by default – like Session. So that’s why I didn’t think of mentioning it 🙂

    But I updated the article with your suggested code snipped.

  8. Still can’t get this to work… 🙁

    In my routes I have :
    Router::parseExtensions(‘rss’,’pdf’);

    In layout/pdf/default.ctp I have :

    load_html(utf8_decode($content_for_layout), Configure::read(‘App.encoding’));
    $dompdf-&gt;render();
    echo $dompdf-&gt;output();

    In Posts/pdf/view.ctp I have :

    An I get :

    Fatal error: Class ‘DOMPDF’ not found in /app/View/Layouts/pdf/default.ctp on line 3

    🙁 And the require one method didn’t work either. Any idea ?

  9. Look at Mark’s section above about what should be in /Layouts/pdf/default.ctp.

    You seem to be missing the first two lines which require the vendor library and then create an instance of DOMPDF();

    This is why you are getting a message saying $dompdf does not exist.

  10. I followed the first set of steps above (that do not use the DOMPDF class) and I get the downloaded PDF (1.3 MB), but it can’t be opened.

    When opened in Preview on Mac OS X it says the file can’t be opened because it is corrupt.

    When opened in Fedora’s Doc Viewer it says HTML is not a supported file type… this makes me think Cake isn’t parsing the extension correctly. I tried –

    1 opening the PDF file with BBEdit and….
    1a changing the Content-Type of the meta tag to application/pdf
    1b changing the DOCTYPE to pdf

    2 removing all the JavaScript

    After searching, I found direction that indicates you must put the

     Router::parseExtensions('pdf')

    in the top of the /app/Config/routes.php file… still didn’t work for me though. Any thoughts?

  11. My solution:

    require_once(APP . ‘Vendor’ . DS . ‘dompdf’ . DS . ‘dompdf_config.inc.php’);
    spl_autoload_register(‘DOMPDF_autoload’);
    $dompdf = new DOMPDF();

    Thanks!

  12. Sorry for the noob question. But I hope someone will tell me the answer. Here’s my question: What do you put on the default.ctp (View/layouts/pdf/default.ctp) and the action_name.ctp (View/Controller/pdf/action_name.ctp).

  13. Oh by the way, i just want to add up. I always get this error when opening the pdf file in cake (url:site/controller/action.pdf) it loads in grey background that looks like a legit pdf page and says "Failed to load PDF Document".

  14. Btw, I think it should be $this-&gt;response-&gt;download($filename); for the download.

  15. Cake 2.2:

    App::import('Vendor', 'dompdf', true, array(), 'dompdf' . DS . 'dompdf_config.inc.php');
    $dompdf = new DOMPDF();
    $dompdf->load_html($this->fetch('content'), Configure::read('App.encoding'));
    $dompdf->render();
    echo $dompdf->output();

    Perfect!

  16. thanks for the update. maybe they got the utf8 problem fixed. but the fetch syntex sure is nice 🙂

  17. Hello,
    Good article, only I am a noobie in CakePHP.
    I am getting the following strange characters:

    %PDF-1.3 1 0 obj > endobj 2 0 obj > endobj 3 0 obj > >> /MediaBox [0.000 0.000 612.000 792.000] >> endobj 
    4 0 obj [/PDF /Text ] endobj 5 0 obj > endobj 6 0 obj > endobj 7 0 obj > stream 
    xœã2Ð300P@&‹Ò¹œBŒMôÍÌ•´©¹¥BHŠ‚¾›¡‚¡‘ž•BHš‚B´FIjq‰Br~^Ij^‰BFjQªf¬Bˆ—‚k

    The code i am using in download.ctp (for my invoices) is:

    App::import('Vendor', 'dompdf', true, array(), 'dompdf' . DS . 'dompdf_config.inc.php');
    $dompdf = new DOMPDF();
    $dompdf->load_html(utf8_decode('test content here'), Configure::read('App.encoding'));
    $dompdf->render();
    echo $dompdf->output();

    You can se that I want to output only "test content here".
    Am I doing this the wrong way?

    Hope someone can help me!

  18. Hi all,

    thanks for the article and the helpful comments.
    I used Jackson Bicalho’s solution:

    $dompdf->load_html($this->fetch('content'), Configure::read('App.encoding'));

    It works great, except that I’m missing any CSS formatting as it only fetches the content. How would I add CSS formatting in a DRY way, i.e. get the header with the CSS imports from the layout of the HTML view?

    Thanks for any hints
    George

  19. Is there a way to load PDF layouts Dynamically?

    what i want is i could choose different templates and styles for each pdf file that i wanted to view or download.

  20. Sure, why not?
    You can always modify the layout and its path in your controller, e.g. the action.

  21. Cheers for the article Mark. Just getting started with CakePHP myself and hoped you could point me in the right direction:

    I have a controller with an action called "getxml". This action basically retrieves some data from the DB and sets variables for the view to use, and sets an ’empty’ layout. The view then outputs an xml file.

    I would like the app to force the download of this xml file however, rather than viewing it in the browser. Is this possible?

    Thanks again!

  22. Thanks again Mark. I’ve taken a look but I’m still a bit lost however. What functions in particular do you think I should be looking at?

    I really appreciate your help 🙂

  23. I’ve tried the following, but no luck as of yet:

    $this-&gt;response-&gt;type(‘application/force-download’);

  24. Thank You for this excellent post!!

    I use mpdf and it is working pretty well.
    If my method uses GET, everything works as expected, but if I get the data from a form parameters based search (POST) I don’t now how to use this approach, because the link /controller/action/id.pdf won’t work, once the action only allows POST.

    Do you know how can I workaround this situation?
    Thank You once more.

    Nilson

  25. it sounds highly unconventional to post to a .pdf file.
    You might want to post to /controller/action/id directly (if its a normal post) and then upon success redirect to the file as .pdf.

    Note that its always a good approach to use PRG redirects anway.

    Also note that .pdf and other "assets" are usually also cached (or at least should be).

  26. It is not my intention to post direct to PDF! The form is used by the users to POST the search parameters ($conditions) that controller will use to get the result that will be show in the view.ctp. Then, If the user wants to generate a PDF in tihis case? There won´t be an id.pdf. Maybe I could use query strings to make a link with de search parameters. What do you think about?

    Thank you!

  27. As I mentioned before, don’t post to .pdf – post to the normal add/edit action and redirect afterwards to the then existing pdf.

  28. Thanks Mark, I’m a noob too, as the other noob above, I followed your tutorial and my application give me back:

    %PDF-1.3 1 0 obj &lt;&gt; ….

    I need so much to do that work.

    Could you light my way and help me , please?

  29. Hi i get this error on output

    %PDF-1.3 1 0 obj &lt;&gt; endobj 2 0 obj &lt;&gt; endobj 3 0 obj &lt;&lt; /Type /Pages /Kids [6 0 R ] /Count 1 /Resources &lt;&lt; /ProcSet 4 0 R /Font &lt;&gt; &gt;&gt; /MediaBox [0.000 0.000 612.000 792.000] &gt;&gt; endobj 4 0 obj [/PDF /Text ] endobj 5 0 obj &lt;&gt; endobj 6 0 obj &lt;&gt; endobj 7 0 obj &lt;&gt; stream x���]o�0���+�]Ri�����CڢD�4M����)r�o�fƨ�~��Gڴa�s��y��\y��&lt;8���Շ-�~�߃mx탟 �.�kny+c����OXm{�7�bv�qc(�QzY�Q�G�MȩI���5 J��W$� r�#~�҈�V�`sv �o9�Fw���z�q]�H ��VeVj�F���m���XsY�t-�T�S��k&lt;���8��������A�q�3^���������� endobj 10 0 obj &lt;&gt; endobj xref 0 11 0000000000 65535 f 0000000008 00000 n 0000000091 00000 n 0000000137 00000 n 0000000302 00000 n 0000000331 00000 n 0000000445 00000 n 0000000508 00000 n 0000001084 00000 n 0000001112 00000 n 0000001220 00000 n trailer &lt;&gt; startxref 1330 %%EOF

  30. Hi,
    When i hit the action it showing me error :-
    Class ‘Frame_Tree’ not found

    Thanks.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.