本文介紹基於PHP擴充套件xlswriter的Vtiful\Kernel\Excel類可以支援無限層級的複雜表頭匯出!後續也可能會持續更新優化
一、準備xlswriter擴充套件
1、windows系統:
到PECL網站下載符合自己本地PHP環境的ddl檔案下載地址:https://pecl.php.net/package/xlswriter,並複製到PHP的擴充套件目錄ext資料夾下,修改php.ini檔案,
加上這行
extension=xlswriter
2、Linux系統:
使用命令安裝
pecl install xlswriter
php配置檔案新增
extension = xlswriter.so
重啟:php nginx 檢視PHP安裝xlswriter擴充
二、封裝匯出類檔案(重點來了)
<?php
namespace App\Services;
use Vtiful\Kernel\Excel;
class MultiFloorXlsWriterService
{
// 預設寬度
private $defaultWidth = 16;
// 預設匯出格式
private $exportType = '.xlsx';
// 表頭最大層級
private $maxHeight = 1;
// 檔名
private $fileName = null;
private $xlsObj;
private $fileObject;
private $format;
/**
* MultiFloorXlsWriterService constructor.
* @throws \App\Exceptions\ApiException
*/
public function __construct()
{
// 檔案預設輸出地址
$path = base_path().'/public/uploads/excel';
$config = [
'path' => $path
];
$this->xlsObj = (new \Vtiful\Kernel\Excel($config));
}
/**
* 設定檔名
* @param string $fileName
* @param string $sheetName
* @author LWW
*/
public function setFileName(string $fileName = '', string $sheetName = 'Sheet1')
{
$fileName = empty($fileName) ? (string)time() : $fileName;
$fileName .= $this->exportType;
$this->fileName = $fileName;
$this->fileObject = $this->xlsObj->fileName($fileName, $sheetName);
$this->format = (new \Vtiful\Kernel\Format($this->fileObject->getHandle()));
}
/**
* 設定表頭
* @param array $header
* @param bool $filter
* @throws \Exception
* @author LWW
*/
public function setHeader(array $header, bool $filter = false)
{
if (empty($header)) {
throw new \Exception('表頭資料不能為空');
}
if (is_null($this->fileName)) {
self::setFileName(time());
}
// 獲取單元格合併需要的資訊
$colManage = self::setHeaderNeedManage($header);
// 完善單元格合併資訊
$colManage = self::completeColMerge($colManage);
// 合併單元格
self::queryMergeColumn($colManage, $filter);
}
/**
* 填充檔案資料
* @param array $data
* @author LWW
*/
public function setData(array $data)
{
foreach ($data as $row => $datum) {
foreach ($datum as $column => $value) {
$this->fileObject->insertText($row + $this->maxHeight, $column, $value);
}
}
}
/**
* 新增Sheet
* @param string $sheetName
* @author LWW
*/
public function addSheet(string $sheetName)
{
$this->fileObject->addSheet($sheetName);
}
/**
* 儲存檔案至伺服器
* @return mixed
* @author LWW
*/
public function output()
{
return $this->fileObject->output();
}
/**
* 輸出到瀏覽器
* @param string $filePath
* @throws \Exception
* @author LWW
*/
public function excelDownload(string $filePath)
{
$fileName = $this->fileName;
$userBrowser = $_SERVER['HTTP_USER_AGENT'];
if (preg_match('/MSIE/i', $userBrowser)) {
$fileName = urlencode($fileName);
} else {
$fileName = iconv('UTF-8', 'GBK//IGNORE', $fileName);
}
header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
header('Content-Disposition: attachment;filename="' . $fileName . '"');
header('Content-Length: ' . filesize($filePath));
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Cache-Control: max-age=0');
header('Pragma: public');
if (ob_get_contents()) {
ob_clean();
}
flush();
if (copy($filePath, 'php://output') === false) {
throw new \Exception($filePath . '地址出問題了');
}
// 刪除本地檔案
@unlink($filePath);
exit();
}
/**
* 組裝單元格合併需要的資訊
* @param array $header
* @param int $col
* @param int $cursor
* @param array $colManage
* @param null $parent
* @param array $parentList
* @return array
* @throws \Exception
* @author LWW
*/
private function setHeaderNeedManage(array $header,int $col = 1,int &$cursor = 0,array &$colManage = [], $parent = null,array $parentList = [])
{
foreach ($header as $head) {
if (empty($head['title'])) {
throw new \Exception('表頭資料格式有誤');
}
if (is_null($parent)) {
// 迴圈初始化
$parentList = [];
$col = 1;
} else {
// 遞迴進入,高度和父級集合通過相同父級條件從已有陣列中獲取,避免遞迴增加與實際資料不符
foreach ($colManage as $value) {
if ($value['parent'] == $parent) {
$parentList = $value['parentList'];
$col = $value['height'];
break;
}
}
}
// 單元格標識
$column = $this->getColumn($cursor) . $col;
// 組裝單元格需要的各種資訊
$colManage[$column] = [
'title' => $head['title'], // 標題
'cursor' => $cursor, // 遊標
'cursorEnd' => $cursor, // 結束遊標
'height' => $col, // 高度
'width' => $this->defaultWidth, // 寬度
'mergeStart' => $column, // 合併開始標識
'hMergeEnd' => $column, // 橫向合併結束標識
'zMergeEnd' => $column, // 縱向合併結束標識
'parent' => $parent, // 父級標識
'parentList' => $parentList, // 父級集合
];
if (isset($head['children']) && !empty($head['children']) && is_array($head['children'])) {
// 有下級,高度加一
$col += 1;
// 當前標識加入父級集合
$parentList[] = $column;
$this->setHeaderNeedManage($head['children'], $col, $cursor, $colManage, $column, $parentList);
} else {
// 沒有下級,遊標加一
$cursor += 1;
}
}
return $colManage;
}
/**
* 完善單元格合併資訊
* @param array $colManage
* @return mixed
* @author LWW
*/
private function completeColMerge(array $colManage)
{
$this->maxHeight = max(array_column($colManage, 'height'));
$parentManage = array_column($colManage, 'parent');
foreach ($colManage as $index => $value) {
// 設定橫向合併結束範圍:存在父級集合,把所有父級的橫向合併結束範圍設定為當前單元格
if (!is_null($value['parent']) && !empty($value['parentList'])) {
foreach ($value['parentList'] as $parent) {
$colManage[$parent]['hMergeEnd'] = self::getColumn($value['cursor']) . $colManage[$parent]['height'];
$colManage[$parent]['cursorEnd'] = $value['cursor'];
}
}
// 設定縱向合併結束範圍:當前高度小於最大高度 且 不存在以當前單元格標識作為父級的項
$checkChildren = array_search($index, $parentManage);
if ($value['height'] < $this->maxHeight && !$checkChildren) {
$colManage[$index]['zMergeEnd'] = self::getColumn($value['cursor']) . $this->maxHeight;
}
}
return $colManage;
}
/**
* 合併單元格
* @param array $colManage
* @param bool $filter
* @author LWW
*/
private function queryMergeColumn(array $colManage,bool $filter)
{
foreach ($colManage as $value) {
$this->fileObject->mergeCells("{$value['mergeStart']}:{$value['zMergeEnd']}", $value['title']);
$this->fileObject->mergeCells("{$value['mergeStart']}:{$value['hMergeEnd']}", $value['title']);
// 設定單元格需要的寬度
if ($value['cursor'] != $value['cursorEnd']) {
$value['width'] = ($value['cursorEnd'] - $value['cursor'] + 1) * $this->defaultWidth;
}
// 設定列單元格樣式
$toColumnStart = self::getColumn($value['cursor']);
$toColumnEnd = self::getColumn($value['cursorEnd']);
$this->fileObject->setColumn("{$toColumnStart}:{$toColumnEnd}", $value['width']);
}
// 是否開啟過濾選項
if ($filter) {
// 獲取最後的單元格標識
$filterEndColumn = self::getColumn(end($colManage)['cursorEnd']) . $this->maxHeight;
$this->fileObject->autoFilter("A1:{$filterEndColumn}");
}
}
/**
* 獲取單元格列標識
* @param int $num
* @return string
* @author LWW
*/
private function getColumn(int $num)
{
return Excel::stringFromColumnIndex($num);
}
}
三、使用示例
程式碼如下
/**
* 匯出測試
* @author LWW
*/
public function export()
{
$header = [
[
'title' => '一級表頭1',
'children' => [
[
'title' => '二級表頭1',
],
[
'title' => '二級表頭2',
],
[
'title' => '二級表頭3',
],
]
],
[
'title' => '一級表頭2'
],
[
'title' => '一級表頭3',
'children' => [
[
'title' => '二級表頭1',
'children' => [
[
'title' => '三級表頭1',
],
[
'title' => '三級表頭2',
],
]
],
[
'title' => '二級表頭2',
],
[
'title' => '二級表頭3',
'children' => [
[
'title' => '三級表頭1',
'children' => [
[
'title' => '四級表頭1',
'children' => [
[
'title' => '五級表頭1'
],
[
'title' => '五級表頭2'
]
]
],
[
'title' => '四級表頭2'
]
]
],
[
'title' => '三級表頭2',
],
]
]
]
],
[
'title' => '一級表頭4',
],
[
'title' => '一級表頭5',
],
];
$data= [];
// header頭規則 title表示列標題,children表示子列,沒有子列children可不寫或為空
for ($i = 0; $i < 100; $i++) {
$data[] = [
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
'這是第'. $i .'行測試',
];
}
$fileName = '很厲害的檔案匯出類';
$xlsWriterServer = new MultiFloorXlsWriterService();
$xlsWriterServer->setFileName($fileName, '這是Sheet1別名');
$xlsWriterServer->setHeader($header, true);
$xlsWriterServer->setData($data);
$xlsWriterServer->addSheet('這是Sheet2別名');
$xlsWriterServer->setHeader($header); //這裡可以使用新的header
$xlsWriterServer->setData($data); // 這裡也可以根據新的header定義資料格式
$filePath = $xlsWriterServer->output(); // 儲存到伺服器
$xlsWriterServer->excelDownload($filePath); // 輸出到瀏覽器
}
匯出效果
本作品採用《CC 協議》,轉載必須註明作者和本文連結