説明#
以前の バックエンドレンダリングによる Excel エクスポート では、 PHP_XLSXWriter を使用して軽量に Excel をエクスポートしましたが、大量のデータによるメモリオーバーフローを心配する必要はありませんでした。しかし、nginx のタイムアウトが大きなデータのエクスポートに影響を与えました。
解決思路#
FastAdmin は TP5 に基づいているため、thinkphp-queue を使用します。具体的な使用方法はドキュメントを参照してください。
エクスポートするデータが一定量を超えると、エクスポートデータに必要なパラメータをキューに追加し、ユーザーがしばらく待ってから自分でダウンロードできるようにします。
キュータスクを実行します(渡されたパラメータに基づいて、エクスポートする必要があるデータをクエリし、その後 Excel をローカルにエクスポートします)。
キューに追加#
主なコード
注意:FastAdmin のバックエンドコントローラーでは、 list ($where, $sort, $order, $offset, $limit) = $this->buildparams ();
返される $where は匿名オブジェクトであり、直接キューに渡すことも、直接シリアライズすることもできません。
以下のコードのように where 条件を取り出して配列に変換するか、 super_closure を使用して匿名オブジェクトをシリアライズすることを試みてください。
public function export()
{
//主なコードのみ表示
$job = 'AsyncExport'; //キューを実行するクラス名
$queue = 'asyncexport'; //キュー名
//TODO 匿名オブジェクトからstaticを取得し、whereを再構成
$reflection = new \ReflectionFunction($where);
$whereParams = $reflection->getStaticVariables()['where'];
$where2 = [];
foreach ($whereParams as $v){
$where2[$v[0]] = [$v[1],$v[2]];
}
$data = [
$columns_arr,
$columns,
$where2,
$whereIds,
$map,
$sort,
$order,
get_class($this->model),
$this->auth->id,
];
$isPushed = \think\Queue::push($job,$data,$queue); //キューに追加
if (!$isPushed){
$this->error(__('export queue Error'), '');
}
$this->success(__('export queue Success,Please go to the export list to download,The larger the amount of data, the longer the waiting time'), '');
}
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);
}
$count = $this->model
->where($where)
->where($whereIds)
->where($map)
->count();
//一定量を超えた場合はキューに追加
if ($count > 10000) {
$job = 'AsyncExport';
$queue = 'asyncexport';
//TODO 匿名オブジェクトからstaticを取得し、whereを再構成
$reflection = new \ReflectionFunction($where);
$whereParams = $reflection->getStaticVariables()['where'];
$where2 = [];
foreach ($whereParams as $v) {
$where2[$v[0]] = [$v[1], $v[2]];
}
$data = [
$columns_arr,
$columns,
$where2,
$whereIds,
$map,
$sort,
$order,
get_class($this->model),
$this->auth->id,
];
$isPushed = \think\Queue::push($job, $data, $queue);
if (!$isPushed) {
$this->error(__('export queue Error'), '');
}
$this->success(__('export queue Success,Please go to the export list to download,The larger the amount of data, the longer the waiting time'), '');
}
$title = date("YmdHis");
$fileName = $title . '.xlsx';
$writer = new 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) {
//タイトルデータtitleに基づいて、titleのフィールド順にデータをExcelに追加
$sheet = 'Sheet1';
$row = [];
foreach ($item as $field => $value) {
//渡されたフィールドのみをエクスポートし、createtime_textなどのmodeld内の追加フィールドをフィルタリング
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
namespace app\job;
use app\common\library\XLSXWriter;
use fast\Random;
use think\Exception;
use think\queue\Job;
class AsyncExport {
//主なコードのみ表示、think-queueの具体的な使用は省略
/**
* メッセージ内のデータに基づいて実際のビジネス処理を行います...
*/
private function doJob($data)
{
//キューに渡すパラメータを整理
list($columns_arr, $columns, $where, $whereIds, $map, $sort, $order, $model, $admin_id) = $data;
//$title = date("YmdHis");
$title = Random::alnum(16);
$fileName = $title . '.xlsx';
$writer = new XLSXWriter();
$sheet = 'Sheet1';
//タイトルデータを処理し、すべてをstring型に設定
$header = [];
foreach ($columns_arr as $value) {
$header[__($value)] = 'string'; //表のデータをすべてstring型に設定
}
$writer->writeSheetHeader($sheet, $header);
model($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) {
//タイトルデータtitleに基づいて、titleのフィールド順にデータをExcelに追加
$sheet = 'Sheet1';
$row = [];
foreach ($item as $field => $value) {
//渡されたフィールドのみをエクスポートし、createtime_textなどのmodeld内の追加フィールドをフィルタリング
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);
$output_dir = ROOT_PATH . 'public' . DS . 'export' . DS . $fileName;
$writer->writeToFile($output_dir);
$insertData = [
'filename' => $fileName,
'filesize' => filesize($output_dir),
'url' => DS . 'export' . DS . $fileName,
'mimetype' => 'xlsx',
'model' => $model,
'admin_id' => $admin_id,
'createtime' => time(),
];
//Excelエクスポート後、サーバーに保存し、エクスポートされたExcelの基本情報とパスを表に保存してユーザーに表示し、ダウンロードを容易にします。
model('app\admin\model\ExportList')->insert($insertData);
return true;
}
}
エクスポート記録の保存#
エクスポートテーブル エクスポートされた Excel を記録
admin_id は誰がダウンロードしたかを区別するために使用され、彼にのみ自分のダウンロードを表示します。
CREATE TABLE `t_export_list` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`filename` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'ファイル名',
`url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'ファイルパス',
`filesize` int(10) NULL DEFAULT NULL COMMENT 'ファイルサイズ',
`mimetype` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'mimeタイプ',
`admin_id` int(10) NULL DEFAULT NULL COMMENT '管理者ID',
`model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'エクスポートモデル',
`createtime` int(10) NULL DEFAULT NULL COMMENT '作成時間',
PRIMARY KEY (`id`) USING BTREE,
INDEX `admin_id`(`admin_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
FastAdmin に対応するエクスポートリストのコントローラー
<?php
namespace app\admin\controller\general;
use app\common\controller\Backend;
/**
*
*
* @icon fa fa-circle-o
*/
class Exportlist extends Backend
{
protected $dataLimit = true; //バックエンドのデフォルトでadmin_idに基づくデータ制限を有効にします
}