Search code examples
phpcakephpsavephpexcelcakephp-2.x

cakePHP - PHP EXCEL saves as .html


I have implemented PHPExcel within my CakePHP application, this is my helper:

<?php
App::uses('AppHelper', 'Helper');

/**
 * Helper for working with PHPExcel class.
 * PHPExcel has to be in the vendors directory.
 */

class PhpExcelHelper extends AppHelper {
  /**
   * Instance of PHPExcel class
   * @var object
   */
  public $xls;
  /**
   * Pointer to actual row
   * @var int
   */
  protected $row = 1;
  /**
   * Internal table params 
   * @var array
   */
  protected $tableParams;

  /**
   * Constructor
   */
  public function __construct(View $view, $settings = array()) {
        parent::__construct($view, $settings);
    }

  /**
   * Create new worksheet
   */
  public function createWorksheet() {
    $this->loadEssentials();
    $this->xls = new PHPExcel();
  }

  /**
   * Create new worksheet from existing file
   */
  public function loadWorksheet($path) {
    $this->loadEssentials();
    $this->xls = PHPExcel_IOFactory::load($path);
  }

  /**
   * Set row pointer
   */
  public function setRow($to) {
    $this->row = (int)$to;
  }

  /**
   * Set default font
   */
  public function setDefaultFont($name, $size) {
    $this->xls->getDefaultStyle()->getFont()->setName($name);
    $this->xls->getDefaultStyle()->getFont()->setSize($size);
  }

  /**
   * Start table
   * inserts table header and sets table params
   * Possible keys for data:
   *  label   - table heading
   *  width - "auto" or units
   *  filter  - true to set excel filter for column
   *  wrap  - true to wrap text in column
   * Possible keys for params:
   *  offset  - column offset (numeric or text)
   *  font  - font name
   *  size  - font size
   *  bold  - true for bold text
   *  italic  - true for italic text
   *  
   */
  public function addTableHeader($data, $params = array()) {
    // offset
    if (array_key_exists('offset', $params))
      $offset = is_numeric($params['offset']) ? (int)$params['offset'] : PHPExcel_Cell::columnIndexFromString($params['offset']);
    // font name
    if (array_key_exists('font', $params))
      $this->xls->getActiveSheet()->getStyle($this->row)->getFont()->setName($params['font_name']);
    // font size
    if (array_key_exists('size', $params))
      $this->xls->getActiveSheet()->getStyle($this->row)->getFont()->setSize($params['font_size']);
    // bold
    if (array_key_exists('bold', $params))
      $this->xls->getActiveSheet()->getStyle($this->row)->getFont()->setBold($params['bold']);
    // italic
    if (array_key_exists('italic', $params))
      $this->xls->getActiveSheet()->getStyle($this->row)->getFont()->setItalic($params['italic']);

    // set internal params that need to be processed after data are inserted
    $this->tableParams = array(
      'header_row' => $this->row,
      'offset' => $offset,
      'row_count' => 0,
      'auto_width' => array(),
      'filter' => array(),
      'wrap' => array()
    );

    foreach ($data as $d) {
      // set label
      $this->xls->getActiveSheet()->setCellValueByColumnAndRow($offset, $this->row, $d['label']);
      // set width
      if (array_key_exists('width', $d)) {
        if ($d['width'] == 'auto')
          $this->tableParams['auto_width'][] = $offset;
        else
          $this->xls->getActiveSheet()->getColumnDimensionByColumn($offset)->setWidth((float)$d['width']);
      }
      // filter
      if (array_key_exists('filter', $d) && $d['filter'])
        $this->tableParams['filter'][] = $offset;
      // wrap
      if (array_key_exists('wrap', $d) && $d['wrap'])
        $this->tableParams['wrap'][] = $offset;

      $offset++;
    }
    $this->row++; 
  }

  /**
   * Write array of data to actual row
   */
  public function addTableRow($data) {
    $offset = $this->tableParams['offset'];

    foreach ($data as $d) {
      $this->xls->getActiveSheet()->setCellValueByColumnAndRow($offset++, $this->row, $d);
    }
    $this->row++;
    $this->tableParams['row_count']++;
  }

  /**
   * End table
   * sets params and styles that required data to be inserted
   */
  public function addTableFooter() {
    // auto width
    foreach ($this->tableParams['auto_width'] as $col)
      $this->xls->getActiveSheet()->getColumnDimensionByColumn($col)->setAutoSize(true);
    // filter (has to be set for whole range)
    if (count($this->tableParams['filter']))
      $this->xls->getActiveSheet()->setAutoFilter(PHPExcel_Cell::stringFromColumnIndex($this->tableParams['filter'][0]).($this->tableParams['header_row']).':'.PHPExcel_Cell::stringFromColumnIndex($this->tableParams['filter'][count($this->tableParams['filter']) - 1]).($this->tableParams['header_row'] + $this->tableParams['row_count']));
    // wrap
    foreach ($this->tableParams['wrap'] as $col)
      $this->xls->getActiveSheet()->getStyle(PHPExcel_Cell::stringFromColumnIndex($col).($this->tableParams['header_row'] + 1).':'.PHPExcel_Cell::stringFromColumnIndex($col).($this->tableParams['header_row'] + $this->tableParams['row_count']))->getAlignment()->setWrapText(true);
  }

  /**
   * Write array of data to actual row starting from column defined by offset
   * Offset can be textual or numeric representation
   */
  public function addData($data, $offset = 0) {
    // solve textual representation
    if (!is_numeric($offset))
      $offset = PHPExcel_Cell::columnIndexFromString($offset);

    foreach ($data as $d) {
      $this->xls->getActiveSheet()->setCellValueByColumnAndRow($offset++, $this->row, $d);
    }
    $this->row++;
  }

  /**
   * Output file to browser
   */
  public function output($filename = 'export.xlsx') {
    // set layout
    $this->View->layout = '';
    // headers
    header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    header('Content-Disposition: attachment;filename="'.$filename.'"');
    header('Cache-Control: max-age=0');
    // writer
    $objWriter = PHPExcel_IOFactory::createWriter($this->xls, 'Excel2007');
    $objWriter->save('php://output');
    // clear memory
    $this->xls->disconnectWorksheets();
  }

  /**
   * Load vendor classes
   */
  protected function loadEssentials() {
    // load vendor class
    App::import('Vendor', 'PHPExcel/Classes/PHPExcel');
    if (!class_exists('PHPExcel')) {
      throw new CakeException('Vendor class PHPExcel not found!');
    }
  }
}

And this is my controller:

    public $helpers = array('PhpExcel');
    ...
    public function excel() {
        $this->set('participants', $this->Participant->find('all'));
    } 

This is my view:

<?php
    $this->PhpExcel->createWorksheet();
    $this->PhpExcel->setDefaultFont('Calibri', 12);

    // define table cells
    $table = array(
        array('label' => __('id'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Förnamn'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Efternamn'), 'width' => 'auto', 'filter' => true),
        array('label' => __('E-postadress'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Mobiltelefon'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Specialkost'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Enhet'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Seminarium'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Utanför Stockholm'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Dela rum'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Transfer'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Bara där på dagen'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Låt'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Lärare som blivit hyllad'), 'width' => 'auto', 'filter' => true),
        array('label' => __('Kommentar'), 'width' => 'auto', 'filter' => true),
    );

    // heading
    $this->PhpExcel->addTableHeader($table, array('name' => 'Cambria', 'bold' => true));

    foreach ($participants as $d) {
        $this->PhpExcel->addTableRow(array(
            $d['Participant']['id'],
            $d['Participant']['f_name'],
            $d['Participant']['l_name'],
            $d['Participant']['email'],
            $d['Participant']['mobile_phone'],
            $d['Participant']['special'],
            $d['Participant']['school'],
            $d['Participant']['seminarium_id'],
            $d['Participant']['outside_sthlm'],
            $d['Participant']['share_room'],
            $d['Participant']['transfer'],
            $d['Participant']['only_day'],
            $d['Participant']['song'],
            $d['Participant']['teacher'],
            $d['Participant']['comments']
        ));
    }

    $this->PhpExcel->addTableFooter();
    $this->PhpExcel->output();
    $this->PhpExcel->exit();
?>

When I try to download this in firefox I get the right file extension, xlsx but when trying to download with safari it gives me report.xlsx.html ? and the file becomes useless unless you rename it to report.xlsx, How come?


Solution

  • First of all, you should set your 'layout' to 'false' to prevent the view being loaded in the default (HTML) layout of your website.

    Also set the response-type to the type for Excel (as others have mentioned. You can do so via the response-object (http://book.cakephp.org/2.0/en/controllers/request-response.html#dealing-with-content-types)

    And finally, do not exit(); in your helper. If you're not rendering your view in a layout, you don't have to exit after outputting the result

    In your controller;

    public $helpers = array('PhpExcel');
    ...
    public function excel() {
        // disable the layout
        $this->layout = false;
    
        // Add/define XLS contenttype
        $this->response->type(array('xls' => 'application/vnd.ms-excel'));
    
        // Set the response Content-Type to xls
        $this->response->type('xls');
    
        $this->set('participants', $this->Participant->find('all'));
    }
    

    note As Mark Baker mentioned, XLS and XLSX use a different mime-type, my example uses the mime type for 'classic' XLS, if you're outputting XLSX, modify the mime type accordingly