-1

I have a button on a modal and when clicked I want to generate an Excel workbook based off of a xls template file and then prompt it for the user to download/open.

The button uses javascript to open a new blank window

The click event simply does

var tsUrl = baseUrl + 'index.php?r=office/gen-t-s-xls&proj='+projNo+'&leg='+legNo+'&driver='+driverId;
var win = window.open(tsUrl, '_blank');

and calls the following controller Action

public function actionGenTSXls($proj, $leg, $driver){
    // https://phpspreadsheet.readthedocs.io/en/develop/topics/recipes/
    if(!is_numeric($proj) || !is_numeric($leg) || !is_numeric($driver)){
        echo 'Invalid entry.';
        die();
    }
    //Get the Project Model
    $model = Projects::find()
             ->where(['=', 'ProjNo', $proj])
             ->one();
    if (!$model) {
        echo "There was a problem with the submitted request. (1)";
        die();
    }
    //Get the Project Leg Model
    $modelLeg = ProjectsLegs::find()
                ->where(['=', 'ProjId', $model->ProjId])
                ->andWhere(['=', 'LegNo', $leg])
                ->andWhere(['=', 'DriverId', $driver])
                ->one();
    if (!$modelLeg) {
        echo "There was a problem with the submitted request. (2)";
        die();
    }

    $timestamp = date('Ymd\THis');  // Date/Time stamp
    $filename = 'TripSheet_'.$model->ProjNo.'_'.$modelLeg->LegId.'_'.$timestamp.'.xls';
    if(YII_ENV_DEV){
        $filepath = Yii::$app->basePath.'\web\files\excel\\';
    }else{
        $filepath = \Yii::getAlias('@webroot').'/files/excel/';
    }
    $file = $filepath . $filename;
    header('Content-Type: application/vnd.ms-excel');
    header('Content-Disposition: attachment;filename="'.$filename.'"');
    header('Cache-Control: max-age=0');

    $sFile = Yii::$app->basePath.DIRECTORY_SEPARATOR.'templates'.DIRECTORY_SEPARATOR.'TS_Tmplt.xlsx';
    $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load( $sFile );
    $spreadsheet->getProperties()
                ->setCreator("TS")
                ->setLastModifiedBy(User::findOne(Yii::$app->user->getId())->Dispatcher)
                ->setTitle("TS ".$model->ProjNo.'_'.$modelLeg->LegId)
                ->setSubject("TS ".$model->ProjNo.'_'.$modelLeg->LegId)
                ->setDescription("TS ".$model->ProjNo.'_'.$modelLeg->LegId);
    //working on something with the spreadsheet/worksheet
    $sheet = $spreadsheet->getSheetByName('Sheet1');

    //Date
    $sheet->setCellValue('K3', date("n/j/Y"));
    $sheet->getStyle('K3')
          ->getNumberFormat()
          ->setFormatCode('m/d/yy');
    //Order
    if(!is_null($model->ProjNo)){$sheet->setCellValue('K4', $model->ProjNo);}
    //Carrier Order
    if(!is_null($model->CompTripNo)){
        $sheet->setCellValue('K5', $model->CompTripNo);
        $sheet->getStyle('K5')->getAlignment()->setShrinkToFit(true);
    }
    //Dispatcher
    if(!is_null($model->DispatcherId)){$sheet->setCellValue('J11', User::findOne($modelLeg->DispatcherId)->Dispatcher);}
    //Company Name
    if(!is_null($model->ClientId)){
        $sheet->setCellValue('E4', Clients::findOne($model->ClientId)->Company);
        $sheet->getStyle('E4')->getAlignment()->setShrinkToFit(true);
    }
    //Start Date
    if(!is_null($modelLeg->StartDt)){
        $date = \DateTime::createFromFormat('Y-m-d H:i:s', $modelLeg->StartDt);
        $cellVal = $date->format("n/j/Y g:i A");
        $sheet->setCellValue('E8', $cellVal); 
    }

    $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls');
    $writer->save('php://output');
}

Now, in a general sense, it works. It create the xls, the user is prompted to open/download. But it generates an error in my yii log.

[error][yii\web\HeadersAlreadySentException] exception 'yii\web\HeadersAlreadySentException' with message 'Headers already sent

I've looked at countless similar thread, and tried all sorts of permutations involving

ob_start();
if (ob_get_length()) ob_end_clean();
if (ob_get_contents()) ob_end_clean();
//etc...

nothing seems to get me around my current issue.

I've been turning in circles for quite some time (not a lack of trying, doing research, ...) and thought I'd get some experts advice and learn. I am a novice, so if you can explain so I can learn it would be greatly appreciated.

Update 1

I decided to simplify my code to see what the issue was, so I generated the xls file, so I could temporarily eliminate that code and even so, with only 7 lines of code, I get the same error!

public function actionDownload(){
    $filename = "TS.xls";
    $file = "C:\Users\Dev\Downloads\TS.xls";
    $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load( $file );
    $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter( $spreadsheet, 'Xls');
    header('Content-Type: application/vnd.ms-excel');
    header('Content-Disposition: attachment;filename="'.$filename.'"');
    $writer->save("php://output");
}

Update 2 / Workaround

Did more Googling, and found a posting that I tried. By adding die(); at the very end of my action, the error goes away?! Can someone explain. I'm assuming this is not a proper solution though even if it does appear to work.

Update 3 / Solution

This ended up being the proper way to handle this

replace the following part of the code

header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment;filename="'.$filename.'"');
$writer->save("php://output");

with

$response = Yii::$app->getResponse();
$headers = $response->getHeaders();
$headers->set('Content-Type', 'application/vnd.ms-excel');
$headers->set('Content-Disposition', 'attachment;filename="'.$filename.'"');
$headers->set('Cache-Control: max-age=0');
ob_start();
$writer->save("php://output");
$content = ob_get_contents();
ob_clean();
return $content;

and a big thank you to all of you for taking the time to try and help me. Your time and efforts are greatly appreciated!

Still Learning
  • 130
  • 2
  • 12
  • Echo in some cases might result in such an error. Try return or throw an error. – Alex Stulov Jul 05 '19 at 05:08
  • First piece of “expert advice” - _pay attention_ to what error messages are actually trying to tell you. A “Headers already sent” error message usually contains a clear mention of where the first output was created, file & line number. So go check what is happening at that place in your code. – misorude Jul 05 '19 at 06:31
  • It's complaining about the vendor librairy vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php on line 292. which I doubt is the issue. It has to be my code. – Still Learning Jul 05 '19 at 08:36
  • You've mentioned line 252 and 292 now but neither one of those in that code does anything to output header information. That aside, If you already have a XLS file why don't you just send it directly to the browser? Creating a writer etc isn't even needed. – Dave Jul 05 '19 at 10:28
  • Dave. Could you explain, give an example please. Also, the existing xls is used as a template, then I enter values in various cells, and that is the final output I want the user to be able to download. – Still Learning Jul 05 '19 at 13:04
  • Alex, I removed the echos and it made no difference. – Still Learning Jul 05 '19 at 13:20
  • misorude, the code being flagged is from the PHPOffice/PHPSpreadsheet library. Considering how widespread it is, I highly doubt the true root cause lies there, nor would I have the skillset to address it. I checked their github and don't see anyone flagging this issue either. Thus I keep coming back to my controller being flawed. – Still Learning Jul 05 '19 at 13:22

1 Answers1

1

Right before your first header call add:

ob_end_clean();  // clear out anything that may have already been output

Your code would look something like:

$file = $filepath . $filename;
ob_end_clean();  // clear out anything that may have already been output
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment;filename="'.$filename.'"');
header('Cache-Control: max-age=0');
Dave
  • 5,108
  • 16
  • 30
  • 40
  • It doesn't seem to make any difference. It always err stating vendor\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Shared\OLE\PPS\Root.php on line 252 – Still Learning Jul 05 '19 at 09:56