HTTP通信クライアント、"HttpClient"

[Symfony Component Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony-component)の17日目の記事です。

## 最初に
[Symfony](https://symfony.com)はPHPのフレームワークのひとつです。しかし、公式サイトの説明文には

> Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
  （SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。）

と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。

今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。

※記事内ではautoloadのインポートは省略します。


## HTTP通信クライアント、"HttpClient"

[HttpClient](https://symfony.com/doc/current/http_client.html)は、その名の通りHTTP通信のクライアントを扱うコンポーネントです。通常のHTTP通信に加え、SSRF対策、HTTP/2通信、ストリーム通信などに対応しています。


### インストール

```shell
composer require symfony/http-client
```

#### HTTP通信する

`HttpClient`オブジェクトを使って通信を行います。Symfonyの場合は、いつも通りオートワイヤリングでDIできます。

#### GET

```php


use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\HttpClient;

// Symfony以外の場合
$client = HttpClient::create();
$response = $client->request('GET', 'https://qiita.com/api/v2/items?query=Symfony', [
    'headers' => [
        'Authorization' => 'Bear: ......'
    ],
]);

// ステータスコード
$statusCode = $response->getStatusCode();
// レスポンスヘッダ
$headers = $response->getHeaders();
// レスポンスを文字列で
$response->getContent();
// レスポンスを配列で
$response->toArray();

// Symfonyの場合
class SomeService
{
    public function __construct(private readonly HttpClientInterface $client)
    {
    }
    
    public function someMethod()
    {
        $response = $client->request('GET', 'https://qiita.com/api/v2/items?query=Symfony', [
            'headers' => [
                'Authorization' => 'Bear: ......'
            ],
        ]);
        
        // 以下同様
    }
}
```

##### POST

POSTデータを送る場合は、`request()`メソッドの第３引数に`body`というキーで設定します。JSONを送信する場合は`body`ではなく、`json`を使うと、`Content-Type: application/json`を勝手にヘッダに追加してくれます。

```php

$client = HttpClient::create();
$response = $client->request('POST', 'https://qiita.com/api/v2/items', [
    'body' => '{"titie": "Symfonyすき"}', // 生データの場合
    'body' => ['title' => 'Symfonyすき'], // 配列の場合
    'json' => ['title' => 'Symfonyすき'], // JSONで送る場合
]);

// レスポンスはGETと同じように扱えます
```

#### SSRF（Server-side Request forgery）対策

HttpClientでは、[SSRF](https://blog.tokumaru.org/2018/12/introduction-to-ssrf-server-side-request-forgery.html)対策として、`NoPrivateNetworkHttpClient`クラスが用意されています。

`NoPrivateNetworkHttpClient`を使うと、内部サーバへのHTTP通信ができないようになります。

```php

$url = $request->request->get('your_url');

try {
    $client = new NoPrivateNetworkHttpClient(HttpClient::create());
    $response = $client->request('GET', $url);
    ...
} catch (TransportException $e) {
    // 内部サーバへのHTTP通信の場合、TransportExceptionを吐きます。
    ...
}
```

#### HTTP/2対応

以下の条件をクリアしていれば自動的にHTTP/2通信となります。

- httpsにアクセス
- libcurl 7.36以上 もしくは、amphp/http-client 4.2以上インストール済み

#### ストリーム通信対応

アクセス先がストリーミング配信の場合、`stream()`メソッドを使うことで対応可能です。

```php
$response = $client->request('GET', $url);

foreach ($client->stream($response) as $chunk) {
    print_r($chunk->getContent()); // ストリーム配信されたデータを受け取るたびに出力
}
```

また、非同期で複数のリクエストも受け取ることができます。

```php
$responses = [];
$images = [
    'a.png',
    'b.png',
    'c.png',
];
foreach($images as $image) {
    $url = sprintf("https://some-domain/%s", $image);
    $responses[] = $client->request('GET', $url);
}

foreach ($client->stream($responses) as $response => $chunk) {
    // $responseはそれぞれのレスポンスオブジェクト
    // $chunkはそれぞれの配信されたデータ
}
```

## まとめ

今回は`HttpClient`について紹介しました。PHPでHTTP通信といえば[Guzzle](https://docs.guzzlephp.org/en/stable/)ですが、Symfony公式が用意してくれてるので、せっかくならこちらを使っていきたいところです。