MultipartStreamを使用してmultipart/form-dataでPOSTする

## 目的
`multipart/form-data`形式で添付ファイルを伴いPOSTするForm画面を作ったが、諸般の事情でBrowserKitを使ったテストを使っており、その流れで`MultipartStream`を使うことになったのでその手順を記載します。

## テストするフォーム
テストするフォームはここでは簡易的にビルトインウェブサーバーを使用してダミーのものを用意します。

```shell
% mkdir public_html
% cd ~/public_html
% vi test.php
%
```

```php5:test.php
<?php

// POSTされた内容を表示
$fp = fopen('php://stdout', 'w');
fprintf($fp, var_export($_POST, true));
fprintf($fp, "\n");

// ファイルがある場合は情報と内容を表示
foreach($_FILES as $file) {
    fprintf($fp, "ファイル情報\n");
    fprintf($fp, var_export($file, true));
    fprintf($fp, "ファイル内容\n");
    fprintf($fp, var_export(file_get_contents($file['tmp_name']), true));
}
```

```shell:ビルトインウェブサーバーを起動
% php -S localhost:8000 -t ~/public_html
[Sun Jun 15 17:01:13 2025] PHP 8.4.7 Development Server (http://localhost:8000) started
```

## テストスクリプト
```shell-session:テスト環境作成
mkdir test_script
cd test_script
# composer.phar をインストール（手順省略）

php composer.phar require symfony/browser-kit
php composer.phar require symfony/http-client
php composer.phar require symfony/mime  
php composer.phar require guzzlehttp/guzzle

vi post_multipart_form.php
```

```php5:post_multipart_form.php
<?php

require_once __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Psr7\MultipartStream;
use GuzzleHttp\Psr7\UploadedFile;
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;

function getBrowser(): HttpBrowser
{
    try {
        $client = HttpClient::create();
        $browser = new HttpBrowser($client);
    } catch (Exception $e) {
        echo $e->getMessage() . "\n";
    }
    return $browser;
}

function create_post_data(array $array, string $prefix = '', string $suffix = ''): array
{
    $result = [];

    foreach ($array as $key => $value) {
        // 再帰的に処理
        if (is_array($value)) {
            $result = array_merge($result, flatten($value, $prefix . $key . $suffix . '[', ']'));
        } else {
            if ($value instanceof UploadedFile) {
                // ファイルの場合
                $result[] = [
                    'name' => $prefix . $key . $suffix,
                    'filename' => $value->getClientFilename(),
                    'Mime-Type' => $value->getClientMediaType(),
                    'contents' => file_get_contents($value->getClientFilename()),
                ];
            } else {
                // ファイル以外の場合
                $result[] = [
                    'name' => $prefix . $key . $suffix,
                    'contents' => $value,
                ];
            }
        }
    }

    return $result;
}

// POSTするデータ
$file = __DIR__ . '/test.txt';
$data = [
    'title' => 'こんにちは',
    'description' => 'よろしくお願いします',
    'file' => new UploadedFile($file, filesize($file), UPLOAD_ERR_OK, $file, 'text/plain')
];

$browser = getBrowser();
$stream = new MultipartStream(create_post_data($data));
$contents = $stream->getContents();
$browser->request(
    'POST',
    'http://127.0.0.1:8000/test.php',
    [],
    [],
    ['Content-Type' => 'multipart/form-data; boundary=' . $stream->getBoundary()], $contents);
```
:::note info
ここで重要なのは`Content-Type`に`boundary=`を**必ず指定すること**です。これが無い or 指定した値に誤りがあるとサーバーサイドでは`multipart/form-data`であることを正確に認識してくれません。
:::

アップロードするファイルは適当に用意
```text:text.txt
あああああああああああああああああ
```

実際にPOSTしてみます

```shell-session
% php post_multipart_form.php
```

ビルトインサーバー側では以下のように表示されます
```shell-session
[Sun Jun 15 18:35:46 2025] PHP 8.4.7 Development Server (http://localhost:8000) started
[Sun Jun 15 18:35:49 2025] 127.0.0.1:65123 Accepted
array (
  'title' => 'こんにちは',
  'description' => 'よろしくお願いします',
)
ファイル情報
array (
  'name' => 'test.txt',
  'full_path' => 'test.txt',
  'type' => 'text/plain',
  'tmp_name' => '/private/var/folders/1x/zscw44zn5pjczmjb4cl7lzqr0000gn/T/php0bpnfo4fk4jkbbzgFC7',
  'error' => 0,
  'size' => 52,
)
ファイル内容
'あああああああああああああああああ
'[Sun Jun 15 18:35:49 2025] 127.0.0.1:65123 [200]: POST /test.php
[Sun Jun 15 18:35:49 2025] 127.0.0.1:65123 Closing
```

POSTした内容が`multipart/form-data`形式となっていることとファイルの内容が正しいことが確認できました。以上です。