想定読者
この記事は、PHPのcurl拡張でHTTP/2やHTTP/3の対応状況を確認したことがあり、PHP 8.4のfeature_listが何を改善したのか知りたい人を想定しています。
あわせて、ユーザーランド関数とPHP標準APIの責任境界や、curl_has_feature()を標準化する価値に関心がある人も対象です。
コマンドラインでcurl --versionを実行すると、そのcurl環境で利用できるプロトコルや機能を確認できます。
$ curl --version
Protocols: dict file ftp ftps http https ...
Features: alt-svc AsynchDNS brotli HTTP2 HTTP3 IPv6 SSL ...
PHPからcurlを使う場合も、知りたいことは基本的に同じです。
このPHP環境で使われているlibcurlは、HTTP/2やHTTP/3に対応しているのか。
ところがPHP 8.3以前では、この問いに答えるためにビットマスクを読む必要がありました。
PHP 8.3以前はビットマスクを読んでいた
従来のHTTP/3判定は、次のように書けます。
$version = curl_version();
$supportsHttp3 =
defined('CURL_VERSION_HTTP3')
&& (($version['features'] & CURL_VERSION_HTTP3) !== 0);
curl_version()が返すfeaturesは、CURL_VERSION_*定数を組み合わせた整数です。
このコードを書くには、次の前提を理解する必要があります。
-
featuresはビットマスクである - HTTP/3の対応状況は
CURL_VERSION_HTTP3で調べる - 環境によっては定数が存在しない
- ビット演算で結果を判定する
さらに、curl拡張には似た名前の定数が並びます。
CURL_VERSION_HTTP3
CURL_HTTP_VERSION_3
CURL_HTTP_VERSION_3ONLY
CURLOPT_HTTP_VERSION
CURLINFO_HTTP_VERSION
役割はそれぞれ異なります。
-
CURL_VERSION_HTTP3は、libcurlがHTTP/3に対応しているかを調べる -
CURLOPT_HTTP_VERSIONは、通信時に使うHTTPバージョンを設定する -
CURL_HTTP_VERSION_3は、その設定へ渡す値である -
CURLINFO_HTTP_VERSIONは、実際に使われたHTTPバージョンを調べる
難しいのはビット演算そのものではありません。
多数の定数から、自分の問いに答えるものを見つけることです。
PHP 8.4でfeature_listが追加された
PHP 8.4では、curl_version()の戻り値にfeature_listが追加されました。
$features = curl_version()['feature_list'];
$supportsHttp2 = $features['HTTP2'] ?? false;
$supportsHttp3 = $features['HTTP3'] ?? false;
結果は次のような連想配列です。
[
'SSL' => true,
'HTTP2' => true,
'HTTP3' => false,
'BROTLI' => true,
]
PHP 8.3以前の判定と比べると、違いは明確です。
// PHP 8.3以前
($version['features'] & CURL_VERSION_HTTP3) !== 0;
// PHP 8.4以降
$version['feature_list']['HTTP3'] ?? false;
feature_listによって、PHP利用者は次の作業から解放されました。
- ビット演算を書く
-
CURL_VERSION_*定数を探す - libcurlの内部表現をPHP側で解釈する
環境診断やテストも書きやすくなります。
feature_listは新しい通信機能を追加したものではありません。既存の機能情報を、PHPコードから扱いやすい形へ変換した機能です。
知りたいのはバージョンではなく対応機能
ここで一つ違和感が残ります。
curl_version()['feature_list']['HTTP3'] ?? false;
知りたいのはHTTP/3への対応状況ですが、呼び出している関数はcurl_version()です。
現在のcurl_version()は、単純なバージョン取得関数ではありません。
- libcurlのバージョン
- TLSライブラリの情報
- zlibの情報
- ビルド対象ホスト
- 対応プロトコル
- 機能ビットマスク
- 機能ごとの真偽値
実態は、libcurl環境全体の診断APIに近くなっています。
既存の戻り値へfeature_listを追加した判断は、変更規模を小さくする方法として合理的です。一方で、利用者の問いに対して最も自然な入口とは限りません。
既存APIへ追加しやすい場所と、利用者が探しやすい場所は同じとは限りません。
教育用ショートカットとしてcurl_features()を作る
責務の違いを見えやすくするため、記事内でcurl_features()を定義してみます。これはPHP標準関数ではありません。
function curl_features(): array
{
static $features = null;
return $features ??=
curl_version()['feature_list'] ?? [];
}
利用側は次のようになります。
$features = curl_features();
$supportsHttp2 = $features['HTTP2'] ?? false;
$supportsHttp3 = $features['HTTP3'] ?? false;
複数の機能をまとめて確認する場合にも使えます。
$features = curl_features();
if (
($features['HTTP3'] ?? false)
&& ($features['BROTLI'] ?? false)
) {
// 必要な機能がそろっている
}
この関数は新しい判定方法を導入していません。curl_version()から機能一覧だけを取り出しているだけです。
curl_version()
libcurl環境全体の情報を取得する
curl_features()
その中から機能一覧だけを取り出す
curl_features()はユーザーランドで簡単に書けるため、標準関数として追加する必要性は高くありません。ただし、curl_version()が複数の責務を持っていることを理解する教材にはなります。
単一判定にはcurl_has_feature()
一つの機能だけを確認するなら、述語関数のほうが利用者の問いに合います。
function curl_has_feature(string $feature): bool
{
return curl_features()[$feature] ?? false;
}
if (curl_has_feature('HTTP3')) {
// HTTP/3を利用する構成を選ぶ
}
feature_listとcurl_has_feature()の違いは、判定内容ではなくAPIの入口です。
feature_list
利用できる機能を一覧する
curl_has_feature()
一つの機能が利用できるか問い合わせる
複数の機能をまとめて扱うなら一覧取得が向いています。一つだけ確認するなら、curl_has_feature()のほうが意図を直接表現できます。
curl 7.87.0は機能を名前で報告し始めた
libcurl側でも、機能情報の表現が変化しています。
curl 7.87.0では、curl_version_info_dataにfeature_namesが追加されました。利用できる機能を、NULL終端の文字列配列として取得できます。
const char *const *feature_names;
従来のfeaturesは、機能ごとにビットを割り当てる方式でした。
bit 16 = HTTP2
bit 25 = HTTP3
feature_namesでは、利用できる機能を名前で列挙します。
HTTP2
HTTP3
brotli
ECH
HTTPSRR
NativeCA
これは可読性だけの改善ではありません。新しい機能を公開するたびに、固定されたビット空間へ割り当て続ける必要がなくなります。
libcurlの現在のドキュメントでも、従来のfeaturesビットマスクよりfeature_namesの利用が推奨されています。
一覧と単一判定は同じ情報を使う
curl_has_feature()を追加するなら、feature_listとは別の判定ロジックを持たせるべきではありません。ext/curl内部でlibcurlの機能情報を読み取り、両方へ同じ結果を返します。
古いlibcurlではfeaturesビットマスク、curl 7.87.0以降ではfeature_namesも利用できます。この違いはext/curl内部で吸収します。
curl_has_feature('HTTP3')
=== (curl_version()['feature_list']['HTTP3'] ?? false);
利用者にとって重要なのは、内部表現ではなく、一覧取得と単一判定の結果が一致することです。
標準 curl_has_feature()を検討する論点
ユーザーランド版は簡単に実装できます。
function curl_has_feature(string $feature): bool
{
return curl_version()['feature_list'][$feature] ?? false;
}
しかし、この実装では機能が未対応なのか、名前を間違えたのかを区別できません。
curl_version()['feature_list']['HTTP3'] ?? false;
curl_version()['feature_list']['HTP3'] ?? false;
どちらもfalseになりますが、前者はHTTP/3未対応、後者は入力ミスです。
標準のcurl_has_feature()を設けるなら、一般的な表記揺れはext/curl側で吸収できます。
curl_has_feature('HTTP3');
curl_has_feature('http3');
curl_has_feature('HTTP/3');
一方、正規化しても認識できない名前は、falseではなくValueErrorにする設計が考えられます。
curl_has_feature('HTP3');
// ValueError
結果の意味は次のように分離できます。
true
機能を認識しており、現在の環境で利用できる
false
機能を認識しているが、現在の環境では利用できない
ValueError
機能名を認識できない
未知の名前をfalseにすれば将来追加される機能を問い合わせやすくなりますが、タイプミスも見逃します。feature_listが既知の機能名を一覧として提供していることを考えると、未知の名前をValueErrorにする方が判定の意味は明確です。
標準関数として追加する意義は、コードを短くすることだけではありません。
表記揺れはext/curlが吸収し、入力ミスはエラーにする。
falseは、正しい機能名を指定したうえで未対応だった場合だけに使う。
これにより、うろ覚えの機能名を未対応と誤判定する事故を防げます。
まとめ
PHP 8.4のfeature_listは、libcurlの機能ビットマスクを、機能名と真偽値からなる連想配列へ変換しました。
これにより、PHP利用者はCURL_VERSION_*定数やビット演算を直接扱わずに、HTTP/2やHTTP/3への対応状況を確認できます。
$supportsHttp3 =
curl_version()['feature_list']['HTTP3'] ?? false;
一方、一つの機能を確認するだけでも、curl_version()の戻り値とfeature_listの構造を知る必要があります。
curl_has_feature('HTTP3');
標準のcurl_has_feature()があれば、libcurlのバージョン差、内部表現、機能名の対応関係をext/curlへ集約し、「この機能を使えるか」という問いを直接表現できます。
feature_listは機能情報を一覧として読みやすくし、curl_has_feature()は同じ情報へ単一判定の入口を加えます。両者が同じ結果を返すことで、ext/curlはlibcurl固有の事情をPHP利用者から隠す役割を果たせます。