PHPで処理の進捗状況の表示と、処理終了時に自動でダウンロードさせる処理のサンプル

## 概要

時間のかかる処理（ファイル出力やバッチ処理など）の処理状況を随時送信し使用者のストレスを軽減する。また、デバッグが簡単。

## 処理概要

1. PHPでHTMLの＜head＞をブラウザに送信
1. ブラウザでHTMLの＜head＞に記述されたスクロール処理を永久ループ開始
1. PHPの処理状況をブラウザに随時送信
1. ブラウザ側で＜body＞内の行が増え自動的に最新状況へスクロール
1. PHPの出力が終了するとブラウザでDOMContentLoadedが呼ばれる
1. DOMContentLoaded内にてスクロール処理の停止(フラグ)
1. DOMContentLoaded内にてダウンロード用の＜ａ＞タグをクリック
1. ファイルのダウンロードリクエストを行う
1. ブラウザのファイル保存ダイアログが表示される

## サンプルプログラムのキャプチャ

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Qiita記事埋め込み用 <a href="https://t.co/v6ZTTLf6UU">pic.twitter.com/v6ZTTLf6UU</a></p>&mdash; やみ姐さん🔪 (@Yamine1San) <a href="https://twitter.com/Yamine1San/status/1415574083534684163?ref_src=twsrc%5Etfw">July 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## サンプルプログラムが動作するURL

[サンプルプログラムが動作するURL](https://yamine1san.ddns.net/qiita/csv-progress/index.php)
※私のパソコンが起動していると開ける

## サンプルプログラム

### 処理概要1～7の処理

```echo_flush_download.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>処理の進捗状況の表示と完了後のダウンロードサンプル</title>
  <script>
      let flg_dom_content_loaded = false;

      window.addEventListener('DOMContentLoaded', function () {
          flg_dom_content_loaded = true;
          [].forEach.call(document.getElementsByClassName('onload_click'), function (x) {
              x.click();
          });
      });

      loopprocess();

      /**
       * 全コンテンツの出力が終了するまで下へスクロールし続けさせる
       */
      function loopprocess() {
          window.scrollTo(0, 1000000);
          if (true === flg_dom_content_loaded) {
              return;
          }
          setTimeout('loopprocess()', 300);
      }
  </script>
</head>
<body>
<?php

// <head>の送信
ob_flush();
flush();

function echo_flush($str) {
    echo $str;
    ob_flush();
    flush();
}

echo_flush('<h1>CSV作成処理を開始しました。</h1>');

$csv_nm = 'test.csv';
$csv_path = dirname(__FILE__).'/'.$csv_nm;
$handle = fopen($csv_path, 'w');

$start_time = microtime(true);

$total = 3280;
$limit = 100;
$last_page = ceil($total / $limit);

echo_flush(
    sprintf(
        "<div>%s: %d件の処理開始 現在のメモリ使用量:%sMB 最大メモリ使用量:%sMB</div>"
        , date('Y-m-d H:i:s')
        , $total
        , round(memory_get_usage() / pow(1024, 2), 1)
        , round(memory_get_peak_usage() / pow(1024, 2), 1)
    )
);

for ($page_no = 1; $page_no <= $last_page; $page_no++) {

    $page_start = (($page_no - 1) * $limit) + 1;
    $page_last = $page_no * $limit;
    if ($total < $page_last) $page_last = $total;

    echo_flush(
        sprintf(
            "<div>%s: ページ%d/%d レコード%d～%d/%d件のデータ取得開始 現在のメモリ使用量:%sMB 最大メモリ使用量:%sMB 合計時間 %s秒</div>"
            , date('Y-m-d H:i:s')
            , $page_no
            , $last_page
            , $page_start
            , $page_last
            , $total
            , round(memory_get_usage() / pow(1024, 2), 1)
            , round(memory_get_peak_usage() / pow(1024, 2), 1)
            , round((microtime(true) - $start_time), 2)
        )
    );

    // データ取得クエリ実行
    // $rs = find_many();
    usleep(500000);

    echo_flush(
        sprintf(
            "<div>%s: ページ%d/%d レコード%d～%d/%d件のデータ取得終了 現在のメモリ使用量:%sMB 最大メモリ使用量:%sMB 合計時間 %s秒</div>"
            , date('Y-m-d H:i:s')
            , $page_no
            , $last_page
            , $page_start
            , $page_last
            , $total
            , round(memory_get_usage() / pow(1024, 2), 1)
            , round(memory_get_peak_usage() / pow(1024, 2), 1)
            , round((microtime(true) - $start_time), 2)
        )
    );

    // ランダムでエラー出力
    mt_srand();
    if (0 === mt_rand(0, 8)) {
      $error_msg = date('Y-m-d H:i:s').': '.$page_start.'行目でエラーが発生しました。 カラム〇○の値が未設定のためスキップしました。';
      echo_flush('<div style="font-weight: bold;color:red;">'.$error_msg.'</div>');
    }

    // CSV追加
    // foreach ($rs as $r) {
    //    fputcsv();
    // }

    fputs($handle,
        mb_convert_encoding(sprintf(
            "%s: ページ%d/%d レコード%d～%d/%d件のデータの書き込み 現在のメモリ使用量:%sMB 最大メモリ使用量:%sMB 合計時間 %s秒"
            , date('Y-m-d H:i:s')
            , $page_no
            , $last_page
            , $page_start
            , $page_last
            , $total
            , round(memory_get_usage() / pow(1024, 2), 1)
            , round(memory_get_peak_usage() / pow(1024, 2), 1)
            , round((microtime(true) - $start_time), 2)
        ), 'SJIS-win', 'UTF-8')."\r\n"
    );
}

fclose($handle);

echo_flush(
    sprintf(
        "<div>%s: ページ%d/%d レコード%d件の処理終了 現在のメモリ使用量:%sMB 最大メモリ使用量:%sMB 合計時間 %s秒</div>"
        , date('Y-m-d H:i:s')
        , $page_no
        , $last_page
        , $total
        , round(memory_get_usage() / pow(1024, 2), 1)
        , round(memory_get_peak_usage() / pow(1024, 2), 1)
        , round((microtime(true) - $start_time), 2)
    )
);

echo_flush('<h2>CSVファイルダウンロードリンク:<a href="./echo_flush_download_file.php?file='.$csv_nm.'" class="onload_click" download="CSVファイル.csv">CSVファイル.csv</a></h2>');

echo_flush('<h1>CSV作成処理を終了しました。</h1>');
```

### 処理概要8の処理

```echo_flush_download_file.php
<?php

// TODO CSVを出力した人によるダウンロードリクエストなのか要確認
$csv_path = dirname(__FILE__).'/test.csv';

header("Content-Disposition: attachment; filename=\"CSVファイル.csv\"");
header('Content-Length: '.filesize($csv_path));
header('Content-Type: application/force-download;');
readfile($csv_path);
```