Обработка представлений как файлов в Cake2
На самом деле, отличий от Cake1.3 не так уж много. Но, так как я только что поэкспериментировал с этим в 2.0, для примеров я буду использовать эту версию.
С чего начать
Пропустите этот пункт если хотите перейти сразу к делу.
В файле routes.php нужно добавить Router::parseExtensions(); (или только конкретное).
Это указывает Cake на то, что URL-адреса заканчивающиеся на “.xyz” будут обрабатываться как (встроенные файлы или как загрузка вложения).
Настройка
Не забудьте добавить RequestHandler к списку компонентов-контроллера.
public $components = array('RequestHandler');
Предположим, что вы хотите отобразить счет в pdf формате. Обычный URL-адрес будет /invoices/view/1.
Далее, устанавливаем ссылку в представлении к файлу:
$this->Html->link('View as PDF', array('action'=>'view', 'ext'=>'pdf', 1));
Поскольку мы используем компонент RequestHandler, Cake автоматически определяет, что это pdf-файл, и:
а) установит правильный заголовок (application/pdf)
b) попробует найти указанное pdf представление в /View/Invoices/pdf/index.ctp и pdf макет в /View/Layouts/pdf/default.ctp
Вот и всё.
Скачать прямо сейчас
Файлы типа pdf могут отображаться inline(встроенными). Поэтому браузер обычно не требует их загрузки.
Если это то что вы хотите, вам необходимо вызвать $this->response->download($filename); в действии контроллера.
Примечание: Если ваш браузер не понимает формат файла (в данном случае pdf), возможно это вызовет немедленную загрузку.
Ошибки браузера
Во время моих проверок я обнаружил, что файлы обрабатываемые inline (Content-Disposition: inline; filename=”…”) не используют указанное имя файла при сохранении. Вместо этого, они будут сохранены с именем в URL-адресе. В приведенном выше примере это было бы “1.pdf”. Я поискал информацию: и как оказалось это известная недоработка браузера, которую еще не исправили.
Хорошо, но никто ведь не хочет, чтобы его счет выглядел как “1.pdf”. И что мы можем с этим сделать?
Я нашел неплохо работающее решение:
$this->Html->link('View as PDF', array('action'=>'view', 'ext'=>'pdf', 1, 'invoice-2011-11-01_some_customer_tag'));
Как видите, нужно просто добавить имя файла в URL – после идентификатора(!).
Поскольку это не более чем “косметика имени файла” нам нет необходимости добавлять второй переданный параметр в метод:
public function view($id = null) {}
Он будет игнорироваться в самом действии.
Таким образом, мы получим сгенерированный URL-адрес /invoices/view/1/invoice-2011-11-01_some_customer_tag.pdf и результат сохраненного файла “invoice-2011-11-01_some_customer_tag.pdf”.
Пример для PDFs
Быстрый пример как выводить ваши материалы в виде pdf, используя Dompdf.
В макете (default.ctp в /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();
Мне показалось, что библиотека глючит без utf8_decode – хоть и заявляется поддержка utf8.
Если у вас уже имеются pdf-файлы, которые хотите просто отобразить или и использовать для загрузки вы можете напрямую взаимодействовать с объектом Response:
$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
Пример для ics (iСalendar) файлов
Простой пример использования IcalHelper c плагином Tools
Пожалуйста, обратите внимание: на последующий запрос регистрации. До тех пор, вам необходимо или вручную исправить ResponseClass (быстрое решение), добавив недостающий MIME-тип 'ics' => 'text/calendar', или определить его в контроллере с помощью $this->response->type(array('ics' => 'text/calendar')); и самостоятельно установить все пути…
Код контроллера:
$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';
Макет (в /Plugin/Tools/View/Layouts/ics/default.ctp):
<?php echo $content_for_layout; ?>
Представление (в /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();
Осталось только связать действие таким образом:
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));
Общий пример использования file()
Он будет работать со всеми типами данных, если они уже доступны как 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() также устанавливает правильный тип заголовков. В большинстве случаев этого достаточно.
Если же нет, то после file() добавляем следующее:
$this->response->type('ext');
Кроме того, возможно объявляем расширение, если оно еще неизвестно CakePHP.
Подробнее тут
Это будет работать со всеми типами “динамически генерируемых данных”, в нашем случае с изображением:
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'); }
При необходимости, можно также использовать download() , чтобы выполнить принудительную загрузку вместо отображения inline.
Старайтесь избегать возврата объекта Response. Особенно не стоит использовать send() , таким образом вы вызываете его дважды (Диспетчеризация контроллера автоматически вызовет этот метод в конце запроса) и в итоге получите необычные результаты.
Загрузка записей в формате CSV/JSON и пр.
В случае если вы хотите, чтобы ваше индексированное представление данных было загружаемым, попробуйте также использовать расширения представления.
Здесь мы ссылаемся на то же действие, но с необходимым расширением:
echo $this->Html->link('As CSV', array('action' => 'index', 'ext' => 'csv'));
В нашем случае для CSV мы можем использовать CsvView чтобы легко переключаться с обычного вывода HTML на CSV:
if (!empty($this->request['params']['ext'])) { $this->viewClass = 'CsvView.Csv'; $this->set(compact('data', '_serialize')); return; }
Если необходимо, чтобы принудительная загрузка выполнялась только в отдельных случаях, используйте маленькую хитрость строки запроса, что и выше:
if ($this->request->query('download')) { $this->response->download($this->request->params['action'] . '.' . $this->request->params['ext']); }
И ссылку:
echo $this->Html->link('Download as CSV', array('action' => 'index', 'ext' => 'csv', '?' => array('download' => 1)));
Для того чтобы только ссылка содержащая .../action.csv?download=1 загружалась принудительно, а обычная сначала отображала данные в браузере.
То же самое относится к JSON и другим подходам, классов представления.
Смотрите пример в sandbox.
Автоматическое переключение классов просмотра
public $components = [ 'RequestHandler' => [ 'viewClassMap' => ['csv' => 'CsvView.Csv'] ] ];
Если расширение cvs, будет использоваться CsvView.