PHPExcel Clone Worksheet - Efficient use of memory - php

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.

Related

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!

Yii2 - PHPSpreadSheet - Header already sent

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

Selective download with PHPExcel

I'm using PHPExcel to download some data stored in MySQLi. I made an algorithm that is working for every data base (in theory). I have tested it with some of them and it was working fine.
I extracted names of the columns in an array: column_names and then, I'm adding titles and data to the excel report.
// Adding titles
$objPHPExcel->setActiveSheetIndex(0)
->setCellValue('A1',$bigTitle);
$counter = 0;
$let = 'a';
while ($counter <= count($column_names)){
$objPHPExcel->setActiveSheetIndex(0)
->setCellValue(strtoupper($let).'3', $column_names[$counter]);
$let++;
$counter++;
}
//Adding data
$i = 4;
while ($row = $result->fetch_array()) {
$counter = 0;
$let = 'a';
while ($counter <= count($column_names)){
$objPHPExcel->setActiveSheetIndex(0)
->setCellValue(strtoupper($let).$i, $row[$column_names[$counter]]);
$let++;
$counter++;
}
$i++;
}
I'm connecting to the database using
$conexion = new mysqli('localhost','user','pass','SAT_dbname',21);
I cloned "SAT_db1" database to "SAT_db2". They have exactly the same structure but different information. The download is working if I'm using
$conexion = new mysqli('localhost','user','pass','SAT_db1',21);
But it's not working if I'm using
$conexion = new mysqli('localhost','user','pass','SAT_db2',21);
I don't know what is wrong if they're the same with different names. Is not PHPExcel working with cloned databases? What else could it be?
The error shows up in the browser as "File not found".
EDIT
I was testing the download all day and I finally found something: I can download when I have few registers. When I have a few more, I can't.
I'm sending the file to the browser:
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="Report.xlsx"');
header('Cache-Control: max-age=0');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$objWriter->save('php://output');
Still haven't found a solution.
PHPExcel has limits with cache. I had to change these limits manually. I used this code before creating PHPExcel object:
set_time_limit(0) ;
$cacheMethod = PHPExcel_CachedObjectStorageFactory:: cache_to_phpTemp;
$cacheSettings = array( 'memoryCacheSize' => '500MB');
PHPExcel_Settings::setCacheStorageMethod($cacheMethod, $cacheSettings);
I assume if you have bigger files, you can expand "memoryCacheSize".

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.

PHPExcel Set cell with variable

I have this test script that is acting just like my production script except production script pulls from a SQL db. I want to set the cell placement using a variable based off the count of an array. In the script below, (which you can copy and run as an example) I want to set the cell number based off how many of the same car that is in an array. So for an example there are 7 BMW's listed in the array. So I want the Comments: data copied to cell A12 using the code $num=$no+5; $cell='A'.$num;
The problem I am having is it does place it on that line but it also places it 7 more times about it. If there are 4 cars of that type it would place it on the right line but also place it 3 times above it. I just want it to place it once at the desired location. Any help would be great. Here is the code:
<?PHP
require_once 'Classes/PHPExcel.php';
include 'Classes/PHPExcel/Writer/Excel2007.php';
$dataArray= array();
$cars=array("Versa","Volt","Volt","Volt","Volt","Volkswagen","Bentley","Benz","BMW","BMW","BMW","BMW","BMW","BMW","BMW","Cobra","Cord","Daewoo","Datsun","Dodge","Dodge","Dixi");
$objPHPExcel = new PHPExcel();
$objPHPExcel->setActiveSheetIndex(0);
while (list($var, $val) = each($cars)) {
if ($val!=$id){
$dataArray = array();
$objWorksheet = new PHPExcel_Worksheet($objPHPExcel);
$objPHPExcel->addSheet($objWorksheet);
$objWorksheet->setTitle(''. $val);
$row_array[$val] = $val;
array_push($dataArray,$row_array);
$no = count($dataArray);
$num=$no+5;
$cell='A'.$num;
$objWorksheet->setCellValue('A1' , $no);
$objWorksheet->setCellValue('A2' , $val);
$objWorksheet->setCellValue($cell , 'Comments:');
$id = $val;
}
else {
$row_array[$val] = $val;
$count=count($cars);
$count2=count($loc);
array_push($dataArray,$row_array);
$no = count($dataArray);
$num=$no+5;
$cell='A'.$num;
$objWorksheet->setCellValue('A2' , $no);
$objWorksheet->setCellValue('A3' , $val);
$objWorksheet->setCellValue($cell , 'Comments:');
$id = $val;
}
}
// Save Excel 2007 file
#echo date('H:i:s') . " Write to Excel2007 format\n";
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
ob_end_clean();
// We'll be outputting an excel file
header('Content-type: application/vnd.ms-excel');
// It will be called file.xls
header('Content-Disposition: attachment; filename="cars.xlsx"');
$objWriter->save('php://output');
Exit;
?>
This fixes the problem.
<?PHP
require_once 'Excel/PHPExcel.php';//path for my config, rewrite for yours
//include 'Classes/PHPExcel/Writer/Excel2007.php'; // not needed, lazy loader job
$cars=array("Versa","Volt","Volt","Volt","Volt","Volkswagen","Bentley","Benz","BMW","BMW","BMW","BMW","BMW","BMW","BMW","Cobra","Cord","Daewoo","Datsun","Dodge","Dodge","Dixi");
$objPHPExcel = new PHPExcel();
$objWorksheet=$objPHPExcel->setActiveSheetIndex(0);
$id='';
$countRows=0;
while (list($var, $val) = each($cars)) {
if ($val!=$id && $id!=''){
$objWorksheet->setTitle($id);
$num=$countRows+5;
$cell='A'.$num;
$objWorksheet->setCellValue($cell , 'Comments:');
$objWorksheet = new PHPExcel_Worksheet($objPHPExcel);
$objPHPExcel->addSheet($objWorksheet);
$id = $val;
$countRows=0;
}//end if
if($id=='') $id=$val;//the first car
$no = ++$countRows;
$objWorksheet->setCellValue('A'.$no , $no);
$objWorksheet->setCellValue('B'.$no , $id);//Versa, Volt, ...
}
if($countRows>0 && $id!=''){// the last car - if $id=='' the workbook is empty
$objWorksheet->setTitle($id);
$num=$countRows+5;
$cell='A'.$num;
$objWorksheet->setCellValue($cell , 'Comments:');
}//end if
// Save Excel 2007 file
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
ob_end_clean();
// We'll be outputting an excel file
header('Content-type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); // note : use correct mime type (not xls for xlsx)
// It will be called cars.xlsx
header('Content-Disposition: attachment; filename="cars.xlsx"');
$objWriter->save('php://output');
Exit;
?>

Categories