RSS
 
21. Nov. 2011

Serving views as files in Cake2

21 Nov

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).

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

Posted by Mark in CakePHP

 

Tags: , , , , ,

Leave a Reply

Tip:
If you need to post a piece of code use {code type=php}...{/code}.
Allowed types are "php", "mysql", "html", "js", "css".

Please do not escape your post (leave all ", <, > and & as they are!). If you have encoded characters and need to reverse ("decode") it, you can do that here!
 

 
  1. S.Morteza

    November 27, 2011 at 05:51

    Hi Mark, so glad to your update .
    good article.
    thanks.

     
  2. Adrian Porras

    February 9, 2012 at 04:45

    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?

     
  3. Mark

    February 9, 2012 at 10:06

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

     
  4. pgpMagpie

    February 10, 2012 at 14:57

    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

     
  5. phpMagpie

    February 10, 2012 at 15:42

    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.

     
  6. phpMagpie

    February 10, 2012 at 18:52

    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.

     
  7. phpMagpie

    February 10, 2012 at 20:53

    Scratch that … not sure why, but all appears to be working fine now.

    Thanks for the heads up on your post Mark!

     
  8. R.Chaplin

    March 18, 2012 at 16:21

    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!

     
  9. Mark

    March 18, 2012 at 16:34

    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.

     
  10. A.

    March 24, 2012 at 15:16

    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 ?

     
  11. phpMagpie

    March 25, 2012 at 11:31

    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.

     
  12. Devo Sanchez

    April 14, 2012 at 00:16

    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?

     
  13. Lucas Elias

    July 4, 2012 at 18:28

    My solution:

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

    Thanks!

     
  14. asdzxc

    September 3, 2012 at 20:36

    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).

     
  15. asdzxc

    September 3, 2012 at 20:41

    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".

     
  16. asdzxc321654

    September 9, 2012 at 14:37

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

     
  17. Mark

    September 9, 2012 at 14:41

    maybe you got some debug output there which screws up the pdf content?

     
  18. Jackson Bicalho

    September 20, 2012 at 19:03

    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!

     
  19. Mark

    September 20, 2012 at 19:09

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

     
  20. Rens

    October 17, 2012 at 13:24

    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!

     
  21. George

    October 28, 2012 at 15:29

    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

     
  22. asdzxc

    November 2, 2012 at 15:44

    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.

     
  23. Mark

    November 2, 2012 at 15:59

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

     
  24. Sean

    January 19, 2013 at 14:17

    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!

     
  25. Mark

    January 19, 2013 at 14:22

    Take a look at the new and powerful response class: <a href="http://book.cakephp.org/2.0/en/controllers/request-response.html#CakeResponse::download" title="CakeResponse::download" rel="nofollow">CakeResponse::download()</a>

     
  26. Sean

    January 19, 2013 at 15:55

    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 🙂

     
  27. Sean

    January 19, 2013 at 16:13

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

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

     
  28. Nilson

    January 27, 2014 at 15:31

    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

     
  29. Mark

    January 27, 2014 at 15:34

    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).

     
  30. Nilson

    January 28, 2014 at 14:47

    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!

     
  31. Mark

    January 28, 2014 at 15:07

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

     
  32. jeferson tadeu

    May 21, 2014 at 03:25

    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?

     
  33. aneh

    July 8, 2015 at 14:26

    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

     
  34. Jit Dhar

    June 23, 2016 at 07:28

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

    Thanks.