amber

amber

FastAdmin 個人記録 - バックエンドレンダリングエクスポートExcel

バックエンドレンダリング
キューエクスポート

説明#

FastAdmin バックエンド Bootstrap Table に備わっているデータエクスポート機能は、フロントエンドレンダリングによって Excel を生成します。少量のデータエクスポートには便利ですが、大量のデータエクスポートではカクつきやフリーズを引き起こす可能性があります。
php の PhpSpreadsheet を利用して、バックエンドレンダリングで Excel を生成することを検討しています。

参考リンク#

具体的なコード#

エクスポートボタン#

.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;
        }
    }

不足している点#

大量データのエクスポートは、依然としてメモリオーバーフローを引き起こす可能性があります。

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' => '趣味'
        ];
    }
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。