Yii2 - PHPSpreadSheet - Header already sent - php

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!

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');

Related

get empty file after download

i want to download a file of projet but i get it empty. i'am using a spreadsheet librairy
Notice : i a make a dump after save function , my file is full and not empty in the path directory of project
Someone can help me !
bellow is my code :
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('template.xlsx');
$worksheet = $spreadsheet->getActiveSheet();
$filename = 'write.xls';
$worksheet->getCell('A1')->setValue('John');
$worksheet->getCell('A2')->setValue('Smith');
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls');
$writer->save($filename); die;
// to download file
header('Content-Type: application/vnd.ms-excel');
header("Content-Length:".filesize($filename));
header("Content-Disposition: attachment;filename=$filename");
header('Cache-Control: max-age=0');
$writer->save('php://output');
exit();
i except a full file after downloading it
This function would work:
define ("ONE_DAY", 86400);
function getExisting()
{
$rootFolder = "pathTodirectory";
//first clear old files
$files = scandir($rootFolder,1);
array_pop($files); array_pop($files);
foreach($files as $file)
{
$fp = $rootFolder . DIRECTORY_SEPARATOR . $file;
$filemtime=filemtime($fp);
if (time() - $filemtime >= (2 * ONE_DAY))unlink($fp);
}//end clearing old files
//second rescan folder for current files
$files = scandir($rootFolder,1);
array_pop($files); array_pop($files);
$existing = array_reverse($files);
return $existing;
}
$existing = getExisting();
echo "\n<p> Select file or enter office number to review inventory:";
echo "\n <ul>";
foreach($existing as $rpt)
{
$spd = "pathTodirectory" . $rpt; \\make sure to follow up with relative path name here also
echo "\n <li><a href=\"$spd\" >" . $rpt ."</a></li>";
}
echo "\n </ul>";
I think it is the load() usage issue, your code works with following correction in my site :
$file_loc = 'template.xlsx';
$file_type = \PhpOffice\PhpSpreadsheet\IOFactory::identify($file_loc);
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($file_type);
// $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('template.xlsx');
$spreadsheet = $reader->load($file_loc);
$worksheet = $spreadsheet->getActiveSheet();
$filename = 'write.xls';
$worksheet->getCell('A1')->setValue('John');
$worksheet->getCell('A2')->setValue('Smith');
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls');
// save a physical file in server, you can skip this actually
$writer->save($target_dir . $filename);
// die; // don't die, be happy (^_^)
// to download file
header('Content-Type: application/vnd.ms-excel');
header("Content-Length:" . filesize($filename));
header("Content-Disposition: attachment;filename=$filename");
header('Cache-Control: max-age=0');
$writer->save('php://output');
exit();

PhpSpreadsheet - Download file instead of saving it

I need to generate an excel file (xls) and trigger the download after it is generated.
I found this example in the documentation.
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', 'Hello World !');
$writer = new Xlsx($spreadsheet);
$writer->save('hello world.xlsx');
It shows how to create a excel file and save it on the server.
How can I serve the result to the client instead and "force" him to download it?
I need to get the data of the $writer somehow.
I am currently solving it without PhpSpreadsheet:
// Excel Export
$filename = 'export_'.date('d-m-y').'.xls';
$filename = $validator->removeWhitespace($filename);
header('Content-type: application/ms-excel');
header('Content-Disposition: attachment; filename='.$filename);
exit($response["output"]); // <-- contains excel file content
But it is not working with my delimiter (semicolon). The semicolon is not getting interpreted and everything is getting written into one column.
If I export it as .csv, then it works. But I need it as .xls or .xlsx
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
class DownloadExcel
{
public static function createExcel(array $data, array $headers = [],
$fileName = 'data.xlsx')
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
for ($i = 0, $l = sizeof($headers); $i < $l; $i++) {
$sheet->setCellValueByColumnAndRow($i + 1, 1, $headers[$i]);
}
for ($i = 0, $l = sizeof($data); $i < $l; $i++) { // row $i
$j = 0;
foreach ($data[$i] as $k => $v) { // column $j
$sheet->setCellValueByColumnAndRow($j + 1, ($i + 1 + 1), $v);
$j++;
}
}
$writer = new Xlsx($spreadsheet);
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="'. urlencode($fileName).'"');
$writer->save('php://output');
}
}
This is what I use to create a spreadsheet with PhpSpreadsheet and output directly to php://output for download.
I had the same problem and found a solution here : https://github.com/PHPOffice/PhpSpreadsheet/issues/217
I ended my method with $writer->save('php://output'); then exit()
My answer :
PHP:
$writer = new Xlsx($spreadsheet);
ob_start();
$writer->save('php://output');
$ret['data'] = base64_encode(ob_get_contents());
ob_end_clean();
JS:
var linkSource = 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,'+ response.data ;
var downloadLink = document.createElement("a");
var fileName = 'clients.' + format;
downloadLink.href = linkSource;
downloadLink.download = fileName;
downloadLink.click();
I solved it with a workaround. I temporarily save the file on the server, then I load the content into a variable and serve it as a download file. Then I delete the file from the server.
Workaround:
$date = date('d-m-y-'.substr((string)microtime(), 1, 8));
$date = str_replace(".", "", $date);
$filename = "export_".$date.".xlsx";
try {
$writer = new Xlsx($response["spreadsheet"]);
$writer->save($filename);
$content = file_get_contents($filename);
} catch(Exception $e) {
exit($e->getMessage());
}
header("Content-Disposition: attachment; filename=".$filename);
unlink($filename);
exit($content);
call ob_end_clean(); just before the $writer->save('php://output').
ob_end_clean();
$writer->save('php://output');
This worked for me:
$excel = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $excel->getActiveSheet();
$sheet->setTitle('This is a test', true);
ob_end_clean();
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="filename_' . time() . '.xlsx"');
header('Cache-Control: max-age=0');
$xlsxWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($excel, 'Xlsx');
$xlsxWriter = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($excel);
exit($xlsxWriter->save('php://output'));
If you have problems where the files download corrupted, it is always good to check if there is any extra whitespace at the top of your file output. If your PHP files have blank white lines, whilst HTML won't have a problem, your phpspreadsheet file will. Spent a good chunk of time trying to fix these issues but the problem was with the whitespace!

Download differnt type of files using the same code PHP

I am trying to download files from server and it works fine for pdf files, I am wondering is there a way to download any type of files such as doc, zip,.. etc.
My code:
<?php
$doc = $sqlite->readDoc($documentId);
if ($doc != null) {
header("Content-Type:" . $doc['mime_type']);
header('Content-disposition: attachment; filename="something"');
print $doc['doc'];
} else {
echo 'Error occured while downloading the file';
}
?>
You'll want to identify the file - maybe using this function:
http://php.net/manual/en/function.mime-content-type.php
and then you can create a switch to indicate the content-type and appropriate disposition. There's a decent example here: PHP - send file to user
A simple function that can be used to download any file formate form path.
function DownloadFile($strFilePath)
{
$strContents = file_get_contents(realpath($strFilePath));
if (strpos($strFilePath,"\\") > -1 )
$strFileName = substr($strFilePath,strrpos($strFilePath,"\\")+1);
else
$strFileName = substr($strFilePath,strrpos($strFilePath,"/")+1);
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"${strFileName}\"");
echo $strContents;
}
Usage Example
$file = $_GET['file_id'];
$path = "../uploads/".$file; // change the path to fit your websites document structure
DownloadFile($path);

PHPExcel Clone Worksheet - Efficient use of memory

I need to clone the first worksheet a few times, accordingly to the amount of rows, but something may be wrong.
The code is:
public function downloadFile()
{
date_default_timezone_set('America/Sao_Paulo');
if(file_exists("xpto.xlsx")){
$objPHPExcel = PHPExcel_IOFactory::load("xpto.xlsx");
$sheets = 3;//3 is enough to throw the error
for($i = 0; $i<$sheets; $i++){
$objClonedWorksheet = clone $objPHPExcel->getSheet(0);
$objClonedWorksheet->setTitle('Sheet ' . $i);
$objClonedWorksheet->setCellValue('A1', 'Test ' . $i);
$objPHPExcel->addSheet($objClonedWorksheet);
}
$objPHPExcel->setActiveSheetIndex(0);
$filename = 'file.xlsx';
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="'.$filename.'"');
header('Cache-Control: max-age=0');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
ob_end_clean();
$ret = $objWriter->save('php://output');
exit;
}
}
But I got an exhausted memory error. Than I tried the most commented solution (that is actually an workaround) that is to add
ini_set('memory_limit', '-1');
I added this line just after the load function and it worked, but I don't think it is a good solution to use on a SaaS application. I don't even think most hosts (AWS, for example) will allow me to use that.
I also tried to clone the sheet before the for loop, but when use addSheet, I realized that this function doesn't create a new object and when I change the name of the sheet (by the second iteration of the for loop), it changes the last sheet created, throwing an "already existing sheet with the same name" error.
Trying to use one of the links #rhazen listed, I changed the for loop to:
$objFromSheet = $objPHPExcel->getSheet(0);
$sheets = 3;
for($i = 1; $i<=$sheets; $i++){
$objToSheet = $objPHPExcel->createSheet($i);
foreach($objFromSheet->getRowIterator() as $row){
$cellIterator = $row->getCellIterator();
$cellFrom = $cellIterator->current();
$cellTo = $objToSheet->getCell($cellFrom->getCoordinate());
$cellTo->setXfIndex($cellFrom->getXfIndex());
$cellTo->setValue($cellFrom->getValue());
}
}
But it seems not to work either. Is there a misunderstanding about Iterator or XfIndex?
The solution is in the edited question. Thanks for those who helped.

PHPExcel passing PHP variables into spreadsheet properties

<?php
//Initialize variables for downloads
$report_name = $_POST['herd-reports-download-name'];
$filetype = $_POST['group1'];
$farm_id = $_POST['farm_id'];
$username = $_POST['user_name'];
$json_array = json_decode($_POST['table_data'], true);
$wksDescription = "Report generated for farm";
//Branch casing depending on filetype
switch($filetype) {
case 'PDF':
generate_pdf();
break;
case 'Excel':
generate_excel();
break;
}
//Functions for downloads here.
function generate_pdf() {
//echo 'pdf';
}
function generate_excel() {
//echo 'excel';
require '../includes/libraries/PHPExcel/Classes/PHPExcel.php';
$objPHPExcel = new PHPExcel(); //Spreadsheet generated, one worksheet is always added to it.
$cacheMethod = PHPExcel_CachedObjectStorageFactory::cache_in_memory_serialized;
PHPExcel_Settings::setCacheStorageMethod($cacheMethod); //Serializes to cut memory usage slightly at a minor performance hit.
$objPHPExcel->getProperties()->setCreator("myself")
->setLastModifiedBy("myself")
->setTitle("Report for a farm")
->setSubject("A report")
->setDescription($wksDescription)
->setKeywords("office 2007 etc")
->setCategory("Report File");
$objPHPExcel->setActiveSheetIndex(0)
->setCellValue('A1', 'Snoop Dawg');
//Once it's done, begin prep to output to browser
$objPHPExcel->setActiveSheetIndex(0);
header('Content-Type: application/vnd.ms-excel');
header("Content-Disposition: attachment;filename=vroom");
header('Cache-Control: max-age=1');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5');
$objWriter->save('php://output');
}
?>
My question is on setDescription() and setting the file name, why won't php variables work? They are currently returning nothing in those respective areas. It's probably a matter of manipulating them somehow, as setDescription() takes a string as an argument, and I'm passing a string, but it doesn't work. Nothing is being returned.
The file downloads fine, but properties that aren't strictly a string being passed aren't being set.
try making that variable global within the function.
function generate_excel() {
global $wksDescription;
The variable doesn't exist within the scope of the function unless you pull it in from the outside.

Categories