説明#
FastAdmin バックエンド Bootstrap Table に備わっているデータエクスポート機能は、フロントエンドレンダリングによって Excel を生成します。少量のデータエクスポートには便利ですが、大量のデータエクスポートではカクつきやフリーズを引き起こす可能性があります。
php の PhpSpreadsheet を利用して、バックエンドレンダリングで Excel を生成することを検討しています。
参考リンク#
- https://ask.fastadmin.net/article/6055.html
- https://ask.fastadmin.net/question/1670.html
- https://www.cnblogs.com/xuanjiange/p/14545111.html
- https://ask.fastadmin.net/question/14012.html
- https://blog.csdn.net/qq_15957557/article/details/113607843
具体的なコード#
エクスポートボタン#
.html ファイル内
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-success btn-export" title="{:__('Export')}" id="btn-export-file"><i class="fa fa-download"></i> {:__('Export')}</a>
</div>
具体的な js#
submitForm メソッドを通じて、bootstrapTable の検索条件、ソート、フィールド、searchList をバックエンドコントローラメソッドに渡します。
//カスタムエクスポート
var submitForm = function (ids, layero) {
var options = table.bootstrapTable('getOptions');
//console.log(options);
var columns = [];
var searchList = {};
$.each(options.columns[0], function (i, j) {
if (j.field && !j.checkbox && j.visible && j.field != 'operate') {
columns.push(j.field);
//searchListを保存し、バックエンドで処理するために渡す
if (j.searchList){
searchList[j.field] = j.searchList;
}
}
});
var search = options.queryParams({});
//具体的なformを指定しないと、選択条件で検索後にエクスポートがカクつく
var form = document.getElementById('export');
$("input[name=search]", layero).val(options.searchText);
$("input[name=ids]", layero).val(ids);
$("input[name=filter]", layero).val(search.filter);
$("input[name=op]", layero).val(search.op);
$("input[name=columns]", layero).val(columns.join(','));
$("input[name=sort]", layero).val(options.sortName);
$("input[name=order]", layero).val(options.sortOrder);
$("input[name=searchList]", layero).val(JSON.stringify(searchList));
//$("form", layero).submit();
form.submit(ids, layero);
};
$(document).on("click", ".btn-export", function () {
var url = ""; //エクスポートメソッドのコントローラURL
var ids = Table.api.selectedids(table);
var page = table.bootstrapTable('getData');
var all = table.bootstrapTable('getOptions').totalRows;
console.log(ids, page, all);
//ここにformがあり、inputとsubmitForm内で対応しています。他のパラメータを渡したい場合は、inputを追加できます。
Layer.confirm("エクスポートするオプションを選択してください<form action='" + Fast.api.fixurl(url) + "' id='export' method='post' target='_blank'><input type='hidden' name='ids' value='' /><input type='hidden' name='filter' ><input type='hidden' name='op'><input type='hidden' name='sort'><input type='hidden' name='order'><input type='hidden' name='search'><input type='hidden' name='columns'><input type='hidden' name='searchList'></form>", {
title: 'データをエクスポート',
btn: ["選択項目(" + ids.length + "件)", "このページ(" + page.length + "件)", "すべて(" + all + "件)"],
success: function (layero, index) {
$(".layui-layer-btn a", layero).addClass("layui-layer-btn0");
}
, yes: function (index, layero) {
submitForm(ids.join(","), layero);
return false;
}
,
btn2: function (index, layero) {
var ids = [];
$.each(page, function (i, j) {
ids.push(j.id);
});
submitForm(ids.join(","), layero);
return false;
}
,
btn3: function (index, layero) {
submitForm("all", layero);
return false;
}
})
});
バックエンドコントローラメソッド#
public function export()
{
if ($this->request->isPost()) {
set_time_limit(0);
$search = $this->request->post('search');
$ids = $this->request->post('ids');
$filter = $this->request->post('filter');
$op = $this->request->post('op');
$sort = $this->request->post('sort');
$order = $this->request->post('order');
$columns = $this->request->post('columns');
$searchList = $this->request->post('searchList');
$searchList = json_decode($searchList,true);
$spreadsheet = new Spreadsheet();
$spreadsheet->getProperties()
->setCreator("FastAdmin")
->setLastModifiedBy("FastAdmin")
->setTitle("タイトル")
->setSubject("件名");
$spreadsheet->getDefaultStyle()->getFont()->setName('Microsoft Yahei');
$spreadsheet->getDefaultStyle()->getFont()->setSize(10);
//セルのフォーマットをテキストに設定
$spreadsheet->getDefaultStyle()->getNumberFormat()->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_TEXT);
$worksheet = $spreadsheet->setActiveSheetIndex(0);
$whereIds = $ids == 'all' ? '1=1' : ['id' => ['in', explode(',', $ids)]];
$this->request->get(['search' => $search, 'ids' => $ids, 'filter' => $filter, 'op' => $op, 'sort' => $sort, 'op' => $order]);
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
//$columnsは取得したフィールドで、ここで自分のロジックを記述できます。例えば、他のフィールドを削除または追加することができます。
$columns_arr = explode(',',$columns);
$line = 1;
$list = [];
$this->model
->field($columns)
->where($where)
->where($whereIds)
->chunk(100, function ($items) use (&$list, &$line, &$worksheet,&$columns_arr,&$searchList) {
$list = $items = collection($items)->toArray();
foreach ($items as $index => $item) {
$line++;
$col = 1;
foreach ($item as $field => $value) {
//渡されたフィールドのみをエクスポートし、createtime_textなどのモデル内の追加フィールドをフィルタリングします。
if (!in_array($field,$columns_arr)) continue;
//フロントエンドから渡された$searchListに基づいて状態などを処理します。
if (isset($searchList[$field])){
$value = $searchList[$field][$value] ?? $value;
}
if (strlen($value) < 10){
$worksheet->setCellValueByColumnAndRow($col, $line, $value);
}else{
//長い数字の科学的表記を防ぐ
$worksheet->setCellValueExplicitByColumnAndRow($col, $line, $value,\PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
}
$col++;
}
}
},$sort,$order);
$first = array_keys($list[0]);
foreach ($first as $index => $item) {
//渡されたフィールドのみをエクスポート
if (!in_array($item,$columns_arr)) continue;
$worksheet->setCellValueByColumnAndRow($index + 1, 1, __($item));
//セルの幅を自動調整
$spreadsheet->getActiveSheet()->getColumnDimensionByColumn($index + 1)->setAutoSize(true);
//最初の行を太字に
$spreadsheet->getActiveSheet()->getStyleByColumnAndRow($index + 1, 1)->getFont()->setBold(true);
//水平中央揃え
$styleArray = [
'alignment' => [
'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
],
];
$spreadsheet->getActiveSheet()->getStyleByColumnAndRow($index + 1, 1)->applyFromArray($styleArray);
}
$spreadsheet->createSheet();
// クライアントのウェブブラウザに出力をリダイレクト(Excel2007)
$title = date("YmdHis");
//header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
//header('Content-Disposition: attachment;filename="' . $title . '.xlsx"');
//header('Cache-Control: max-age=0');
// IE 9に提供する場合、次のことが必要な場合があります
//header('Cache-Control: max-age=1');
// SSL経由でIEに提供する場合、次のことが必要な場合があります
//header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // 過去の日付
//header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // 常に修正済み
//header('Cache-Control: cache, must-revalidate'); // HTTP/1.1
//header('Pragma: public'); // HTTP/1.0
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls');
//ドキュメントをダウンロード
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment;filename="' . $title . '.xlsx"');
header('Cache-Control: max-age=0');
$writer = new Xlsx($spreadsheet);
$writer->save('php://output');
return;
}
}
不足している点#
大量データのエクスポートは、依然としてメモリオーバーフローを引き起こす可能性があります。
- メモリオーバーフローの問題を解決できる軽量の Excel ラッパーがあります https://github.com/mk-j/PHP_XLSXWriter
PHP_XLSXWriter 参考リンク#
- https://blog.csdn.net/qq_36025814/article/details/117136435
- https://blog.csdn.net/qq_21193711/article/details/105285623
- https://github.com/mk-j/PHP_XLSXWriter
PHP_XLSXWriter 使用方法#
copy の使い方#
- xlsxwriter.class.php ファイルをディレクトリのルート \application\common\libs に置きます。
- ファイル名を xlsxwriter.class.php から XLSXWriter.php に変更します。
- ファイル内で名前空間を引き入れます namespace app\common\library;
- 圧縮パッケージのクラス名:(1)、ZipArchive () を \ZipArchive () に変更;(2)、ZipArchive::CREATE を \ZipArchive::CREATE に変更
- 拡張:XLSXWriter_BuffererWriter を抽出し、独立したクラス XLSXWriterBufferWriter に変更します。XLSXWriter.php 内で呼び出す場所を修正します。
<?php
namespace app\common\library;
class XLSXWriter
{
}
以前の export メソッドを置き換える#
public function export()
{
if ($this->request->isPost()) {
set_time_limit(0);
ini_set("memory_limit", "256M");
$search = $this->request->post('search');
$ids = $this->request->post('ids');
$filter = $this->request->post('filter');
$op = $this->request->post('op');
$sort = $this->request->post('sort');
$order = $this->request->post('order');
$columns = $this->request->post('columns');
$searchList = $this->request->post('searchList');
$searchList = json_decode($searchList,true);
$whereIds = $ids == 'all' ? '1=1' : ['id' => ['in', explode(',', $ids)]];
$this->request->get(['search' => $search, 'ids' => $ids, 'filter' => urldecode($filter), 'op' => urldecode($op), 'sort' => $sort, 'order' => $order]);
list($where, $sort, $order, $offset, $limit) = $this->buildparams();
//$columnsは取得したフィールドで、ここで自分のロジックを記述できます。例えば、他のフィールドを削除または追加することができます。
$columns_arr = $new_columns_arr = explode(',',$columns);
$key = array_search('serviceFee',$columns_arr);
if ($key){
$new_columns_arr[$key] = "(platformFee + agentFee)/100 AS serviceFee";
$columns = implode(',',$new_columns_arr);
}
//todo
$userInfo = $this->auth->getUserInfo();
$map = [];
//代理
if ($userInfo['level'] == \Pc::Agent_Level){
$info = model('Agent')->where([
'accname' => $userInfo['username'],
])->find();
if (!$info) $this->error(__('Permission denied'));
$map['agentID'] = $info['id'];
}
//商人表示の制限
if ($userInfo['level'] == \Pc::Merchant_Level){
$info = model('Merchant')->where([
'accname' => $userInfo['username'],
])->find();
if (!$info) $this->error(__('Permission denied'));
$map['mid'] = $info['mid'];
}
$count = $this->model
->where($where)
->where($whereIds)
->where($map)
->count();
if ($count > 50000){
$this->error('データ量が多すぎます。分割してエクスポートすることをお勧めします。','');
}
$title = date("YmdHis");
$fileName = $title . '.xlsx';
$writer = new \app\common\library\XLSXWriter();
$sheet = 'Sheet1';
//タイトルデータを処理し、すべてをstring型に設定
$header = [];
foreach ($columns_arr as $value) {
$header[__($value)] = 'string'; //表のヘッダーのデータをすべてstring型に設定
}
$writer->writeSheetHeader($sheet, $header);
$this->model
->field($columns)
->where($where)
->where($whereIds)
->where($map)
->chunk(1000, function ($items) use (&$list, &$writer,&$columns_arr,&$searchList) {
$list = $items = collection($items)->toArray();
foreach ($items as $index => $item) {
//タイトルデータに基づいて、タイトルのフィールド順にデータをExcelに追加します。
$sheet = 'Sheet1';
$row = [];
foreach ($item as $field => $value) {
//渡されたフィールドのみをエクスポートし、createtime_textなどのモデル内の追加フィールドをフィルタリングします。
if (!in_array($field,$columns_arr)) continue;
//フロントエンドから渡された$searchListに基づいて状態などを処理します。
if (isset($searchList[$field])){
$value = $searchList[$field][$value] ?? $value;
}
$row[$field] = $value;
}
$writer->writeSheetRow($sheet, $row);
}
},$sort,$order);
//ヘッダーを設定し、ブラウザでダウンロードします。
header('Content-disposition: attachment; filename="'.XLSXWriter::sanitize_filename($fileName).'"');
header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Pragma: public');
$writer->writeToStdOut();
exit(0);
}
}
基本的な使用法#
<?php
use app\common\libs\XLSXWriter;
class Test {
//エントリーメソッド
public function test() {
$title = self::getTitle();
$data = self::getData();
//ダウンロードしてファイルに保存
// self::downloadExcel($title, $data, 'download_by_file.xlsx');
//ブラウザでポップアップダウンロード
self::downloadExcel($title, $data, 'downloadByBrowser.xlsx', 2);
}
/**
* データをファイルとしてダウンロード
* @param $title タイトルデータ
* @param $data 内容データ
* @param $fileName ファイル名(パスを含むことができます)
* @param int $type Excelダウンロードタイプ:1-ファイルをダウンロード;2-ブラウザポップアップダウンロード
* @param string $sheet Excelのワークシート
*/
public function downloadExcel($title, $data, $fileName, $type = 1, $sheet = 'Sheet1')
{
$writer = new XLSXWriter();
if ($type == 2) {
//ヘッダーを設定し、ブラウザでダウンロードします。
header('Content-disposition: attachment; filename="'.XLSXWriter::sanitize_filename($fileName).'"');
header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Pragma: public');
}
//タイトルデータを処理し、すべてをstring型に設定
$header = [];
foreach ($title as $value) {
$header[$value] = 'string'; //表のヘッダーのデータをすべてstring型に設定
}
$writer->writeSheetHeader($sheet, $header);
//タイトルデータに基づいて、タイトルのフィールド順にデータをExcelに追加します。
foreach ($data as $key => $value) {
$row = [];
foreach ($title as $k => $val) {
$row[] = $value[$k];
}
$writer->writeSheetRow($sheet, $row);
}
if ($type == 1) { //ファイルを直接保存
$writer->writeToFile($fileName);
} else if ($type == 2) { //ブラウザでファイルをダウンロード
$writer->writeToStdOut();
// echo $writer->writeToString();
exit(0);
} else {
die('ファイルダウンロード方法が間違っています。');
}
}
public function getData()
{
$data = [];
for ($i = 0; $i < 10; $i ++) {
$data[] = ['name' => "名前{$i}", 'hobby' => "趣味{$i}"];
}
return $data;
}
public function getTitle()
{
return [
'name' => '名前',
'hobby' => '趣味'
];
}
}