Laravel / Guzzle (HTTP Client) で画像ファイルを送信したい

Laravel + Guzzle を使用し、テキストや画像を混在させた状態のデータをHTTP Client経由で送りたいときの備忘録です。検証はしてませんがPDFやテキストファイルなど、その他のファイル形式でもいけるはずです。
APIでの画像アップロード（ファイルアップロード）のお供にどうぞ。

### 検証環境
 - Laravel 6.20.12
 - Guzzle 7.2
 - PHP 7.4


## 前提条件: base64 と multipart/form-data

ファイルの送信にはよく ```base64``` へのエンコードが用いられますが、Guzzleで画像やその他ファイルをエンコードせずに送るためには ```multipart/form-data``` という形式で送る必要があるようです。

```base64``` はざっくり言えば、64進数を意味しており、データを64種類の文字 (アルファベット[ a - z ], [ A - Z ], 数字[ 0 - 9 ]、一部の記号[ +,  / ] )で表すエンコード方式です。

[Base64 出典: フリー百科事典『ウィキペディア（Wikipedia）』](https://ja.wikipedia.org/wiki/Base64)
[「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 - Base64](https://wa3.i-3-i.info/word11338.html)
[base64ってなんぞ？？理解のために実装してみた](https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4)


```multipart/form-data``` は ```Content-Type```（メディアの種別を表すもの）の一種で**複合データ型**を意味し、HTMLの送信フォームでファイルをアップロードしたいときなどによく見かけます。

下記のフォームでは ```enctype``` として ```multipart/form-data``` を設定しています。この設定がないと画像やPDFなど諸々ファイルアップロードができないので、設定を忘れてフォームが動かなかった、という経験のある方も多いのではないでしょうか。

```html
<form action="/" method="post" enctype="multipart/form-data">
    <input type="text" name="foo" value="foobar">
    <input type="file" name="fooFile">
    <button type="submit">送信</button>
</form>
```

[HTML フォームにおける Content-Type - MDN - Mozilla](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Type)
[【HTTP】multipart/form-data の boundary って何ぞや？](https://qiita.com/kunihiros/items/e11daacfd62d85d83c06)
[Multipart/form-dataの仕様メモ](https://qiita.com/sivertigo/items/14957200af5ad953d63b)
[[フロントエンド] multipart/form-dataを理解してみよう](https://www.yoheim.net/blog.php?q=20171201)
[SuikaWiki > Wiki > multipart/form-data (MIME)](https://wiki.suikawiki.org/n/multipart%2Fform-data)

## Guzzle公式ドキュメントより

まずはコチラの公式ドキュメントを見てみます。英語だし端的にしか書かれていないので色々と不安になりますが、一度書いて動かしてしまえば大丈夫です。

[Guzzle Docs: Request Options - multipart](https://docs.guzzlephp.org/en/stable/request-options.html#multipart)

multipart のセクションで、以下のように書いてあります。

---

> Sets the body of the request to a multipart/form-data form.
（中略）
> The value of multipart is an array of associative arrays, each containing the following key value pairs:
> 
> - name: (string, required) the form field name
> - contents: (StreamInterface/resource/string, required) The data to use in the form element.
> - headers: (array) Optional associative array of custom headers to use with the form element.
> - filename: (string) Optional string to send as the filename in the part.

*リクエストのボディを ```multipart/form-data``` 形式で設定します。*
（中略）
*multipartの値は連想配列の配列であり、それぞれに次のキーと値のペアが含まれています。*

- *name:（string, 必須）フォームのフィールド名*
- *contents:（StreamInterface/resource/string, 必須）form要素で使用するデータ。*
- *headers:（array）フォーム要素で使用するカスタムヘッダーのオプションの連想配列。*
- *filename:（string）パーツのファイル名として送信するオプションの文字列。*

---

**オプション**という表現も、慣れていないと「つまりどういうことだってばよ」という気持ちになったりしますが、元の英文の ```headers``` と ```filename``` に **Optional** と書いてあり、そういうときは大体、必須では無い、あってもなくても良い、必要に応じて入れる、という意味になります。
なので、今回の実装で必須なのは `name` と `contents` だということになります。

## 実装

Laravel 7 以上のバージョンでは、デフォルトでGuzzleのパッケージを含むそうで、使い方は以下のドキュメントに書かれています。

[Laravel 7.x HTTPクライアント](https://readouble.com/laravel/7.x/ja/http-client.html)

今回はLaravel 6 以下での話になるので、Laravel 6 以下のバージョンは `composer` コマンドで `guzzlehttp/guzzle` パッケージのインストールを行います。

```php
composer require guzzlehttp/guzzle
```

簡単に書くと、Guzzleを使ったリクエスト（画像ファイル送信）はこんな感じです。
一度Storageに保存した画像をGuzzleで読み込みさせるように実装しました。


```php
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7;

public function postApi($data)
{
    $method = 'POST';
    $api_url = 'https://example.com/v1/upload';

    $params = 
        [
            'name' => 'token',
            'contents' => Auth::user()->token
        ],
        [
            'name' => 'product_id',
            'contents' => $data['product_id']
        ],
        [
            'name' => 'image_file',
            'contents' => Psr7\Utils::tryFopen('../' . Storage::url($data['file_path']))
        ];

    $client = new Client();
    $response = $client->request($method, $api_url, ['multipart' => $params, 'http_errors' => false]);

    return $response;
}
```

### headers/Content-Typeについて
Guzzle のリクエストタイプには `query` や `form_params` を使用することの方が多いかもしれませんが、画像をそのままファイルとして送信したいときは `multipart` を使用します。

Stack Overflow など見ていると http における headers の内容を配列で別途指定している方もいるようですが、Guzzle ではリクエスト方式に `multipart` を選んだ時点で `Content-Type` が `multipart/form-data` になるため、あえて headers の `Content-Type` を手動で再設定する必要はありません。
認証などで headers をカスタマイズしなければいけないときのみ手動で設定してください。またその際は、`Content-Type` を誤った形式で上書きしないように注意してください。

### パラメータについて
`name` にはAPIのパラメータ名（フィールド名）、`contents` にはパラメータとして送りたい内容を入れます。
`multipart` のときは、ただのテキスト情報も、画像などのファイルも、一緒に送れます。
`filename` には任意でファイル名を入れられますが、なくても大丈夫です。

### ファイルパスについて
Guzzleが `app` ディレクトリと同じ並びの `vendor` ディレクトリにあるせいなのか、 `Storage::url()` だけだとパスエラーになったため、パスの先頭に `../` を付けています。（他にもっと良い書き方をご存知の方がいたら教えてくださいmm）

#### その他のパラメータなど
ほかに解説記事が豊富にあってここで説明するほどでもないかなと思った項目は飛ばしていますが、「この設定が知りたい」といったリクエストが万が一あれば（素人の解説でよければ）追記します。

という訳で今日はいったんここまで。
不備不足にお気付きの方はご指摘ください。


#### 参考URL
[Guzzle Docs: Request Options - multipart](https://docs.guzzlephp.org/en/stable/request-options.html#multipart)
[POST - MDN - Mozilla](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/POST)
[PHP GuzzleHttp. How to make a post request with params?](https://stackoverflow.com/questions/27825667/php-guzzlehttp-how-to-make-a-post-request-with-params)
[Multiple files uploaded via Guzzle multipart/form-data request are not recognized by Symfony](https://stackoverflow.com/questions/47550801/multiple-files-uploaded-via-guzzle-multipart-form-data-request-are-not-recognize)
[Sample POST request with Guzzle](https://gist.github.com/juampynr/bfd5e8e38424618b3065b3f6a9713e69)
[【PHP】GuzzleでPOSTリクエストができなかったお話し](https://tektektech.com/php-guzzle-post-local-difference/)
[PHP Http ClientのGuzzleパッケージを使う](https://reffect.co.jp/php/php-http-client-guzzle)
[今時のPHP HTTPクライアントのGuzzleを使ってみた](https://qiita.com/yousan/items/2a4d9eac82c77be8ba8b)
[Guzzleを使って外部APIへPOST送信](https://qiita.com/harutakaoka522/items/52bb60b4b07d4e1a8bd8)