PHPでファイルをzipにまとめて一括ダウンロードする

こんにちは

今日も今日とて台風19号にも負けずに出勤しています。

~~午後から休みになるんじゃないかと一抹の希望を持っていましたが、どうやらそれは叶わないようで~~

iOS, android, API開発に続いて最近は管理画面開発までぶん投げられるようになりました。。。
APIも管理画面もPHPなのであまり抵抗がないのが救いです。


今回は管理画面で大量のmp3をzipにまとめて一括でダウンロードする処理が必要だったのですが、安定の泥沼にどハマりからの給料泥棒をキメてしまったので書き留めます。
ちなみに、mp3のデータを途中でバイナリーにエンコードするので、mp3だけでなく色んなデータに対応できるのではないかと~~勝手に~~思ってます。
というわけで書いていきます。


まず前提として<font color="red">ZipArchiveクラスが使えること</font>、です。
これのインストール方法ですが、調べたら出ますが例えばamazon linux(centOSも多分これ？）だと
まずはphpのヴァージョンを確認(7.0だったとする）

```linux
$php -v
PHP 7.0.33 (cli) (built: Jan  9 2019 22:04:26) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies

$sudo yum install php70-zip.x86_64
(略)
$sudo service httpd restart
```

こんな感じで！
もしヴァージョンが7.1だったらsudo yum install php71-zip.x86_64でいけるかと思います。
最後にアパッチを再起動します（アパッチ２ならsudo service apache2 restart)

運がいい人はここで無事にZipArchiveクラスが使えるようになります。



```php:controller.php
// Zipクラスロード
$zip = new ZipArchive();
// Zipファイル名
$zipFileName = "file_" . date("Ymds") .'.zip';
// Zipファイル一時保存ディレクトリ
$zipTmpDir = '/tmp/zip/';
			
// Zipファイルオープン
$result = $zip->open($zipTmpDir.$zipFileName, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($result !== true) {
    // 失敗した時の処理
} 

// 処理制限時間を外す
set_time_limit(0);

// ファイルのパスの数を取得
$file_cnt = count($filepaths);// $filepathsにはファイルのURLが入っているものとします
// zipに複数のファイルを詰めていく
while ($i < $file_cnt) {
    // ここではAPIを叩いて返ってきたデータをファイルにしてZIPに取り入れる処理を書きます
    $filepath = $filepaths[$i];
    $ch = curl_init($filepath);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_NOBODY, 0);
    // データ容量が大きい場合はここのTIMEOUTの時間を調整してください
    curl_setopt($ch, CURLOPT_TIMEOUT, 120);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);

    $output = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if ($status == 200 && mb_strlen($output) != 0){
        // ファイルの取得に成功したのでzipに詰めてインクリメントします
        $zip->addFromString(basename($filepath), $output);
        $i++;
    } else if ($status == 404){
        // サーバーにファイルが見つからないのでインクリメントします
        $i++;
    } else if ($status == 200 && mb_strlen($output) == 0){	
        // サーバーにファイルがあったが何らかの理由でデータがゼロなのでインクリメントせずに再度APIを叩きます			
    }
    curl_close($ch);
    sleep(1);
    // メモリリーク
    unset($output);
    unset($ch);
}
$zip->close();


// 上記で作ったZIPをダウンロードします。
header("Content-Type: application/zip");
header("Content-Transfer-Encoding: Binary");
// ↓これを書いてるサイトが多かったのですが、これでファイルサイズを指定するとダウンロードが長引くことが多かったのでコメントアウトしました
//header("Content-Length: ".filesize($zipTmpDir.$zipFileName));
header("Content-Disposition: attachment; filename=\"".basename($zipTmpDir.$zipFileName)."\"");
// ファイルを出力する前に、バッファの内容をクリア（ファイルの破損防止）
ob_end_clean();
//readfile($zipTmpDir.$zipFileName);
// これはreadfileの代わりの自作メソッドです（後述）
self::efficient_readfile($zipTmpDir.$zipFileName);

//一時ファイルを削除しておく
unlink($zipTmpDir.$zipFileName);
```

自作メソッドが下記です。

```php
public static function efficient_readfile($path){
    $handle = fopen($path, "rb");
    while(!feof($handle)){
        print fread($handle, 4096);
        ob_flush();
        flush();
    }
    fclose();
}
```

readfile()を使うと大きいZIPファイルをダウンロードできないので細切れにする必要があります。
これに触れている記事は全然なかったのですが、GoogleChomeだと大きめのファイルはreadfile()ではエラーがでて落とせませんでした。

あと、
header("Content-Transfer-Encoding: Binary");
ここなのですが、こうしてバイナリーデータにエンコードしなければダウンロードできないという事態に直面しました。
これは僕だけなのでしょうか、これまた検索しても全然出てこずハマりました。

また、メモリリークと書いているところがありますが、僕が担当しているプロジェクトはなにぶん予算の貧弱なプロジェクトなものでAWSのインスタンスの性能は（ry
ともかく、変数をunsetしなければすぐにメモリ不足のエラーが出てしまいます。
これでもメモリリークしてるようでダウンロード数を増やすとエラーが出るので
sudo vi etc/php.ini
で中身のmemory_limitを大きく変更したりしました（根本的な解決になっていませんが）

とまあよしなにやったら何とかなりましたというお話です。




めちゃ汚いコードですが、そのうちUtilにでもまとめてブラックボックス化した方がいい部分が結構あるかもですね

API叩きまくるなら並列処理しろよと言われそうですが、PHPが対応していなかったので勘弁してくださいwww