【Laravel】ZipArchiveを使ってS3の画像をまとめてzip化する

# はじめに

業務で結構詰まってしまったので、今後のためのメモ。
また、ローカルでは動いたが、ステージングでは動かない問題にも直面したので、
それの対応策についてもまとめる。

# 環境
Laravelは7を使用。パッケージ管理はComposer
プロジェクト開始時は8が出た直後だったので、7使ってました。

## ローカル
いたって普通の windows 10 pro Edition。
ローカルの環境にAWSのIAMロールは付けられないので、
開発中だけ、S3を操作できる最低限のIAMロールを持ったユーザを作成し、
アクセスキーIDや、シークレットアクセスキーを発行して使用する。

## ステージング
AWS EC2を使用。OSは Amazon Linux2。
インスタンス自体にS3を操作できる最低限のIAMロールを持たせており、
アクセスキーIDや、シークレットアクセスキーを使わない運用をする（予定）

# やること

チェックボックスで選択された複数の画像をS3から取得し、zip化してダウンロードする。
この記事ではフロント側の処理は書かず、サーバ側（Laravel）の処理について書きます。

# コード

## S3への接続準備
S3に接続するためのコードです。
アクセスキーID・シークレットアクセスキーを使用するローカルと、
EC2のIAMロールを使用するステージングで少し異なります。

```php
$bucket = config('filesystems.disks.s3.bucket');
$imgDir = "path/to/hogehoge";

// S3に接続するための情報
$s3Config = [
    'version' => 'latest',
    'region' => config('filesystems.disks.s3.region'),
];

// ローカル環境用：　IAMロールを使えないので情報を追加する
if (config('app.env') === 'local') {
    $s3Config['credentials'] = [
        'secret' => config('filesystems.disks.s3.secret'),
        'key' => config('filesystems.disks.s3.key'),
    ];
}

$s3Client = new S3Client($s3Config);
```


ローカル用で使っている、アクセスキーID・シークレットアクセスキーを設定して接続する方法は検索すればたくさん出てきます。
検索に困ることはあまりないでしょう。

問題はIAMロールを使う方法です。
こちらの場合は `$s3Config` 変数の連想配列の `credentials` 項目が丸々ありません。
この項目がない場合の動きは、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/sdk-for-php/v3/developer-guide/guide_configuration.html#credentials)によると下記の通りです

> credentials オプションを指定しない場合、SDK は環境からの認証情報のロードを以下の順序で試行します。
　１．環境変数から認証情報をロードする
　２．credentials.ini ファイルから認証情報をロードする
　３．IAM ロールから認証情報をロードします。

３番目にIAMロールの認証が使われると書いています。
credentialsをつけてしまうと、今回のステージング環境ではS3に接続できなくて動きません。
例え .env で対応する部分を空文字にしていようがエラーになってしまいます。

`Storage::cloud()->get('/path/to')`　みたいにStorageファサードを使う場合は、.env の対応部分を空にしておけばOKなのですが、S3Clientクラスを使う場合はcredentialsオプションごと無くす必要があるのでごっちゃにならないようにしましょう。マジで。

## 画像取得のコマンドをまとめる
取得する画像分の情報をDBから取得し、その数だけ取得コマンドを生成します。

```php
$imgs = []; // DBから取得する画像の情報を取得

// zipファイルの生成場所
$fileName = 'result.zip';
$filePath = storage_path('app/' . $fileName);

// 画像取得コマンド
$commands = [];
foreach ($imgs as $img) {
    $key = $imgDir . $img['name'];

    $commands[] = $s3Client->getCommand(
        'GetObject',
        [
            'Bucket' => $bucket,
            'Key' => $key,
        ]
    );
}

// 作成したコマンドの配列を実行
$command_pool_batch = CommandPool::batch($s3Client, $commands);
```

`$command` 配列にはS3にあるファイルを取得するコマンドが格納されます。

- 'GetObject'
    - ファイルを取得する。という命令
- 'Bucket'
    - S3に生成したバケット
- 'key'
    - バケット内にある、取得したい画像のパス
    - この部分をforeachループで変化させ、選択した画像の取得コマンドを生成

最後に CommandPool::batchで、生成したコマンドを実行させます。
`command_pool_batch` 変数には、取得された結果が入っているので、あとはこの中身をzipに入れてDLさせます。

## zip化して、DL

ZipArchiveクラスを使用します。
先ほども書きましたが、`command_pool_batch` 変数に結果が入っているので、これをループさせます。

```php
// zipファイル作成
$zip = new ZipArchive();
$isOpen = $zip->open($filePath, ZipArchive::CREATE | ZipArchive::OVERWRITE);

// 成功時はbool型なので、型のチェックまで行う
if ($isOpen === true) {

    foreach ($command_pool_batch as $key => $value) {
        // 取得できなかったらS3Exceptionが格納される
        if (strpos(get_class($value), 'S3Exception') !== false) {
            Log::warning('fail get photo');
            continue;
        }

        header("Content-Type: {$value['ContentType']}");

        // ファイル名を指定（しないとS3内のフルパスが使われるらしい）
        $fileName = $imgs[$key]['name'];
        $zip->addFromString($fileName, $value['Body']);
    }

    $zip->close();
    ob_end_clean();
}

// レスポンスヘッダー
$headers = [
    'Content-Type' => 'application/zip',
    'Content-Disposition' => 'attachment'
];

return response()
    ->download($filePath, $fileName, $headers)
    ->deleteFileAfterSend(true);
```

foreach文で `$command_pool_batch as $key => $value` としていますが、`$key` にはインデックス番号が入ってきます。具体的な取得結果は `$value` の方です。
`addFromString` を使ってどんどんzipに画像の情報を追加していきます。

全て追加し終えたら、closeしてフロント側に渡しましょう。