Export.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <?php
  2. namespace addons\shopro\library;
  3. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  4. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  5. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  6. use PhpOffice\PhpSpreadsheet\Style\Border;
  7. use Cache\Adapter\Redis\RedisCachePool;
  8. use Cache\Bridge\SimpleCache\SimpleCacheBridge;
  9. use PhpOffice\PhpSpreadsheet\Settings;
  10. use addons\shopro\library\Redis;
  11. class Export
  12. {
  13. protected $last_memory_limit = '256M';
  14. protected $config = [
  15. 'save_type' => 'download', // 保存方式,download:直接下载, save:保存到服务器
  16. 'list_rows' => 1000, // 每次查询数据条数
  17. 'save_path' => RUNTIME_PATH . 'storage/export/', // 如果保存到服务器,保存路径
  18. 'memory_limit' => '512M', // php 进程内存限制(仅在导出过程生效), 注意单位 M
  19. 'time_limit' => 0, // php 超时时间,单位秒,0 不限制
  20. 'cache_driver' => 'default', // 数据临时存储驱动,default:内存, redis:redis
  21. 'redis_select' => null, // 导出大数据时,请尽量配置一个空的 redis db 库(导出失败,可以随时清除),以免导出失败,db 库中被存入大量垃圾数据
  22. ];
  23. public function __construct()
  24. {
  25. $this->config['time_limit'] = intval($this->config['time_limit']);
  26. $this->config['list_rows'] = intval($this->config['list_rows']) ? intval($this->config['list_rows']) : 1000;
  27. // 设置导出限制
  28. $this->setLimit();
  29. // 设置导出时缓存
  30. $this->setCache();
  31. }
  32. /**
  33. * 导出
  34. *
  35. * @param array $params
  36. * @param \Closure $callback
  37. * @return void
  38. */
  39. public function export($params, \Closure $callback)
  40. {
  41. $fileName = $params['file_name']; // 文件名
  42. $cellTitles = $params['cell_titles']; // 标题
  43. $cell_num = count($cellTitles); // 标题数量
  44. $total = $params['total']; // 记录总条数
  45. $is_sub_cell = $params['is_sub_cell'] ?? false; // 是否有子数据
  46. $sub_start_cell = $params['sub_start_cell'] ?? null; // 子数据开始字段
  47. $sub_field = $params['sub_field'] ?? null; // 子数据字段名
  48. $cellName = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ');
  49. $last_cell_key = $cellName[$cell_num - 1]; // 最后一列的标头
  50. // 最后一页
  51. $last_page = intval(ceil($total / $this->config['list_rows']));
  52. // 实例化excel
  53. $spreadsheet = new Spreadsheet();
  54. // 初始化工作簿
  55. $sheet = $spreadsheet->getActiveSheet(0);
  56. // 给表头字体加粗
  57. $sheet->getStyle('A1:' . $last_cell_key . '1')->getFont()->setBold(true);
  58. // 表头
  59. $i = 0;
  60. foreach ($cellTitles as $key => $cell) {
  61. $sheet->setCellValue($cellName[$i] . '1', $cell);
  62. $i++;
  63. }
  64. $cell_total = 2; // 当前表格已有行数
  65. for($page = 1;$page <= $last_page;$page++) {
  66. $is_last_page = $page == $last_page ? true : false;
  67. // 获取数据
  68. $datas = $callback([
  69. 'page' => $page,
  70. 'list_rows' => $this->config['list_rows'],
  71. 'is_last_page' => $is_last_page
  72. ]);
  73. foreach ($datas as $key => $data) {
  74. if ($is_last_page && $key == count($datas) - 1 && (!is_array($data) || count($data) == 1)) {
  75. $total_text = is_array($data) ? current($data) : $data;
  76. $sheet->mergeCells('A' . $cell_total . ':' . $last_cell_key . $cell_total);
  77. $sheet->setCellValue('A' . $cell_total, $total_text);
  78. } else {
  79. $items_count = 1;
  80. if ($is_sub_cell) {
  81. $items_count = count($data[$sub_field]);
  82. }
  83. $items_count = $items_count >= 1 ? $items_count : 1;
  84. // 每条记录设置边框
  85. // $sheet->getStyle('A' . ($cell_total).':' . $last_cell_key . ($cell_total + $items_count - 1))->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
  86. $i = 0; // 当前循环到第几列了
  87. $sub_start = false;
  88. foreach ($cellTitles as $k => $cell) {
  89. if ($k == $sub_start_cell) {
  90. // 如果有子数据,是否循环到了子数据
  91. $sub_start = true;
  92. }
  93. if ($is_sub_cell) {
  94. if (!$sub_start) {
  95. // 循环主数据
  96. $current_text = $data[$k] ?? '';
  97. if ($items_count > 1) {
  98. // items 有多个,需要合并单元格
  99. $sheet->mergeCells($cellName[$i] . ($cell_total) . ':' . $cellName[$i] . ($cell_total + $items_count - 1));
  100. $sheet->getCell($cellName[$i] . ($cell_total))->getStyle()->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
  101. }
  102. $sheet->setCellValue($cellName[$i] . ($cell_total), $current_text);
  103. } else {
  104. // 循环子数据
  105. foreach ($data[$sub_field] as $j => $sub) {
  106. $current_text = $sub[$k] ?? '';
  107. $sheet->setCellValue($cellName[$i] . ($cell_total + $j), $current_text);
  108. }
  109. }
  110. } else {
  111. $current_text = $data[$k] ?? '';
  112. $sheet->setCellValue($cellName[$i] . $cell_total, $current_text);
  113. }
  114. $i++;
  115. }
  116. // 增加数据写入条数
  117. $cell_total = $cell_total + $items_count;
  118. }
  119. }
  120. }
  121. // 设置表格边框
  122. $sheet->getStyle('A1:' . $last_cell_key . $cell_total)->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
  123. // ini_set('memory_limit', '256M');
  124. return $this->output($spreadsheet, $fileName);
  125. }
  126. /**
  127. * 输出
  128. *
  129. * @param object $spreadsheet
  130. * @param string $fileName
  131. * @return void
  132. */
  133. public function output($spreadsheet, $fileName)
  134. {
  135. $class_name = '\\PhpOffice\\PhpSpreadsheet\\Writer\\Xlsx';
  136. if (!class_exists($class_name)) {
  137. error_stop('文件输出格式不支持');
  138. }
  139. if ($this->config['save_type'] == 'save') {
  140. // 初始化目录
  141. if (!is_dir($this->config['save_path'])) {
  142. @mkdir($this->config['save_path'], 0755, true);
  143. }
  144. $save_file = $this->config['save_path'] . $fileName . '-' . date('YmdHis') . '.xlsx';
  145. $result = [
  146. 'file_path' => str_replace(ROOT_PATH, '项目目录/', $save_file)
  147. ];
  148. } else {
  149. $save_file = $fileName . '-' . date('YmdHis') . '.xlsx';
  150. $result = [
  151. 'file_name' => $save_file
  152. ];
  153. ob_end_clean();
  154. header('pragma:public');
  155. header("Content-type:application/octet-stream; charset=utf-8; name=" . urlencode($save_file));
  156. header("Content-Disposition:attachment; filename=" . urlencode($save_file)); //attachment新窗口打印inline本窗口打印
  157. $save_file = 'php://output';
  158. }
  159. $writer = new $class_name($spreadsheet);
  160. $writer->save($save_file);
  161. return $result;
  162. // 修改为原始内存限制,会影响文件下载,暂时不修改回原来内存
  163. // ini_set('memory_limit', $this->last_memory_limit);
  164. }
  165. /**
  166. * 设置php 进程内存限制
  167. *
  168. * @return void
  169. */
  170. public function setLimit()
  171. {
  172. // 不限时
  173. set_time_limit($this->config['time_limit']);
  174. // 根据需要调大内存限制
  175. $this->last_memory_limit = ini_get('memory_limit');
  176. ini_set('memory_limit', $this->config['memory_limit']);
  177. }
  178. /**
  179. * 设置导出临时缓存
  180. *
  181. * @return void
  182. */
  183. public function setCache()
  184. {
  185. // 设置缓存
  186. if ($this->config['cache_driver'] == 'redis') {
  187. // 将表格数据暂存 redis,可以降低 php 进程内存占用
  188. if (!class_exists(RedisCachePool::class)) {
  189. // 需要安装扩展包 composer require cache/simple-cache-bridge cache/redis-adapter
  190. error_stop('请安装扩展包:composer require cache/simple-cache-bridge cache/redis-adapter');
  191. }
  192. if (is_null($this->config['redis_select'])) {
  193. error_stop('请在 addons/shopro/library/Export.php 文件,defaultConfig 中配置 redis_select 库');
  194. }
  195. $options = [
  196. 'select' => $this->config['redis_select']
  197. ];
  198. $redis = (new Redis($options))->getRedis(); // 不冲突
  199. $pool = new RedisCachePool($redis);
  200. $simpleCache = new SimpleCacheBridge($pool);
  201. Settings::setCache($simpleCache);
  202. }
  203. }
  204. public function getConfig()
  205. {
  206. return $this->config;
  207. }
  208. }