関数(2)
書記素対応文字列関数にロケールを追加
PHP 8.5では、書記素対応文字列関数に、引数としてロケールを追加できるようになりました。既出のgrapheme_levenshtein関数を含む以下の関数に引数localeを指定可能です。
- grapheme_strpos()
- grapheme_stripos()
- grapheme_strrpos()
- grapheme_strripos()
- grapheme_strstr()
- grapheme_stristr()
- grapheme_levenshtein()
ロケールを指定する意味は、文字列がどのような言語に基づくものであるかを明示し、文字列マッチングの精度を上げることで、単一のコード体系で多国語をカバーするUnicodeでは必須と言えます。
例えば以下のリスト(1)では、トルコ語の小文字「İ」(\u{0130})が正しく認識されないため、大文字小文字の区別をしないgrapheme_stripos関数の呼び出しでも、マッチする部分がないとしてfalseが返されます。
var_dump(grapheme_stripos("i", "\u{0130}", 0)); // (1)false
var_dump(grapheme_stripos("i", "\u{0130}", 0, "tr_TR")); // (2)0
var_dump(grapheme_stripos("i", "\u{0130}", 0, "en_US")); // (3)false
(2)ではロケールにトルコ語を表すtr_TRを指定することで、「İ」(\u{0130})が正しく認識されて0文字目で一致とされます。(3)は英語を表すen_USであるので、ここではまた不一致となります。
ロケールは、Locale Data Markup Language(LDML)に基づいて指定します。tr_TRやen_USはこれに基づくものですが、さらに強度(Stlength)を付加することで、マッチングの精度をコントロールすることができます。強度の指定は、日本語の異体字間でのレーベンシュタイン距離の算出やマッチングに利用できます。
例えば以下のリストは、異体字が存在する「葛」について、それぞれレーベンシュタイン距離の算出と検索を行ってみた例です。なお、「葛」に続いている\u{E0101}は異体字セレクタ(IVS)と言い、U+E0100~U+E01EFの範囲で「どの異体字を表すか」を指定するためのものです。
$katsu = '葛';
$katsu_E0101 = "葛\u{E0101}";
var_dump(grapheme_levenshtein($katsu, $katsu_E0101)); // (1)0
var_dump(grapheme_levenshtein($katsu, $katsu_E0101, locale: "ja_JP-u-ks-identic")); // (2)1
var_dump(grapheme_strpos($katsu, $katsu_E0101)); // (3)0
var_dump(grapheme_strpos($katsu, $katsu_E0101, locale: "ja_JP-u-ks-identic"));
// (4)false
(1)ではロケールの指定がないので異体字でも一致と見なされてしまいます。(2)ではロケールにja_JP-u-ks-identic(異体字を調べることのできるマッチング強度を表す:https://www.unicode.org/reports/tr35/tr35-collation.html#Combining_Rulesを参照)を指定したので、異なるものとして扱われています。(3)(4)も同様です。
書記素対応文字列関数にロケールを指定できるようになったことで、レーベンシュタイン距離を求めたり、文字列の検索を行う場合の国際化対応が容易になります。
get_error_handler関数とget_exception_handler関数
PHP 8.5では、エラーハンドラと例外ハンドラ(エラーと例外をカスタム処理する関数)がそれぞれget_error_handler関数とget_exception_handler関数で取得できるようになりました。
もともと、エラハンドラを操作する関数としてset_error_handler関数とrestore_error_handler関数が存在しましたが、取得のための関数がありませんでした。新たな2つの関数によって、取得と設定の関数が揃ったことになります。以下のリストは、設定、取得、復帰を確かめています。
function myErrorHandler($errno, $errstr, $errfile, $errline)
{
return false;
}
$old_handler = set_error_handler('myErrorHandler'); // セット前のハンドラ
var_dump($old_handler); // NULL
$current_handler = get_error_handler(); // 現ハンドラ
var_dump($current_handler); // string(14) "myErrorHandler"
restore_error_handler(); // ハンドラの復帰
var_dump(get_error_handler()); // NULL
これはエラーハンドラの例ですが、例外ハンドラについても同様です。
curl_share_init_persistent関数
PHP 8.5では、リクエスト間でcurlハンドルを共有できるようになるcurl_share_init_persistent関数が使えるようになりました。
curlハンドルとは、HTTPによって外部リソースにアクセスするライブラリcURLで使うハンドルです。curl_init関数でハンドルを取得し、curl_setopt関数でオプションを設定、curl_exec関数でリクエストを実行するのが基本形です。
$url = "http://www.google.co.jp/"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); $res = curl_exec($ch); var_dump($res); // <HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8"> // …
curl_share_init関数を使うと、単一のリクエスト内で共有できるハンドルを作成できます。共有ハンドルと複数のcurlハンドルをひも付けることで、curlハンドル間でクッキーなどを共有できるようになります。ただしあくまでも単一リクエスト内であるので、リクエスト終了時には接続は閉じられます。
curl_share_init_persistent関数を使うと、複数のリクエストにまたがってcurlハンドルを共有できます。これは、1回のリクエストが終了しても接続は生き続け、次のリクエストでもそれが使われることを意味します。
$sh = curl_share_init_persistent([CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_CONNECT]);
$ch1 = curl_init("http://www.google.co.jp/");
curl_setopt($ch1, CURLOPT_SHARE, $sh);
curl_setopt($ch1, CURLOPT_VERBOSE, true);
$res = curl_exec($ch1);
var_dump($res);
// …略…
// * Host www.google.co.jp:80 was resolved.
// * IPv6: (none)
// * IPv4: 172.217.161.35
// * Trying 172.217.161.35:80...
// * Established connection to www.google.co.jp (172.217.161.35 port 80) from 192.168.108.52 port 50175
// * using HTTP/1.x
// …略…
$ch2 = curl_init("http://www.google.co.jp/");
curl_setopt($ch2, CURLOPT_SHARE, $sh);
curl_setopt($ch2, CURLOPT_VERBOSE, true);
$res = curl_exec($ch2);
var_dump($res);
// …略…
// * Reusing existing http: connection with host www.google.co.jp
// …略…
同じオプションを持つ共有ハンドルは、curl_share_init_persistent関数によって1回だけ作成されます。2回目以降の呼び出しは、同じオプションであれば作成済みのものが使われます。上記の例では、1回目のcurl_exec関数の戻り値には接続先の情報が細かく含まれるのに対し、2回目の戻り値には既存の接続を再利用する旨の情報のみとなり、接続が共有されていることが分かります。
このように接続に伴うコストは低減できますが、状況によっては接続が長時間開いたままになるので、外部リソース側では相応の負担が生じることに注意が必要です。
RFC3986/WHATWG標準準拠のURL API
PHP 8.5では、RFC3986/WHATWG標準に基いたURI/URLを正しく扱えるようになりました。
RFC3986とは、2005年に標準化されたURIの書式です。この構文に従うことで、アプリ内はもちろん外部とのやり取りにおいても、正しいURIを使うことが可能になります。
後者はWHATWG URL Standardと呼ばれるURLの標準で、RFC3986のURL版のような位置付けです。JavaScriptを含むWebブラウザでの利用を想定しており、用語もURLに統一されています。URLを正規化する機能も含むので、Webに特化したシステムなら後者への準拠が考えられるでしょう。
しかしながら、PHPではparse_url関数など機能によって古いRFC2396を使っていたりと対応がバラバラで、セキュリティ上のリスクにもなり得るものでした。そこでPHP 8.5では新たにUri\RFC3986モジュール(Uri\WhatWgモジュール)が利用可能になり、URIの生成や加工、取得といった操作を標準に基づいて行えるようになりました。
$uri = new Uri\Rfc3986\Uri("https://naosan.jp"); // この2行は同じ意味
$uri = Uri\Rfc3986\Uri::parse("https://naosan.jp");
var_dump($uri);
// Uri\Rfc3986\Uri Object
// (
// [scheme] => https
// [username] =>
// [password] =>
// [host] => naosan.jp
// [port] =>
// [path] =>
// [query] =>
// [fragment] =>
// )
$url = new Uri\WhatWg\Url("https://naosan.jp"); // この2行は同じ意味
$url = Uri\WhatWg\Url::parse("https://naosan.jp");
print_r($url);
// Uri\WhatWg\Url Object
// …以降は同様…
ここではUri\Rfc3986モジュールとUri\WhatWgモジュールを使っていますが、この範囲では両者は同じオブジェクトとなります。生成したURI(URL)からは、豊富なメソッドを使って加工や文字列の取得、判定を行うことができます。以下のリストは、その一部です。
echo $url->withPath("/contact")->toAsciiString() . PHP_EOL; // https://naosan.jp/contact
echo $url->withQuery("id=100")->toAsciiString() . PHP_EOL; // https://naosan.jp/?id=100
CHIPSのためのsetcookie関数オプションpartitioned
PHP 8.5では、setcookie関数(setrawcookie関数)にCHIPS(Cookies Having Independent Partitioned State)に対応するオプションpartitionedを追加できるようになりました。
CHIPSとは、プライバシーサンドボックス技術の一つで、サードパーティーCookieをドメインごとに分ける仕組みです。サードパーティーCookieの問題点としては、複数のWebサイトを横断してのデータ収集や共有が可能になってしまうことですが、これをWebサイトごとに制限することで、ユーザーの意図しない情報共有を防ぐことができます。
PHPには、クッキーをセットするsetcookie関数がありますが、これまでCHIPSには対応しておらず、samesiteオプションに"Partitioned;"を紛れ込ますなどの対応策がとられてきました。PHP 8.5では正式にpartitionedオプションが指定可能になり、このような回避策は不要になりました。
setcookie('name', 'value', ['secure' => true, 'partitioned' => true]);
// PHP 8.4まで
setcookie('name', 'value', ['secure' => true, 'samesite' => 'strict; Partitioned;']);
