excel.class.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <?php
  2. /**
  3. * The excel library of zdoo, can be used to export excel file.
  4. *
  5. * @copyright Copyright 2009-2016 青岛易软天创网络科技有限公司(QingDao Nature Easy Soft Network Technology Co,LTD, www.cnezsoft.com)
  6. * @license ZPL (http://zpl.pub/page/zplv12.html)
  7. * @author Gang Liu <liugang@cnezsoft.com>
  8. * @author yaozeyuan <yaozeyuan@cnezsoft.com>
  9. * @package Excel
  10. * @version $Id$
  11. * @link http://www.zdoo.com
  12. *
  13. * excel - a library to export excel file, depends on phpexcel.
  14. *
  15. * Here are some tips of excelData(named $data) structure maybe uesful. you can use these to create xls/xlsx file . The API is interchangeable.
  16. *
  17. * Base property:
  18. * data->fileName set the fileName
  19. * data->kind set the kind of excel module
  20. * data->fields is array like ($field => $fieldTitle). The order of data->fields is also the column's order
  21. * data->row is array like ($rowNumber => array($field => $value)).This is the data of Excel file. System will fill $value as data into every cell according to $rowNumber and $field
  22. *
  23. * Merge cell
  24. * if there is set data->dataRowspan[$row][$field] or data->dataColspan[$row][$field], the cells will be merge
  25. * data->dataRowspan[$row][$field] => merge excelColumns[$field]:$rowNumber to excelColumns[$field]:($rowNumber + data->dataRowspan[$row][$field]) into one cell
  26. * data->dataColspan[$row][$field] => merge excelColumns[$field]:$rowNumber to transIntoExcelKey(int(excelColumns[$field]) + dataColspan[$row][$field]):$rowNumber into one cell
  27. *
  28. * html content
  29. * if you set config->excel->editor[$this->rawExcelData->kind] like 'excelColumns1,excelColumns2...', and excelColumns in that, then $value of all column's cell will be process to remove html tag
  30. *
  31. * write sysData and use droplist style on cell
  32. * sysData is an array like (excelColumns => valueList), system use this sheet to store extraData.
  33. * if you want to have droplist style on some column in sheet1, you can set data->($exceKey . 'List'), data->listStyle and data->sysData sothat the data will be writen into the sysData sheet and you can see the droplist style is enable.
  34. * the data->listStyle and data->sysData is an array of series value like ['dropListStyleColumnName' . 'List'] , like ('nameList', 'categoryList', ...) the dropListStyleColumnName used to indicate witch column need that style and data->[dropListStyleColumnName . 'List'] use to transfer data for system get real data to build datalist in sysdata sheet.
  35. *
  36. * FreezePane
  37. * if you set config->excel->freeze->{$this->data->kind}->targetColumnName, this column will be freezed
  38. *
  39. * Set width
  40. * You can set $data->customWidth like array($field => width, $field2 => width, $field3 => width,...) to set width by you like
  41. * or modify
  42. * config->excel->titleFields
  43. * config->excel->centerFields
  44. * config->excel->dateFields
  45. * to have default style
  46. *
  47. * color
  48. * if you set data->nocolor, the excel file won't have color
  49. *
  50. * File title
  51. * The lang->excel->title->{data->kind} is the title of data->kind excel file
  52. *
  53. * SysData title
  54. * The lang->excel->title->sysValue is the name of sysData sheet , this is only can use for xls file.
  55. */
  56. class excel extends model
  57. {
  58. /**
  59. * __construct
  60. *
  61. * @access public
  62. * @return void
  63. */
  64. public function __construct()
  65. {
  66. parent::__construct();
  67. $this->phpExcel = $this->app->loadClass('phpexcel');
  68. $this->file = $this->loadModel('file');
  69. $this->sysDataColIndex = 0;
  70. $this->hasSysData = false;
  71. }
  72. /**
  73. * Init for excel data.
  74. *
  75. * @param int $data
  76. * @access public
  77. * @return void
  78. */
  79. public function init($data)
  80. {
  81. $this->rawExcelData = $data;
  82. $this->fields = $data->fields;
  83. $this->rows = $data->rows;
  84. $this->headerRowCount = isset($data->headerRowCount) ? $data->headerRowCount : 1;
  85. $this->fieldsKey = $this->headerRowCount == 1 ? array_keys($this->fields) : array_keys(reset($this->fields));
  86. }
  87. /**
  88. * Export data to Excel. This is main function.
  89. *
  90. * @param object $data
  91. * @param string $fileType xls | xlsx
  92. * @param string $savePath, if $savePath != '', then the file will save in $savePath
  93. * @access public
  94. * @return void
  95. */
  96. public function export($excelData, $fileType = 'xls', $savePath = '', $schemaSheets = '', $schemaSheetIndex = null)
  97. {
  98. $sheetIndex = 0;
  99. foreach($excelData->dataList as $data) $this->phpExcel->createSheet(); // Create sheets.
  100. foreach($excelData->dataList as $data)
  101. {
  102. $this->init($data);
  103. $this->excelColumns = array();
  104. foreach($this->fieldsKey as $colIndex => $field) $this->excelColumns[$field] = $this->setExcelField($colIndex);
  105. /* Set file base property */
  106. $excelProps = $this->phpExcel->getProperties();
  107. $excelProps->setCreator('ZenTao');
  108. $excelProps->setLastModifiedBy('ZenTao');
  109. $excelProps->setTitle('Office XLS Document');
  110. $excelProps->setSubject('Office XLS Document');
  111. $excelProps->setDescription('Document generated by PhpSpreadsheet.');
  112. $excelProps->setKeywords('office excel PhpSpreadsheet');
  113. $excelProps->setCategory('Result file');
  114. $excelSheet = $this->phpExcel->getSheet($sheetIndex);
  115. $sheetTitle = isset($this->rawExcelData->title) ? $this->rawExcelData->title : $this->rawExcelData->kind;
  116. if($sheetTitle) $excelSheet->setTitle($sheetTitle);
  117. $sheetRow = 1;
  118. if($this->headerRowCount == 1)
  119. {
  120. foreach($this->fields as $field => $fieldName)
  121. {
  122. $cell = $this->excelColumns[$field] . $sheetRow;
  123. $excelSheet->setCellValueExplicit($cell, $fieldName, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
  124. }
  125. $sheetRow++;
  126. }
  127. else
  128. {
  129. foreach($this->fields as $fieldIndex => $fields)
  130. {
  131. foreach($fields as $field => $fieldName)
  132. {
  133. if(isset($this->excelColumns[$field]))
  134. {
  135. $cell = $this->excelColumns[$field] . $sheetRow;
  136. /* Merge Cells.*/
  137. if(isset($this->rawExcelData->headerRowspan[$fieldIndex][$field]) && is_int($this->rawExcelData->headerRowspan[$fieldIndex][$field]))
  138. {
  139. $endCell = $this->excelColumns[$field] . ($sheetRow + $this->rawExcelData->headerRowspan[$fieldIndex][$field] - 1);
  140. $excelSheet->mergeCells($cell . ":" . $endCell);
  141. }
  142. if(isset($this->rawExcelData->headerColspan[$fieldIndex][$field]) && is_int($this->rawExcelData->headerColspan[$fieldIndex][$field]))
  143. {
  144. $column = $this->setExcelField($this->rawExcelData->headerColspan[$fieldIndex][$field] - 1, $this->excelColumns[$field]);
  145. $endCell = $column . $sheetRow;
  146. $excelSheet->mergeCells($cell . ":" . $endCell);
  147. }
  148. $excelSheet->setCellValueExplicit($cell, $fieldName, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
  149. }
  150. }
  151. $sheetRow++;
  152. }
  153. }
  154. /* Write system data in excel.*/
  155. $this->writeSysData();
  156. $skipNumberCell = array(); // When merging cells, data cannot be merged. When assigning values to cells, it is necessary to skip merging data other than the first row
  157. foreach($this->rows as $rowIndex => $row)
  158. {
  159. foreach($row as $field => $value)
  160. {
  161. if(isset($this->excelColumns[$field]))
  162. {
  163. $cell = $this->excelColumns[$field] . $sheetRow;
  164. /* Merge Cells.*/
  165. if (isset($this->rawExcelData->dataRowspan[$rowIndex][$field]) && is_int($this->rawExcelData->dataRowspan[$rowIndex][$field]))
  166. {
  167. $endCell = $this->excelColumns[$field] . ($sheetRow + $this->rawExcelData->dataRowspan[$rowIndex][$field] - 1);
  168. /* Merge underlying data. */
  169. for($row = $sheetRow + 1; $row <= $sheetRow + $this->rawExcelData->dataRowspan[$rowIndex][$field] - 1; $row++)
  170. {
  171. $skipNumberCell[] = $this->excelColumns[$field] . $row;
  172. }
  173. $excelSheet->mergeCells($cell . ":" . $endCell);
  174. }
  175. if(isset($this->rawExcelData->dataColspan[$rowIndex][$field]) && is_int($this->rawExcelData->dataColspan[$rowIndex][$field]))
  176. {
  177. $column = $this->setExcelField($this->rawExcelData->dataColspan[$rowIndex][$field] - 1, $this->excelColumns[$field]);
  178. $endCell = $column . $sheetRow;
  179. $excelSheet->mergeCells($cell . ":" . $endCell);
  180. }
  181. /* Wipe off html tags.*/
  182. if(isset($this->config->excel->editor[$this->rawExcelData->kind]) and in_array($field, $this->config->excel->editor[$this->rawExcelData->kind])) $value = $this->file->excludeHtml($value);
  183. if(!in_array($cell, $skipNumberCell))
  184. {
  185. if(isset($this->rawExcelData->percentageFields[$rowIndex][$field]))
  186. {
  187. $excelSheet->setCellValueExplicit($cell, $value, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
  188. }
  189. elseif(isset($this->rawExcelData->numberFields) && in_array($field, $this->rawExcelData->numberFields))
  190. {
  191. $excelSheet->setCellValueExplicit($cell, $value, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC);
  192. }
  193. else
  194. {
  195. $excelSheet->setCellValueExplicit($cell, $value, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
  196. }
  197. }
  198. /* Add comments to cell, xls don't work, must be xlsx. */
  199. if(isset($this->rawExcelData->comments[$rowIndex][$field])) $excelSheet->getComment($cell)->getText()->createTextRun($this->rawExcelData->comments[$rowIndex][$field]);
  200. /* Calculate date. */
  201. if((strpos($field, 'Date') !== false || in_array($field, $this->config->excel->dateFields)) && !helper::isZeroDate($value)) $excelSheet->setCellValueExplicit($cell, $value, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
  202. }
  203. /* Build excel list.*/
  204. if(isset($this->rawExcelData->listStyle) and in_array($field, $this->rawExcelData->listStyle)) $this->buildList($excelSheet, $field, $sheetRow);
  205. }
  206. $sheetRow++;
  207. }
  208. $this->setStyle($excelSheet, $sheetRow - 1);
  209. $this->setAlignment($excelSheet);
  210. if(isset($this->rawExcelData->help))
  211. {
  212. $excelSheet->mergeCells("A" . $sheetRow . ":" . end($this->excelColumns) . $sheetRow);
  213. $excelSheet->setCellValue("A" . $sheetRow, $this->rawExcelData->help);
  214. }
  215. $sheetIndex++;
  216. }
  217. /* If hasn't sys data remove the last sheet. */
  218. if(!$this->hasSysData) $this->phpExcel->removeSheetByIndex($this->phpExcel->getSheetCount() - 1);
  219. $this->phpExcel->setActiveSheetIndex(0);
  220. if(!empty($schemaSheets))
  221. {
  222. foreach($schemaSheets as $schemaSheet)
  223. {
  224. $schemaSheetRows = $schemaSheet->getHighestRow();
  225. if($schemaSheetRows > 1) $this->phpExcel->addSheet($schemaSheet, $schemaSheetIndex);
  226. }
  227. }
  228. /* Encode the file name for IE. */
  229. $fileName = $excelData->fileName;
  230. if(strpos($this->server->http_user_agent, 'MSIE') !== false || strpos($this->server->http_user_agent, 'Trident') !== false) $fileName = urlencode($fileName);
  231. /* Clean the ob content to make sure no space or utf-8 bom output. */
  232. $obLevel = ob_get_level();
  233. for($i = 0; $i < $obLevel; $i++) ob_end_clean();
  234. /* Set the file name and save path. */
  235. if(preg_match('/Safari/', $_SERVER['HTTP_USER_AGENT']))
  236. {
  237. $fileName = rawurlencode($fileName);
  238. $attachment = "attachment; filename*=utf-8''{$fileName}.{$fileType}";
  239. }
  240. else
  241. {
  242. $fileName = str_replace('+', '%20', urlencode($fileName));
  243. $attachment = "attachment; filename=\"{$fileName}.{$fileType}\";";
  244. }
  245. /* Set the download headers. */
  246. helper::setcookie('downloading', 1, 0, $this->config->webRoot, '', false, false);
  247. helper::header('Content-Type', 'application/vnd.ms-excel');
  248. helper::header('Content-Disposition', $attachment);
  249. helper::header('Cache-Control', 'max-age=0');
  250. $excelWriter = $this->phpExcel->createWriter($fileType);
  251. $excelWriter->setPreCalculateFormulas(false);
  252. $excelWriter->save($savePath ?: 'php://output');
  253. exit;
  254. }
  255. /**
  256. * Set excel filed name.
  257. *
  258. * @param int $colIndex
  259. * @param string $column
  260. * @access public
  261. * @return string
  262. */
  263. public function setExcelField($colIndex, $column = 'A')
  264. {
  265. for($col = 1; $col <= $colIndex; $col++) $column++;
  266. return $column;
  267. }
  268. /**
  269. * Write SysData sheet in xls.
  270. *
  271. * @access public
  272. * @return void
  273. */
  274. public function writeSysData()
  275. {
  276. if(!isset($this->rawExcelData->sysDataList)) return true;
  277. $this->hasSysData = true;
  278. $sheetIndex = $this->phpExcel->getSheetCount() - 1;
  279. $this->phpExcel->getSheet($sheetIndex)->setTitle($this->lang->excel->title->sysValue);
  280. foreach($this->rawExcelData->sysDataList as $field)
  281. {
  282. $column = $this->setExcelField($this->sysDataColIndex);
  283. $dataKey = $field . 'List';
  284. if(!isset($this->rawExcelData->$dataKey)) continue;
  285. $row = 1;
  286. foreach($this->rawExcelData->$dataKey as $value)
  287. {
  288. $this->phpExcel->getSheet($sheetIndex)->setCellValueExplicit("$column$row", $value, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
  289. $row++;
  290. }
  291. $this->sysDataColIndex++;
  292. }
  293. }
  294. /**
  295. * Build dropmenu list.
  296. * For a tip , if you want to modify that function , search "phpExcel DataValidation namedRange" in stackoverflow.com maybe helpful.
  297. *
  298. * @param int $excelSheet
  299. * @param int $field
  300. * @param int $row
  301. * @access public
  302. * @return void
  303. */
  304. public function buildList($excelSheet, $field, $row)
  305. {
  306. $listName = $field . 'List';
  307. $colIndex = array_search($field, $this->rawExcelData->sysDataList);
  308. $column = $this->setExcelField($colIndex);
  309. if(isset($this->rawExcelData->$listName))
  310. {
  311. $itemCount = count($this->rawExcelData->$listName);
  312. if($itemCount == 0) $itemCount = 1;
  313. $range = "{$this->lang->excel->title->sysValue}!\${$column}\$1:\${$column}\$" . $itemCount;
  314. }
  315. else
  316. {
  317. $range = is_array($this->rawExcelData->$listName) ? '' : '"' . $this->rawExcelData->$listName . '"';
  318. }
  319. $objValidation = $excelSheet->getCell($this->excelColumns[$field] . $row)->getDataValidation();
  320. $objValidation->setType(\PhpOffice\PhpSpreadsheet\Cell\DataValidation::TYPE_LIST)
  321. ->setErrorStyle(\PhpOffice\PhpSpreadsheet\Cell\DataValidation::STYLE_INFORMATION)
  322. ->setAllowBlank(false)
  323. ->setShowErrorMessage(false)
  324. ->setShowDropDown(true)
  325. ->setErrorTitle($this->lang->excel->error->title)
  326. ->setError($this->lang->excel->error->info)
  327. ->setFormula1($range);
  328. }
  329. /**
  330. * Set excel style.
  331. *
  332. * @param object $excelSheet
  333. * @param int $endRow
  334. * @access public
  335. * @return void
  336. */
  337. public function setStyle($excelSheet, $endRow)
  338. {
  339. $startRow = $this->headerRowCount + 1;
  340. $endColumn = $this->setExcelField(count($this->excelColumns) - 1);
  341. if(isset($this->rawExcelData->help) and isset($this->rawExcelData->extraNum)) $endRow--;
  342. /* Freeze column.*/
  343. if(isset($this->config->excel->freeze->{$this->rawExcelData->kind}))
  344. {
  345. $column = $this->excelColumns[$this->config->excel->freeze->{$this->rawExcelData->kind}];
  346. $column++;
  347. $excelSheet->FreezePane($column . $startRow);
  348. }
  349. /* Set row height. */
  350. $excelSheet->getDefaultRowDimension()->setRowHeight(20);
  351. /* Set content style for this table.*/
  352. $contentStyle = $excelSheet->getStyle("A1:{$endColumn}{$endRow}");
  353. $contentStyle->getAlignment()
  354. ->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT)
  355. ->setVertical(\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER)
  356. ->setWrapText(true);
  357. $contentStyle->getBorders()->getAllBorders()
  358. ->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN)
  359. ->getColor()->setARGB('FF808080');
  360. $contentStyle->getFont()->setSize(9);
  361. if(!isset($this->rawExcelData->nocolor))
  362. {
  363. $contentStyle->getFill()
  364. ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
  365. ->getStartColor()->setARGB('FFB2D7EA');
  366. }
  367. /* Set header style for this table.*/
  368. $headerStyle = $excelSheet->getStyle("A1:{$endColumn}{$this->headerRowCount}");
  369. $headerStyle->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
  370. $headerStyle->getFont()->setBold(true);
  371. if(!isset($this->rawExcelData->nocolor))
  372. {
  373. $headerStyle->getFill()->getStartColor()->setARGB('FF343399');
  374. $headerStyle->getFont()->getColor()->setARGB('FFFFFFFF');
  375. }
  376. /* Set column width and cell style. */
  377. $customWidth = $this->rawExcelData->customWidth ?? [];
  378. foreach($this->excelColumns as $key => $column)
  379. {
  380. /* Set column width. */
  381. $columnWidth = 10;
  382. if(strpos($key, 'Date') !== false || in_array($key, $this->config->excel->dateFields)) $columnWidth = 12; // Set column width of date fields.
  383. if(in_array($key, $this->config->excel->titleFields)) $columnWidth = $this->config->excel->width->title ?? 25; // Set column width of title fields.
  384. if(isset($this->config->excel->editor[$this->rawExcelData->kind]) and in_array($key, $this->config->excel->editor[$this->rawExcelData->kind])) $columnWidth = $this->config->excel->width->content ?? 50; // Set column width of editor fields.
  385. if(isset($customWidth[$key])) $columnWidth = $customWidth[$key]; // Set column width by userdefined settings.
  386. $excelSheet->getColumnDimension($column)->setWidth($columnWidth);
  387. /* Set cell alignment of center fields. */
  388. if(in_array($key, $this->config->excel->centerFields))
  389. {
  390. $excelSheet->getStyle("{$column}{$startRow}:{$column}{$endRow}")->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
  391. }
  392. /* Set cell format of date fields. */
  393. if(strpos($key, 'Date') !== false || in_array($key, $this->config->excel->dateFields))
  394. {
  395. $excelSheet->getStyle("{$column}{$startRow}:{$column}{$endRow}")->getNumberFormat()->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_YYYYMMDD2);
  396. }
  397. }
  398. /* Set row color. */
  399. if(!isset($this->rawExcelData->nocolor))
  400. {
  401. if(isset($this->rawExcelData->colors))
  402. {
  403. foreach($this->rawExcelData->colors as $row => $color)
  404. {
  405. $beginColumn = $this->excelColumns[$color->begin];
  406. $endColumn = $this->excelColumns[$color->end];
  407. $excelSheet->getStyle("{$beginColumn}{$row}:{$endColumn}{$row}")->getFill()->getStartColor()->setRGB($color->color);
  408. }
  409. }
  410. else
  411. {
  412. if($startRow % 2 == 0) $startRow++; // If data start row is even, plus 1 to make it odd.
  413. /* Set odd row background color. */
  414. for($row = $startRow; $row <= $endRow; $row += 2)
  415. {
  416. $excelSheet->getStyle("A{$row}:{$endColumn}{$row}")->getFill()->getStartColor()->setARGB('FFDEE6FB');
  417. }
  418. }
  419. }
  420. }
  421. /**
  422. * Set alignment.
  423. *
  424. * @param object $excelSheet
  425. * @access public
  426. * @return bool
  427. */
  428. public function setAlignment($excelSheet)
  429. {
  430. if(!isset($this->rawExcelData->percentageFields)) return true;
  431. foreach($this->rawExcelData->percentageFields as $row => $fields)
  432. {
  433. foreach($fields as $key => $field)
  434. {
  435. if(isset($this->excelColumns[$key]))
  436. {
  437. $cell = $this->excelColumns[$key] . ($this->headerRowCount + $row + 1);
  438. $excelSheet->getStyle($cell)->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_RIGHT);
  439. }
  440. }
  441. }
  442. }
  443. }