Ajaxで送信したデータをもとにEXCELファイルを生成してダウンロードする　

画面上に表示された一覧表の内容を編集して、さらにチェックのついた行のみをEXCELでダウンロードするという中々厳しい条件でしたがなんとか実現できたので方法をまとめました。

**ポイントはバイナリーデータのやり取り!**

#処理手順
処理の手順としては大きく3つで概要は次の通り

####1.チェックした行のみをjson形式でajax送信
いつも使っているお気に入りの「[tabulator](http://tabulator.info/)」というデータテーブルを使いました。
一覧にチェックボックスをつけたり、内容を編集できたり、一般的なデータテーブルでも実現は可能だと思います。
メインじゃないので詳細は省略します。
  
肝心なのはリクエストの方法です。

いろいろググってみましたが中々うまくいきません。
単純に、ajaxのdataTypeを「binary」したりresponseTypeを「arraybuffer」するだけではダメっぽい。
"No conversion from text to binary"
ってエラーが吐き出されて全然バイナリーデータを受け取れません。

さらに調べて[stackoverflow](https://stackoverflow.com/questions/33902299/using-jquery-ajax-to-download-a-binary-file)  で見つけました。ソース自体は、ほぼここを参考にしています。

ajaxTransportを使って、リクエストデータの詳細設定をするのがポイントになります。

####2.jsonデータ受け取ったサーバーでEXCELを生成してダウンロード
PHPExcelが非推奨なので、後継のPhpSpreadsheetを使いました。
具体的な方法は「PhpSpreadsheet ダウンロード」とかでググったらすぐ出てきます。

なぜ、表を元にクライアント側で直接エクセル生成しなかったかというと
チェックした行のデータからさらに出力用の文言等は条件によりDBアクセスも必要だったたからです。
あらかじめ、クライアント側にすべての情報を持たせるには多すぎるっていうのもあります。

####3.レスポンスデータをバイナリーデータとして受け取って、クライアント側でダウンロード
以前同じような流れで、CSVダウンロードを作成した経験があり甘く見ていましたが、今回はエクセルということでデータのやり取りがテキストではなくバイナリーだったため嵌ってしまいました。
送受信関係は1で全て設定しているので、受け取ったあとのクライアントへのダウンロードの部分です。

##環境
今回の環境は以下の通りです
PHP 7.3.3（PHP7系であれば問題ないと思います）
jquery 3.3.1(これも3系であれば問題ないと思います。1系、2系でも少しいじればいけるかも・・・)
PhpSpreadsheet ：エクセル生成用のライブラリ


##1.チェックした行のみをjson形式でajax送信

```
//エクセル出力のボタンクリック
$("#output_excel").on("click",function(){
	//ここで一覧のチェックされた行情報を取得する(詳細は省略します)
	var check_list =[];

	if(check_list.length>0){
		//jsonに変換
		var json_check_list = { json_check_lists: JSON.stringify(check_list)};
		//ajax呼び出し元をコール
		output_excel(json_check_list);
	}else{
		alert("リストがチェックされていません。\r\n出力したい行をチェックしてください。");
		return false;
	}
});
```
クライアント側です
表の部分は省略しています。
リクエスト情報をjsonに変換して、ajaxを呼び出しているとこにわたします。

```
function output_excel(request_data){
	//ajaxTransportを使ってリクエスト情報の詳細を設定
	$.ajaxTransport("+binary", function (options, originalOptions, jqXHR) {
	//dataType=binaryとresponse type=blobかarraybufferのチェック
	if (window.FormData && ((options.dataType && (options.dataType == 'binary')) || (options.data && ((window.ArrayBuffer && options.data instanceof ArrayBuffer) || (window.Blob && options.data instanceof Blob))))) {
	return {
 		// 送信処理
		send: function (headers, callback) {
   			var xhr = new XMLHttpRequest(),
			url = options.url,
			type = options.type,
			dataType = options.responseType,
			data = options.data;

			xhr.addEventListener('load', function () {
				var data = {};
				data[options.dataType] = xhr.response;
				callback(xhr.status, xhr.statusText, data, xhr.getAllResponseHeaders());
			});
			//HTTPリクエスト生成
			xhr.open(type, url);

			//ヘッダ情報設定
			for (var i in headers) {
				xhr.setRequestHeader(i, headers[i]);
			}

			xhr.responseType = dataType;
			//リクエスト送信
			xhr.send(data);
		},
		// 中断処理
		abort: function () {
 			jqXHR.abort();
		}
	};
	}
});

jQuery.ajax({
	url: ここにリクエスト先のURL（EXCEL生成をしているPHP）
	dataType: 'binary',
	responseType:'arraybuffer',
 	type:"POST",
	data: request_data,
	headers: { 'X-Requested-With': 'XMLHttpRequest' },
	})
	.then(// 1つめは通信成功時のコールバック
		function (response_data, status, xhr) {
   			var uint8_array = new Uint8Array(response_data);
			if(response_data!=""){
 				var downloadData = new Blob([uint8_array], {type: "application/vnd.ms-excel;"});
				var filename ="example.xlsx";
				//ファイルのダウンロードにはブラウザ毎に処理を分けます
				if(window.navigator.msSaveBlob){ // for IE
					window.navigator.msSaveBlob(downloadData, filename);
				}else{
				var downloadUrl  = (window.URL || window.webkitURL).createObjectURL(downloadData);
				var link = document.createElement("a");
				link.href = downloadUrl;
				link.download = filename;
				link.click();
				(window.URL || window.webkitURL).revokeObjectURL(downloadUrl);
			}
		}
	},
	// 2つめは通信失敗時のコールバック
	function (response_data, status, xhr) {
		console.log(status);
		console.log(xhr);
		console.log(response_data);
	});
}
```
こちらもクライアント側です
ソース的には1と3がごっちゃになっています。
ajaxTransportでバイナリーでのやりとりができるように設定します。ほとんどの[stackoverflow](https://stackoverflow.com/questions/33902299/using-jquery-ajax-to-download-a-binary-file)とおりです。
リクエスト送信までが「1」の部分です。

##2.jsonデータ受け取ったサーバーでEXCELを生成してダウンロード

```
<?php
require 'vendor/autoload.php';

// Content-TypeをJSONに指定する
header('Content-Type: application/json');

$json_check_lists = (string)filter_input(INPUT_POST, 'json_check_lists');
$check_lists = json_decode( $json_check_lists , true ) ;

//EXCELのテンプレートファイルを使う
$template_file_path = "template.xlsx";
$output_file_name = "output_file_".date("ymd_Hi").".xlsx";

/*------------------------------------------------------------------------------
 エクセルクラス生成
 ------------------------------------------------------------------------------*/
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as Writer;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as Reader;
$reader = new Reader();
//テンプレートから読み込み
$spreadsheet = $reader->load($template_file_path);
$sheet = $spreadsheet->getActiveSheet();

/*------------------------------------------------------------------------------
 DBアクセス
 ------------------------------------------------------------------------------*/
//ここで$check_listsを元にデータベースからデータ取得したり、加工したりする
$db_list = array();

$row = 1; //開始行
foreach ($db_list as $value) {
	//EXCELに書き込み
	$sheet->setCellValue('A'.$row , $value["id"]));    
	$sheet->setCellValue('B'.$row , $value["name"]);    
	$sheet->setCellValue('C'.$row , $value["price"]);
	$row++;    
} 
   

//ダウンロード用
header("Content-Description: File Transfer");
header('Content-Disposition: attachment; filename="'.$output_file_name.'" ');
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Expires: 0');
ob_end_clean(); //バッファ消去

$writer = new Writer($spreadsheet);
$writer->save('php://output');
```
PHPでエクセルを出力するサーバー側の処理です。
1に記載した「ここにリクエスト先のURL（EXCEL生成をしているPHP）」で呼び出されているところです。
生成したエクセルをサーバーに保存ではなくダウンロードする様にします。
これで、クライアントはajaxのレスポンスにバイナリーデータを受け取れます。

####3.レスポンスデータをバイナリーデータとして受け取って、クライアント側でダウンロード
```
.then(// 1つめは通信成功時のコールバック
		function (response_data, status, xhr) {
   			var uint8_array = new Uint8Array(response_data);
			if(response_data!=""){
 				var downloadData = new Blob([uint8_array], {type: "application/vnd.ms-excel;"});
				var filename ="example.xlsx";
				//ファイルのダウンロードにはブラウザ毎に処理を分けます
				if(window.navigator.msSaveBlob){ // for IE
					window.navigator.msSaveBlob(downloadData, filename);
				}else{
				var downloadUrl  = (window.URL || window.webkitURL).createObjectURL(downloadData);
				var link = document.createElement("a");
				link.href = downloadUrl;
				link.download = filename;
				link.click();
				(window.URL || window.webkitURL).revokeObjectURL(downloadUrl);
			}
		}
	},
```
1で記載した部分を抜粋しています。(クライアント側で戻ってきたレスポンス部分です)
受け取ったバイナリーデータをクライアントでダウンロードできるようにします。

---
以上のソースで実現可能です。
わざわざajaxでバイナリーデータ扱ったりと中々厄介なことをやっているので、参考にしてみてください。
※抜粋した部分が多いので、そのままでは動かないかもしれません。

余談
teratailに同様の質問がありムリじゃない？的な回答があって、若干諦めましたが実現できたのでよかったです。