📑

PHPからのHTTPリクエスト (2016年版)

2023/06/11に公開

これは2016年にWEB+DB PRESS Vol.92掲載のPHP大規模開発入門連載『PHPからのHTTPリクエスト ファイル操作関数、curl関数、Guzzleライブラリ』として寄稿した原稿を改稿したものです。

ベースは2016年に書かれた記事ですが、PHPからHTTPリクエストを発行するための基礎について押さえられる内容が含まれており、2023年6月にPHPカンファレンス福岡2023で発表予定の『【令和最新版】開発者フレンドリーなHTTP SDK作るには』の前提となる議論も含まれるため、この機会に公開します。

https://twitter.com/tadsan/status/1667783853837254657


PHPはWebと親和性の高いプログラミング環境です。HTTP (Hypertext Transfer Protocol)はWebの標準的な通信プロトコルであり、Webアプリケーションを実装するということは、利用者からのHTTPリクエストを受け付けてHTTPの仕様に沿った応答を返すプログラムを書くことにほかなりません。

実際にはWebサーバやPHP、あるいはWebアプリケーションフレームワークと呼ばれる層が巧妙に隠蔽してくれるので、HTTPについてあまり意識しなくともWebサービスを構築することが可能ですが、今回はPHPから「Webサービスを利用する」という観点でHTTPについて解説します。

HTTPの基礎

HTTPはクライアント/サーバモデルと呼ばれるアーキテクチャに基づく通信モデルです。Hypertextと名前に含まれていますが、実際にはHTMLなどの文字情報によるデータだけではなく、画像や音声・動画・ゲームなどのあらゆるデータを扱うことができます。 (「マルチメディア」という言葉は久しく聞かなくなりましたね……。)

Webを支える技術──HTTP、URI、HTML、そしてREST』ではWeb開発に携わる技術者が知っておくべきWebの技術について幅広く紹介されており、HTTPについても多くの分量が割かれています。興味のわいた方はぜひ手にとってみてください。

HTTPの用途

実世界でHTTPが利用される分野は多岐に渡ります。最も目にする機会が多いのは、http://wdpress.gihyo.jp/ のようなURLの一部分でしょう。このようなURLをブラウザのアドレスバーに入力することでWebサイトを閲覧できます。URLは仕様上はURI (Uniform Resource Identifier)とも呼ばれますが、今回はURLとURIの区別には触れません。

Webサイトにも多様な形態があります。たとえばイラスト(画像)を投稿したりほかの利用者が投稿したイラストを閲覧できるpixiv、動画を投稿またはライブ配信したり、それを見ることができるYouTubeなどです。これらのサービスは不特定多数の利用者にアクセスされることを想定していますが、そうではない領域にもHTTPは利用されています。たとえば全文検索エンジンのApache SolrはApache LuceneというJavaのライブラリをHTTPを介して操作できるようにしたフロントエンドです。

LuceneはJavaのライブラリなので、当然Java(もしくはJVMで動作するプログラミング言語)以外からは直接利用できません。しかし、HTTPを介することで非常に多くのプログラミング言語から利用できます。

実際のWebサービスでは、PHPなどのWebアプリケーションが動作するサーバ(APサーバ)と検索エンジンのサーバを分ける(同じマシン/インスタンスで動作させない)構成をとることが多くあります。HTTPはそのような場合のサーバ間連携のためのプロトコルとしても利用されています。

HTTPクライアント

HTTPリクエストを送信する側のプログラムをHTTPクライアントまたはユーザーエージェントと呼びます。最も身近にあるのはWebブラウザでしょう。現代では携帯情報端末でもWebを閲覧できるのが当たり前になりました。

ブラウザを使わずPHPからHTTPリクエストを送る方法を紹介します。Pythonのrequestsモジュールを開発したKenneth Reitzが提供している https://httpbin.org/ というサービスがあります。これはHTTPリクエストのテストに便利な機能を提供してくれます。たとえば https://httpbin.org/get?foo=bar にGETリクエストすると、どのようなリクエストが受け付けられたか確認できます。

リクエスト、レスポンスの構造

HTTPを送信するためのコマンドとして、UNIX系のOSのほとんどにはcurlwgetのどちらかが標準構成でインストールされています。今回はcurlコマンドを利用します

$ curl -sv -XGET http://httpbin.org/user-agent
*   Trying 54.175.219.8...
* Connected to httpbin.org (54.175.219.8) port 80 (#0)
> GET /user-agent HTTP/1.1 (※リクエストライン)
> Host: httpbin.org
> User-Agent: curl/7.43.0
> Accept: */*
> 
# ↑ここまでリクエストヘッダ

< HTTP/1.1 200 OK  (※ステータスライン)
< Server: nginx
< Date: Tue, 08 Mar 2016 19:25:35 GMT
< Content-Type: application/json
< Content-Length: 34
< Connection: keep-alive
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Credentials: true
< 
# ↑ここまでレスポンスヘッダ

{
  "user-agent": "curl/7.43.0"
}
# ↑ここまでレスポンスボディ

* Connection #0 to host httpbin.org left intact

リクエストおよびレスポンスは、それぞれヘッダボディから構成されます。ただし、GETメソッドではリクエストボディはありませんので上記のcurlの出力にも含まれていません。

コマンドラインオプションに渡されたURL http://httpbin.org/user-agent が、リクエストヘッダのGET /user-agent HTTP/1.1Host: httpbin.orgに分割されているところに注目してください。

GET /user-agent HTTP/1.1リクエストラインと呼ばれ、メソッド(GET)、URI(/user-agent)、プロトコルバージョン(HTTP/1.1)から構成されます。メソッドはどのような操作を行いたいかを示すもので、単に情報を取得したい場合はGET、掲示板の投稿のような操作はPOSTなどを利用します

用語について

HTTPクライアントにもさまざまな形態があります。ブラウザはHTTPリクエストを行うだけでなく、画像やHTMLの表示やJavaScriptを実行エンジンとしての役割を担うなど多機能です。

Webサービスを巡回してWebページの内容を収集するプログラムはクローラスパイダーbotなどと呼ばれます。これらのプログラムを使ってWebページから特定のデータを取り出すことを、特にスクレイピングと呼びます。

特にWeb APIにリクエストを行うプログラムはAPIクライアントと呼ばれます。これらは特定のサービスに対してHTTPリクエストを行うことに特化しています。

これらのプログラムを使ってできることには「Webページを表示する」「ファイルをダウンロードする」「APIにアクセスする」などさまざまな言い回しができますが、通信だけに着目すればすべて「HTTPリクエスト」です。

HTTPサーバ

HTTPクライアントからのリクエストを受け付けるプログラムをHTTPサーバと呼びます。Apache HTTP Servernginxなど有名です。リクエストを受け付けたHTTPサーバは、Apacheの組込みモジュール(mod_php)やFastCGIを介してPHPスクリプトを実行します。今回は詳細に触れませんが、PHPはSAPIというしくみを通じて、さまざまな環境で動作させることができます

簡単なHTTPリクエスト

ここからPHPでHTTPリクエストを行う方法を解説します。簡単なHTTPリクエストについては、PHP標準のファイル操作関数だけでHTTPリクエストを行うことができます。

ストリームラッパ

ファイル操作関数にはfile_get_contents()copy()readfile()fopen()などがあります。PHPはこれらの関数を介してHTTPリクエストを送ることができます。どのように実現しているのか説明します。

ストリームラッパはPHP標準の機能で、特別なファイルやネットワーク上のリソースなどを通常ファイルと同様に読み書きできるしくみです。このしくみによって、Webサイトのコンテンツを取得するような処理は簡単に書くことができます。

説明だけだと難しそうなのですが、実際には次のような関数呼び出しだけです。ここではfile_get_contents()を例に挙げます。

<?php
$s = file_get_contents('http://httpbin.org/get?foo=bar');
echo $s;

次のように実行します。

$ php ./easy_http_request.php
{
  "args": {
    "foo": "bar"
  },
  "headers": {
    "Host": "httpbin.org"
  },
  "origin": "182.249.XXX.XXX",
  "url": "http://httpbin.org/get?foo=bar"
}

file_get_contents()はファイルを開いて内容を文字列として読み込む関数です。file_get_contents(__DIR__.'/data.txt')のようにローカルにあるファイルを読み込むのが通常の利用方法ですが、ここにhttp://またはhttps://から始まるURLを指定することでHTTPリクエストできます。

注意点としては、php.iniallow_url_fopenが無効化されている場合は、ストリームラッパ経由でHTTPリクエストを行うことができません。その場合は、本節は読み飛ばして後半のGuzzleを利用しましょう。

ストリームコンテキスト

http://ストリームラッパは、デフォルトではGETメソッドでHTTPリクエストを行います。しかし、オプションを指定することでPOSTなどほかのHTTPメソッドでリクエストできます。

file_get_contents()などのファイル操作関数はすべてストリームコンテキストという、オプションが設定された特別な値を受け取ります。ストリームコンテキストの作成にはstream_context_create()という関数を利用します。

それでは実際にストリームラッパでHTTPリクエストを行う方法を解説します。

フォームデータを送信する

フォーム形式(application/x-www-form-urlencoded)は、ブラウザがHTMLのform要素をもとにWebサーバにデータを送る際の標準的なデータ形式です。一般的な形式なので、現在ではブラウザではないHTTPクライアントからも利用されることがあります。PHPの$_POST変数は、これを利用しやすいようにデコードしたものです。

フォームデータは key1=value1&key2=value2 のように&=でつなげた形式です。文字はURLエンコードする必要がありますが、http_build_query()関数で配列から変換できます。

// フォームデータを配列で作成してエンコードする
$form = ['fizz' => 'buzz'];
$body = http_build_query($form);

// 連想配列ではないので注意
$headers = [
    'Content-Type: application/x-www-form-urlencoded',
    'Content-Length: ' . strlen($body),
    'User-Agent: PHP/' . PHP_VERSION,
    'X-Foo: Bar'
];

$opts = [
    'http' => [
        'method' => 'POST',
        // ヘッダーは改行(CR+LF)区切りの文字列で渡す必要がある
        'header' => implode("\r\n", $headers) . "\r\n",
        'content' => $body,
    ]
];

// ストリームコンテキストを作成し、引数に渡す
$context = stream_context_create($opts);
$r = file_get_contents('http://httpbin.org/post', false, $context);

// 扱いやすいように、JSONからPHPのデータ型に変換する
var_dump(json_decode($r, true));

関数の使い分け

ストリームラッパが利用できるファイル操作関数にはいくつかあります。

file_get_contents()

file_get_contents()は、レスポンスをプログラム内で操作したい場合に便利です。先のサンプルコードのようにjson_decode()したい場合などです。基本的にはこれを使っておけば大丈夫ですが、レスポンスが巨大な場合はメモリ使用量の上限を超えてしまい、エラーになる場合があるので気を付けてください。

copy()

copy()は、レスポンスをファイルに保存したい場合やメモリに載りきらないほど巨大なデータを一時保存したい場合などに利用できます。

// 巨大なファイルを保存する例
copy('http://example.com/large.iso', '/tmp/large.iso');

readfile()

readfile()は、レスポンス結果を(加工せず)そのまま標準出力します。プロキシのような振る舞いをさせたい場合に利用できます。また、CLIでスクリプトを書く際に標準出力したい場合にも利用できます。

// 画像ファイルをほかのサーバから取得してそのまま出力する
header('Content-Type: image/jpeg');
readfile('http://example.com/icon.jpg');

fopen()

fopen()はファイルポインタを開きます。fread()fwrite()などの関数を使ってファイルの読み書きを行えます。

レスポンスのハンドリング

ストリームラッパを使ってHTTPアクセスを行うと、特殊なローカル変数$http_response_headerが参照できるようになります。この値を判別することでレスポンスに応じた処理をすることができます。

var_dump()で表示してみると次のようになります。

var_dump($http_response_header);
// => [
//      "HTTP/1.1 404 NOT FOUND",
//      "Server: nginx",
//      "Date: Tue, 29 Mar 2016 23:31:56 GMT",
//      "Content-Type: text/html",
//      "Content-Length: 233",
//      "Connection: close",
//      "Access-Control-Allow-Origin: *",
//      "Access-Control-Allow-Credentials: true",
//    ]

HTTPレスポンスコードなどでレスポンスボディ以外の条件から分岐する必要がある場合などは、次のようにすると便利です。

// HTTPバージョンとレスポンスコードを分割する
[$http_version, $response_code, $phrase] = explode(' ', $http_response_header[0], 3);

curl拡張

cURLはさまざまな通信プロトコルをサポートするC言語のライブラリです。PHPには標準でlibcurlのバインディング関数が含まれていますが、libcurlをインストールしてPHPをビルドしないと有効になりません。これらを有効にすることで、ストリームラッパよりも高機能なHTTP通信を行うことができます。

curl関数が有効かどうかはコマンドラインで php -r'var_dump(extension_loaded("curl"));' を実行することで判別できます。Debian GNU/Linuxであればphp5-curlまたはphp-curlを追加でインストールすることで有効になります。DockerのPHPコンテナでは docker-php-ext-install curl で有効化するのが簡単でしょう。

一般的な傾向では、PHP組込みのストリームラッパよりもcurl拡張を使ったほうがパフォーマンス上有利だと言われています。

参考までに、このあと解説するGuzzleなどのライブラリを利用せずにcurl関数だけを使ってHTTPリクエストを行う例は次のとおりです。

$form = ['fizz' => 'buzz'];
$body = http_build_query($form);

// curlのリクエストに必要なリソースを作成する
$ch = curl_init('http://httpbin.org/post');
curl_setopt_array($ch, [
    CURLOPT_USERAGENT => 'MyClient/1.2.3',
    CURLOPT_POST => 1,
    CURLOPT_POSTFIELDS => $body,
]);
$r = curl_exec($ch);

pecl_http

かつてよく利用されていたライブラリにpecl_httpがあります。PHPマニュアルに掲載されており準標準的な扱いの機能だったのですが、PHPには標準で付属しておらずPECL(The PHP Extension Community Library)からインストールする必要がありました。

PHPの標準ライブラリとして統合される提案もありましたが反対多数で否決され、2016年1月にPHPマニュアルからも削除されました。Guzzleもpecl_httpはサポートしていません。

2023年においても開発は継続されていますが、現在となっては、もはや積極的に選択する理由はないでしょう。

Guzzleライブラリを利用する

簡単なHTTPリクエストであれば、ここまでに紹介したストリームラッパとPHPの標準関数を利用するのが手軽です。しかし、弱点としてHTTPリクエストを行う処理のテストがしにくいことが挙げられます。大規模なアプリケーション開発にはテストを欠かすことはできませんが、Guzzleを利用することでテストしやすい構造にすることができます。

またライブラリを利用することで、一般的なエラー処理についても大幅に簡略化できます。HTTPの404 Not Foundや500 Internal Server ErrorなどはWeb APIのクライアントなどの正常な動作では起こり得ない状態ですが、異常な事態として検知しやすいようにPHPの例外に変換してスローする動作がデフォルトになっています。

Guzzleの導入

Guzzleは本稿執筆時点の2016年現在で最も主流となっているHTTPクライアントです。オブジェクト指向的でわかりやすいインターフェイスを備えています。Guzzleは内部的にはcurl関数を利用しています。

インストールにはComposerを使って、次のようにコマンドを実行します。

$ cd /path/to/your-project
$ composer require 'guzzlehttp/guzzle:6.*'

コマンドを実行したあと、composer.jsonに記述されているバージョンが6以上であることを確認してください。6系以外ではクラス/メソッドの構成が異なりますので注意してください。

Guzzleライブラリ

Guzzle 6は次の3つのライブラリに分かれています。

PSR-7は、PHP-FIG (PHPの開発者有志によるフレームワークとライブラリの相互運用のための標準仕様を策定しているグループ)によって策定されたHTTPのインターフェイスです。guzzlehttp/psr7はPSR-7のインターフェイスを実装したHTTPメッセージクラスおよび、実際のユースケースに沿った豊富なストリームクラスを持っています。

Promises(プロミス)は非同期処理に対応したインターフェイスのパターンです。guzzlehttp/promisesPromises/A+というJavaScriptの規約をPHPに実装したものです。

GuzzleでHTTPリクエスト

次のコードでは、Guzzleを使ってPOSTメソッドのリクエストを送っています。

use GuzzleHttp;

$form = ['fizz' => 'buzz'];
$client = new GuzzleHttp\Client();
$r = $client->post('http://httpbin.org/post', [
    'headers' => [
        'X-Foo' => 'Bar'
    ],
    'form_params' => $form,
    'sink' => '/tmp/download/savefile.iso',
]);

// 扱いやすいようにJSONからPHPのデータ型に変換する
var_dump(json_decode($r->getBody(), true));

GuzzleHttp\ClientクラスのメソッドはHTTPメソッドと対応しています。つまり、$client->get()でGETメソッド、$client->post()でPOSTメソッドのリクエストを送ることができます。引数はURLとオプション引数の2つの値を受け取ります。

$client->get($url, array $options);

この$optionsはさまざまなキーをとります。項目ごとのサンプルコードには必要なもののみを記載していますが、実際には上記のサンプルコードのように、1つの配列に組み合わせて設定します。

headers

headersにより、HTTPヘッダを指定できます。たとえばAccept-Languageを指定することで、国際化されたWebサイトではzh-CNなら中国語のコンテンツを、enなら英語のコンテンツを取得できるといった具合です。

// 独自のUser-Agentを設定しAPIの認証情報を付与した例
$options = [
    'headers' => [
        'User-Agent' => 'MyClient/1.2.3(+http://example.com/bot.html)',
        'Accept-Language' => 'zh-CN',
        'Authorization' => 'Bearer XXXXXXXXXX',
    ],
];

User-Agentはログの集計などによく用いられます。明示的に指定しなかった場合はGuzzleHttp/6.1.1 curl/7.47.1 PHP/7.0.4のような形式で自動生成されます。他者が管理するサーバに対して連続してリクエストを行う場合は+http://example.com/bot.htmlのような形式でサーバの管理者に連絡先やプログラムの目的を知らせるURLを掲載するのがエチケットです。

query

クエリパラメータはURLに情報を付加するしくみです。フォームデータと同じような形式で、リクエストボディではなくURLの一部に情報が付加されることが特徴です。たとえば検索機能を持ったWebサービスのURL/search?q=Opera&order=dateのような形式です。

Guzzleではqueryに連想配列の形式で指定できます。

// 検索キーワード「Opera」を日付順にソートする場合
$options = [
    'query' => [
        'q' => 'Opera',
        'order' => 'date',
    ],
];

上記の例ではリクエスト時にq=Opera&order=dateに変換されます。

クエリパラメータはURLの一部なのでHTTPメソッドに関係なく利用できますが、慣習としてPOSTメソッドではフォームデータを、GETメソッドではクエリパラメータを使って情報の受け渡しを行うことが多いです

オプションでqueryを指定せず、 $client->get('http://httpbin.org/get?foo=bar') のように直接記述することも可能です。URLとqueryオプションを両方指定した場合はqueryオプションが優先されます。

form_params

form_paramsでは、PHPの配列をフォーム形式(application/x-www-form-urlencoded)に変換してリクエストします。指定方法はqueryと同様です。後述するjsonmultipartbodyとは同時に指定できません。

json

いわゆるRESTfulなWeb APIには、フォーム形式ではなくJSON形式(application/json)でリクエストを受け付けるものがあります。冒頭で紹介したApache Solrもその一つです。jsonを利用すればJSON形式でリクエストを行えます。

これも指定方法はqueryと同様です。リクエストボディを登録するform_paramsmultipartbodyとは同時に指定できません。

multipart

マルチパートフォーム形式(multipart/form-dataはHTMLフォームからデータを送信する形式の一つです。主な特徴はファイルのアップロードに対応できることです。

// 2つのファイルと、それぞれのタイトルを送信する
$options = [
    'multipart' => [
        [
            'name' => 'title1',
            'content' => 'ワンダースワンカラー',
        ],
        [
            'name' => 'file1',
            'contents' => fopen('/tmp/wsc.png, 'r'),
            'filename' => 'wonderswan.png',
        ],
        [
            'name' => 'title2',
            'content' => 'ゲームボーイアドバンス',
        ],
        [
            'name' => 'file2',
            'contents' => fopen('/tmp/gba.png, 'r'),
        ],
    ],
];

ファイルをアップロードするときは、contentsファイルリソースまたはファイルの内容の文字列と、filenameにファイル名を指定します。contentsがファイルリソースならfilenameの指定は省略できます。

queryjsonとは異なり、key => value形式の配列ではないことに気を付けてください。

auth

HTTPにはパスワード認証のしくみとしてBASIC認証Digest認証があります。認証を行うには、authオプションの引数としてユーザー名・パスワード・認証方法の順に格納された文字列の配列を渡します。

// BASIC認証の場合
$options = [
    'auth' => ['john', 'PasSW0Rd']
];

// Digest認証の場合
$options = [
    'auth' => ['taro', 'p4s5wOrD', 'digest']
];

BASIC認証は手軽なのですが、認証情報がネットワーク上に平文で流れるので機密情報を扱うためには向きません。Digest認証はBASIC認証を改善した方式ですが、通信経路を暗号化するわけではないので注意が必要です。これらの問題は、認証ページをHTTPSにすることで通信を暗号化できるため第三者の盗聴を抑制できます。

authではOAuthやOpenID Connectなどにより認証を行うWeb APIで利用されるBearerトークンはサポートしていません。認証の必要がある場合は「headers」のところで紹介したサンプルコードを参考にしてください。

sink

sinkは、HTTPレスポンスの保存領域を指定するオプションです。これはPSR-7およびGuzzleがストリームを多用したライブラリであることが顕著にわかる部分です。

ファイルリソースファイルパスの文字列またはStreamInterfaceを実装したオブジェクトを指定します。無指定の場合はphp://tempが利用されます。

// 巨大なzipをファイルに直接保存する
$options = [
    'sink' => '/path/to/large.zip',
];

// 5MBまではテンポラリファイルを利用しないようにする
$options = [
    'sink' => fopen('php://temp/maxmemory:5242880', 'r+'),
];

// 最大で5MBまでしか保持せず、溢れたものは切り捨てる
use GuzzleHttp\Psr7;
$stream = Psr7\stream_for(fopen('php://memory', 'r+'));
$options = [
    'sink' => new Psr7\DroppingStream($stream, 5242880),
];

php://memoryphp://tempはどちらも読み書きができるストリームです。実はこれもストリームラッパの一種で、2MBまではデータの保存領域としてメモリを利用し、サイズがその閾値を超えるとシステムが自動的に一時ファイルに保存します。そうすることで無制限にメモリが浪費されることを防いでくれます。

巨大なデータをダウンロードする場合は、最初から文字列でファイルパスを指定してファイルに書き込んでおいたほうが処理の無駄がありません。

proxy

proxyオプションでは、HTTPプロキシを設定します。企業内ネットワークから外部のインターネットと通信するためにプロキシを利用する必要がある場合や、開発用にFiddlerなどのネットワークキャプチャツールを利用する場合などに設定します。

// すべての通信をlocalhost:8765を介する設定
$options = [
    'proxy' => 'tcp://localhost:8765',
];

// HTTPとHTTPSでは別のproxyを設定する
$options = [
    'proxy' => [
        'http'  => 'tcp://localhost:5678,
        'https' => 'tcp://localhost:6666',
        // foo.comと*.mit.eduに対してはプロキシを利用しない
        'no' => ['.mit.edu', 'foo.com'],
    ],
];

http_errors

http_errorsでは、HTTPの正常以外のステータスコード(特に4xx系や5xx系)をPHPの例外として扱うかを設定します。Web APIのクライアントであれば2xx以外が返るのは異常事態なので、この挙動はたいへん有用です。

しかし、APIではない不特定のWebページから情報を収集するような処理では、いちいち例外が発生してしまってはかえってやりにくくなってしまうことがあります。そのようなときは、http_errorsを無効化することで例外を抑制できます。

$options = ['http_errors' => false];

verify

verifyは、HTTPS(HTTP over TLS)通信を設定します。bool値または、サーバ証明書のファイルパスの文字列を指定します。通常では利用する必要はありませんが、プロキシのために独自の証明書を追加する場合や、システムにインストールされた証明書が古いので新しい証明書と差し替える必要がある場合に設定します。

// /path/to/cert.pemに配置された証明書ファイルを利用する
$options = [
   'verify' => '/path/to/cert.pem',
];

debug

debugは、名前のとおり開発中の検証用に役立つオプションです。通信中の情報を標準出力またはファイルストリームに書き込みます。

// 通信情報を標準出力する
$options = ['debug' => true];

// エラーを $f ファイルに保存する
$f = fopen('php://memory', 'r+');
$options = ['debug' => $f];

このように$f変数に情報を出力させたあと、 rewind($f); $s = stream_get_contents($f); を実行すると$sに文字列として取得できます。

PSR-7ミドルウェアとテスト

Guzzle 6の特徴は、PHPでHTTPを取り扱う仕様としてPSR-7を取り入れたことです。実装と仕様(インターフェイス)が分かれたことで、HTTPクライアントのテスト時にcURLやストリームラッパなど具体的な実装を考慮せず済みます。さらに、HTTPリクエストのテストで実際のHTTPリクエストを行う必要がありません

GuzzleにはPromiseインターフェイスを使ったハンドラスタックというしくみがあり、PSR-7形式のオブジェクトを取り扱う関数を数珠つなぎのように実行するようになっています。

プロミスは状態遷移を伴った複数の機能を組み合わせるための標準的なインターフェイスとしてJavaScriptではすでに一般的な概念です。JavaScriptと実行モデルが異なるPHPで普及するかは未知数ですが、興味を持たれた方は『JavaScript Promiseの本』を読むとよいでしょう(Webで全文無料で読むことができます)。

MockHandler

MockHandlerはHTTPリクエストをモック化することに特化したGuzzleハンドラです。MockHandlerは登録された$queueの値を一つ一つ返します。このハンドラを利用すれば、実際のHTTP通信に依存せずにテストを実施できます。

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;

$mock = new MockHandler([
    new Response(200, [], 'ok'),
    new Response(500, [], 'error'),
    new ConnectException("connection error",
        new Request('GET', 'test')
    ),
]);

$handler = HandlerStack::create($mock);

// GuzzleのHTTPClient作成時にハンドラを設定する
$client = new Client(['handler' => $handler]);

echo $client->get($url)->getBody();

PSR-7ミドルウェアの応用

今回はテストの簡単な例しか紹介できませんでしたが、これを応用すれば、たとえば他社や別チームで開発されているWeb APIと連携したいがまだ完成していないといった状況で、別途サーバを用意せずにテスト可能な環境を作るといったことが可能になります。

また極端な例では、社内で連携するWebアプリケーションがPSR-7準拠であれば、ApacheやnginxなどのHTTPサーバに依存せず、連携Webアプリケーションを同一プロセスで動作させライブラリとして利用することで通信のオーバーヘッドを減らす、といった選択肢も生まれるでしょう。

Discussion