AQ Tech Blog

PHP8.2の新機能について | AQ Tech Blog

作成者: chihiro.awatsu|2022年12月21日

サムネ出典URL:https://www.php.net/download-logos.php

はじめに

2022年11月24日にリリースされたPHP8.2での変更について、あまり日本語での情報が多くないため、RFCを実際に翻訳したものを紹介します。 翻訳した内容をまとめたものと1つ1つの変更点に対する意見・感想、最後にまとめという形で構成しています。 それではよろしくお願いします。

新たなランダム拡張の導入

これまでのPHPの乱数にはいくつかの問題があったそうです。大きな問題としては以下の4つがあります。

  • メルセンヌツイスタがいまだに利用されている
  • 状態がグローバルな領域に保存されている
  • 不完全なランダム性
  • 内部実装の乱雑さ

それぞれについて解説していきます。

 

メルセンヌツイスタがいまだに利用されている

まずメルセンヌツイスタの概要を説明します。 メルセンヌツイスタは1996年に国際会議で発表された擬似乱数列生成器です。当時としては、あらゆる擬似乱数生成法の中で最も速いものでした。また、乱数の周期としても十分な長さを確保しているものです。 上記のように、メルセンヌツイスタは優れた擬似乱数列生成器ですが、現代のニーズにはもはや適していません。 一般に、乱数の周期が長いことは良いことですが、それにもかかわらず、いくつかの擬似乱数列生成器に対する乱雑さを計るテスト(BigCrushとCrush)で失敗しています。 また、メルセンヌツイスタが生成できるサイズは32bitに制限されていますが、多くの実行環境が64bit である現状とマッチしていません。

 

状態がグローバルな領域に保存されている

メルセンヌツイスタの状態は暗黙のうちにPHPのグローバルな領域に保存されており、それにアクセスする方法はありません。 そのため、シーディングと実際の乱数の利用の間に、乱数を利用した関数を追加すると状態が変わってしまい、シーディングを行ったにもかかわらず結果が変わってしまいます。RFCに記載されていたコード例は以下の通りです。

<?PHP

function foo(): void {
// do nothing;
}

mt_srand(1234);
foo();
mt_rand(1, 100); // result: 76
<?PHP

function foo(): void {
str_shuffle('abc'); // added randomization
}

mt_srand(1234);
foo();
mt_rand(1, 100); // result: 65

このように同じ1234という値のシードが与えられたはずなのに mt_rand を実行するまでの間に str_shuffle() のような乱数を利用する関数が実行されていた場合に結果が変わってしまっています。 この問題によって、外部パッケージを利用している際に乱数の状態の管理が困難になってしまいます。 またPHP8.1で導入されたFiberを使用すると簡単に乱数の状態が変わってしまう可能性があります。 以上のことからmt_srand()やsrand()を利用した際に再現不可能な乱数が生まれてしまう状況が簡単に起こります。

 

不完全なランダム性

これは1つ上の項でも述べたように、PHPの組み込み関数においてメルセンヌツイスタがデフォルトの乱数生成器としていまだに使われていることが原因です。メルセンヌツイスタでは、暗号的に安全な乱数として生成したい場合に不適切です。そういった要件を満たす乱数を生成したい場合には、開発者側でrandom_int()などの暗号的に安全な乱数を生成する関数を使って新しい関数を実装する必要があります。

 

内部実装の乱雑さ

PHPにおける乱数の実装は、歴史的な理由により標準モジュール内にバラバラに実装されています。 異なるヘッダーファイルであるにもかかわらず相互に依存した実装が行われており、拡張モジュールの開発者にとっては大きな混乱を生む可能性がある状態になっているようです。

この4つの問題点の解消のために、いくつかのインターフェースと、それらを利用したRandom\Randomizer クラスが実装されました。 Random\Randomizer クラスには以下のメソッドが実装されています。(__serializeとunserializeは今回は関係ないので除いています)

__construct(Random\Engine $engine = new Random\Engine\Secure())
getInt(): int // replaces mt_rand()
getInt(int $min, int $max) // replaces mt_rand() and random_int()
getBytes(int length): string // replaces random_bytes()
shuffleArray(array $array): array // replaces shuffle()
shuffleString(string $string): string // replaces str_shuffle()

実際に Random\Randomizer クラスを利用したコードはこんな感じです。

$engine = new Random\Engine\Secure(); //乱数生成に使用するインターフェース
$randomizer = new Random\Randomizer($engine);

$randomizer->getInt(1, 10) // mt_rand()の置き換え
$randomizer->getBytes(10) // random_bytes()の置き換え
$randomizer->shuffleArray([1, 2, 3]) // shuffle()の置き換え
$randomizer->shuffleString('abc') // str_shuffleの置き換え

感想

状態がグローバルなことやメルセンヌツイスタがもう古い!などのPHPの乱数に関する問題は、このRFCを読むまで全く知らなかったのでまず単純に驚きました。 解決法として示された新しいクラスの導入は、シンプルに「これを使えばいいですよ」という話なので助かりますね。 実際案件でも乱数を利用した実装を行っている箇所はありますが、暗号として利用するタイプの実装ではなかったため私個人としての影響はあまり受けなさそうです。 ただ、 Random\Randomizer クラスを利用した実装の方が、よりコードとしてのわかりやすさはあるかな?と感じるので、積極的に活用していこうと思いました。

ランダム拡張の改善

新たなランダム拡張の導入の投票開始後に発覚した問題点の改善や、不足していた機能の追加を行っているRFCです。 問題点については内部実装の改善などがほとんどなので、不足している機能を補う目的で追加された pickArrayKeysメソッド について紹介します。

array_rand()に相当するメソッドがない

新たに追加されたRandomizerにはarray_rand()の代替となるメソッドがありませんでした。これは意図的なものでしたが、さらに調査したところ、array_rand()は多くのパッケージで使用されており、置き換え可能であるべきでした。 そこで Randomizer::pickArrayKeys(array $array, int $num): array が追加されています。 使い方は以下の通りで、 array_rand メソッドと特に変わらない使い方のようです。

$randomizer->pickArrayKeys([1, 2, 3], 1);

感想

array_rand よりもわかりやすい命名にもなっていると感じました。 array_rand だとぱっと見た時にランダムな値が入った配列を作るのかな?と思ったので、 pick という動詞にのおかげでわかりやすくなったと思います。 他にもいくつか変更がありましたが、クラス・メソッドの命名の変更やリファクタリングといった感じでした。

nullとfalseを独立した型として取り扱うようになる

PHP8.1以前では、PHPの組み込み関数で、エラーが発生した場合の戻り値の型としてfalseが使用されている場合がありました。身近な例としてはfile_get_contents関数が挙げられます。 file_get_contens(/* ... */): string | false

しかしPHP8.2ではfalseを独立した型として取り扱うようになるため、falseのみが戻り値の型として定義されている関数を実装することができるようになります。例えば以下のような感じです。

function alwaysFalse(): false
{
return false;
}

nullの例は以下のような感じです。

class Post
{
public function getAuthor(): ?string { /* ... */ }
}

class NullPost extends Post
{
public function getAuthor(): null
{ /*...*/ }
}

感想

false, nullどちらの場合も通常の実装としては使い所があまりピンときていないです。 もしあるとするならば、テストコードでfalseのみやnullのみ返す関数を定義したい場合とか...?

true型の追加

1つ上の項目でも書いたように、nullとfalseを独立した型としてサポートするようになりました。 当初はfalseに比べてtrueを型としてサポートする歴史的理由が存在しないとしており、サポートする予定がなかったようです。 しかし、PHP8.0.0でValueErrorが追加されたため、多くのE_WARNINGがErrors/Exceptionsの取り扱いとなったため、これまで失敗やエラー時にfalseを返していた多くの内部関数が実行に成功した場合は常にtrueを返すようになりました。本来これらはvoidを返すべきですが、破壊的な変更にあたるため簡単には適用できないものになります。また、voidが返り値に設定されたメソッドは常にnullを返しますが、nullはfalseな値です。そのため、結局voidを返すようにする変更を加えてもあまり誰も幸せにならないというのが結論なようです。 RFCに示されていたtrueを返り値の型として利用するコードの例は以下の内容でした。

class User {
function isAdmin(): bool
}

class Admin extends User
{
function isAdmin(): true
{
return true;
}
}

感想

読んでいる途中は、false型、null型と同じくtrue型が必要になった理由はざっくり理解できたけど実際true型ってどのタイミングで使うんだろう...と思っていたので、例を見てなるほどな〜となりました。この例のようにboolだと曖昧さが残ってしまうことを防げるのは良いことだと思うので、こうした使い方ができるタイミングでは必ず使おうと思いました。

MySQLiへのMySQLi_execute_queryメソッドの追加

MySQLiはPHPでMySQLを利用するための拡張モジュールの1つです。 このRFCでは新たにMySQLi_execute_queryメソッドを追加して、MySQLiを使ってパラメータが正しくエスケープされたSELECT文を実行する際の手順を減らす変更について書かれています。 8.2以前のMySQLiではパラメータが正しくエスケープされたSELECT文の実行に以下のような手順が必要でした。

$statement = $db->prepare('SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)');
$statement->bind_param('sii', $name, $type1, $type2);
$statement->execute();
foreach ($statement->get_result() as $row) {
print_r($row);
}

それに対してMySQLi_execute_queryメソッドを使った方法では以下のようになっています。

foreach ($db->execute_query('SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)', [$name, $type1, $type2]) as $row) {
print_r($row);
}

変更内容としては単純にMySQLi_prepare()、MySQLi_bind_param、MySQLi_execute()の3つのメソッドを1つにまとめただけですが、かなり簡潔な書き方になったと思います。 ただし、実行したクエリーは保持されないので、クエリは使い回すがパラメータだけ変えたい場合などは、MySQLi_prepareを使った従来のやり方で行う必要があります。

感想

本文の途中にもありますが、prepare()、bind_param()、execute()の3つの実行をまとめてあることで、bind_param関数で行っているパラメータの型指定なども省略されていて非常に使いやすくなっていますね。私自身はフレームワークを使うことが多く、純粋なPHPでのDB接続を行うことはほぼないので、これらの関数を使う機会はなさそうですが、新たにPHPを学習し始めた初学者の方にはとっつきやすくなっていると思います。

MySQLiからのlibmysqlサポートの削除

このRFCの前提として、現時点ではMySQLi と PDO_mysql の両方が 、mysqlnd および libmysql に対するビルドをサポートしています。PHP 5.4 以降、デフォルトは mysqlnd です。mysqlnd が推奨ドライバーになります。 mysqlnd とlibmysqlの2つのドライバの間には多くの違いがあります。 libmysqlのサポートを削除することで、PHPはコードとユニットテストを簡略することができます。 mysqlndの利点は以下の通りです。

  • PHPにバンドルされている
  • PHPのメモリ管理を使用することでメモリの使用状況を監視しパフォーマンスを向上させることができる
  • 関数による利用のしやすさ(例: get_result)
  • PHPのネイティブ型を使用した数値の返り値
  • オプションのプラグイン機能
  • 非同期クエリ

それに対してlibmysqlの利点は以下の通りです。

  • 自動再接続が可能(この機能は危険なため、mysqlnd では意図的にサポートしていません)
  • LDAP と SASL の認証モードを利用できる(mysqlnd にこの機能を追加するためのPRがいくつか存在している)

しかし、libmysqlには多くの欠点があります。

  • PHPのメモリモデルを使用していない PHPのメモリモデルを使用していないため、メモリ制限の無視・メモリ統計の対象外・内部最適化が利用できないといったデメリットが存在します。また、メモリモデルを使用していないことによりフィールドの長さと同じだけのメモリを確保する必要があり、LONGTEXTのような長いカラムを使用することが非常に困難です。

  • get_result, bind-in-execute, fetch_all & next_result をサポートしていない(PHP8.1より前のバージョンの場合) これらの関数をサポートしていないためlibmysqlは回避策を講じなければならず、結果としてコードがより複雑になりパフォーマンスが低下してしまいます。

  • バージョンによって機能が異なる PHPがどのバージョンのlibmysqlにコンパイルされたかによって、使える機能に差があります。

  • 多くの失敗するテスト テストの中には修正を加えることで成功させることもできますが、非互換性のために失敗するものもあります。

  • メモリリークが発生している これらのメモリリークはセキュリティ上の脆弱性とみなされる可能性があります。

  • Windowsで使用できない 多くのディストリビューションがPHP with mysqlndをデフォルトで提供していますが、新しいユーザーにとって混乱を招くようなものもあります。特に、cPanelを使用している場合、ユーザーはmysqlndでmysqlを有効にする方法について混乱することが多いです。また、MySQLiを有効にする代わりにnd_MySQLi拡張を有効にしなければならず、これは直感的ではありません。限られた機能しか持たない外部ライブラリに対してMySQLiをビルドできるようにすることは、混乱させるだけでなく不必要なことです。

これらの理由からPHP8.2ではlibmysqlに対するMySQLiのビルドサポートを削除します。

感想

業務などではデフォルトのmysqlndしか利用していないのでほぼ関係なさそうです。 やはり言語が指定しているデフォルトのものを利用するのが無難なことが多いんだなと改めて感じました。

readonlyクラスの追加

PHP8.1でreadonlyプロパティが導入されましたが、PHP8.2では全てのクラスプロパティを一度にreadonlyにする構文が追加されています。それぞれ以下のようになります。 readonlyプロパティを使った書き方

class Post
{
public function __construct(
public readonly string $title,
public readonly Author $author,
public readonly string $body,
public readonly DateTime $publishedAt,
) {}
}

readonlyクラスを使った書き方

readonly class Post
{
public function __construct(
public string $title,
public Author $author,
public string $body,
public DateTime $publishedAt,
) {}
}

クラスをreadonlyにすることと全てのプロパティをreadonlyにするのは、基本的には大きな違いはありません。 違いが出てくるのは、動的プロパティをクラスに追加しようとした時です。全てのプロパティをreadonlyにした場合は動的プロパティの追加は可能です。しかし、クラスをreadonlyにした場合は、Uncaught Errorが発生します。

$post = new Post(/* … */);

$post->unknown = 'wrong';

Uncaught Error: Cannot create dynamic property Post::$unknown

また翻訳元の記事には載っていませんが、readonlyなクラスを親クラスとして継承する場合は、子クラスもreadonlyなクラスでなければなりません。

readonly class readonlyHoge
{
}

class notReadonlyHoge extends readonlyHoge
{
}

Fatal error: Non-readonly class notReadonlyPost cannot extend readonly class Post in /var/www/html/index.PHP on line 21

感想

他にも関連性の高い変更があり、動的プロパティを廃止する動きが強いようです。 私個人としては大賛成なので、使っても問題ない場面ではreadonlyクラスを使っていきたいと思います。

動的プロパティの非推奨化

動的プロパティはPHP8.2で非推奨となり、PHP9.0ではErrorExceptionをthrowするようになります。

class Post
{
public string $title;
}

$post = new Post();
$post->name = 'Name';

Deprecated: Creation of dynamic property Post::$name is deprecated in /var/www/html/index.PHP on line 27

ただし、注意しなければいけない点として、__get  __set を実装したクラスはこれまでと同様の動作を行います。 たとえば以下のような例です。

class Post
{
private array $properties = [];

public function __set(string $name, mixed $value): void
{
$this->properties[$name] = $value;
}
}

$post->name = 'Name';

また、動的プロパティを使用してもWarningを表示させたくない場合には、以下のように#[AllowDynamicProperties]を使用することで回避可能です。

#[AllowDynamicProperties]
class Post
{
public string $title;
}

$post = new Post();
$post->name = 'Name';

感想

readonlyクラスの追加と同じく大賛成です。そもそも使うこともなかったですが、今後もこの変更に則って使わないようにしようと思います。

スタックトレース内に表示されるパラメータをマスクする

スタックトレースをサードパーティのチャットツールなどに通知することがよくあるかと思います。その際に環境変数やDB接続情報、パスワード、ユーザー名などの機密情報が含まれている場合が時々あるかと思います。 PHP8.2では、このような「センシティブなパラメータ」を指定して、スタックトレース上で表示されないようにすることができるようになりました。以下のような書き方になります。

function test(
$foo,
#[\SensitiveParameter] $bar,
$baz
) {
throw new Exception('Error');
}

test('foo', 'bar', 'baz');

Fatal error: Uncaught Exception: Error in /var/www/html/index.PHP:34 Stack trace: #0 /var/www/html/index.PHP(37): test('foo', Object(SensitiveParameterValue), 'baz') #1 {main} thrown in /var/www/html/index.PHP on line 34

第2引数にSensitiveParameterを付けることで Object(SensitiveParameterValue) という表示になります。

感想 以前、スタックトレース上にDBのパスワードが表示されるのはセキュリティ上どうなんだ!という話になったことがあり、マスクするのはやろうと思えばできるけどどう書くのがいいんだろう...と思っていたので、明示的に書くことができるこの書き方はかなり好みです。PHP8.2で1から開発することがあったら積極的に使っていきたいと思います。

utf8_encode() および utf8_decode()の非推奨化

PHP 8.2では、utf8_encode() あるいは utf8_decode() のいずれかを使用すると、以下のWarningが表示されます。

「Function utf8_encode() is deprecated」 / 「Function utf8_decode() is deprecated」

また、PHP9.0ではこれらの関数は削除される予定となっています。 RFCに記載されている非推奨化の理由は、「これらの関数は不正確な命名をされており、混乱を引き起こすため」とのことでした。 PHP.netのutf8_encode()のページを確認したところ、「 ISO-8859-1 文字列を UTF-8 に変換する」という説明でした。utf8_decode()の方も「UTF-8 エンコードされた文字列を、ISO-8859-1(Latin 1)に変換し、表現できない文字を置換する」という説明になっており、関数名のわりに対応している文字コードが1つだけという仕様でした。 また、翻訳元の記事では代替としてmb_convert_encoding()を利用するべきと記載されていました。

感想

日本語圏だとなかなかISO-8859-1の文字コード対応を迫られることもなさそうなので、あまり影響はなさそうですね。やはり日本語話者としては、mb_*系の関数をきちんと使うことを意識していく必要があるなと改めて感じました。

strtolower()とstrtoupper()がロケールを考慮しなくなる

PHP8.2からstrtolower()とstrtoupper()がロケールを考慮しなくなるようになります。 ローカライズドされた文字列に対して使用する場合は、mb_strtolower()、mb_strtoupper()の利用を推奨します。

感想

これも日本語圏だとあまり関係ないのかな?という印象です。多言語対応が必要な場合には注意しておかないと文字化けやバグの原因になりそうです。

いくつかの SPL メソッドの引数の変更

SPLクラスのいくつかのメソッドは、正しい型引数を適切に強制するように変更されました。 以下が変更されたメソッドの一覧です。

SplFileInfo::_bad_state_ex()
SplFileObject::getCsvControl()
SplFileObject::fflush()
SplFileObject::ftell()
SplFileObject::fgetc()
SplFileObject::fpassthru()
SplFileObject::hasChildren()
SplFileObject::getChildren()

https://github.com/PHP/PHP-src/blob/master/UPGRADING#L49-L60 にはもう少し具体的な内容が記載されていたのでそちらを参考にします。

- SPL:
. The following methods now enforce their signature:
* SplFileInfo::_bad_state_ex()
* SplFileObject::getCsvControl()
* SplFileObject::fflush()
* SplFileObject::ftell()
* SplFileObject::fgetc()
* SplFileObject::fpassthru()
. SplFileObject::hasChildren() now has a tentative return type of false,
previously it was bool
. SplFileObject::getChildren() now has a tentative return type of null,
previously it was ?RecursiveIterator

. SplFileObject::hasChildren() now has a tentative return type of false, previously it was bool と . SplFileObject::getChildren() now has a tentative return type of null,  previously it was ?RecursiveIterator の部分を訳すとそれぞれ暫定的にfalseとnullに返り値の型を変更されているようです。 そのため、「1. nullとfalseを独立した型として取り扱うようになる」に関連した変更ではあるようです。 他のメソッドについてもfalseやnullが返る場合があるものがいくつかあったので、このあたりの取扱いについて検討しているのかもしれません。

感想

SPLのファイル周りのメソッドは比較的使いそうですし、適用範囲はSPL全体になると思うので、8.2の正式リリース時には適用されるメソッドを確認しようと思いました。

PCRE形式の正規表現でのn修飾子のサポート

新たにPCRE形式におけるn修飾子がサポートされたそうです。 https://github.com/PHP/PHP-src/blob/master/UPGRADING#L97-L102 には詳細について以下のように記載されていました。

- PCRE:
. Added support for the "n" (NO_AUTO_CAPTURE) modifier, which makes simple
`(xyz)` groups non-capturing. Only named groups like `(?<name>xyz)` are
capturing. This only affects which groups are capturing, it is still
possible to use numbered subpattern references, and the matches array will
still contain numbered results.

単純なグループに対してのn修飾子についてはサポート外で、名前付きグループに対してのn修飾子の利用をサポートしているようです。この仕様は、どのグループについてn修飾子でのマッチ回数の上限が適用されるかにのみ影響し、番号づけされたサブパターンでの検索を使用することは可能で、マッチした結果が番号付された結果を含むことに変わりはありません。

感想

私個人としてはもっと単純な正規表現しか書いたことがないので、このn修飾子というものは利用したことがありませんでした。ただ、ヒットさせる回数や名前付きグループ化したもの中でヒットしたものを順に取ってこれるのは痒いところに手が届きそうな気がします。

ODBC接続時にユーザーネームとパスワードがエスケープされる

PHP8.2以降では、ODBC拡張モジュールに接続文字列とユーザー名/パスワードの両方が渡され、ユーザーの入力値などを付加する必要がある場合にユーザー名とパスワードをエスケープするようになりました。 以前は、エスケープが必要な値を含むユーザー値によって、不正な接続文字列が作成されたり、ユーザーが入力したデータから値が注入される可能性がありました。 PDO_ODBC拡張モジュールも、ODBC拡張モジュールの変更と同様に接続文字列が渡された際に、ユーザー名とパスワードをエスケープするようになります。

感想

正直、今までされてなかったの...?と思う変更に見えます。 接続文字列にユーザーの入力を付加するシチュエーションがあまり想像できませんが、エスケープしてなかったと考えるとなかなか恐ろしい気がします。

文字列による変数名指定の一部廃止

PHPでは、文字列に変数を埋め込む方法が4つありますが、この変更ではそのうち2つの方法を非推奨化とします。 この2つの方法はほとんど使用されず、時々混乱を招くことが理由です。 非推奨化される2つの方法は以下の内容になります。

"Hello ${world}";
Deprecated: Using ${} in strings is deprecated


"Hello ${(world)}";
Deprecated: Using ${} (variable variables) in strings is deprecated

今後も利用できる2つの方法は以下の内容になります

"Hello {$world}";
"Hello $world";

感想

確かにパッとみた時に、ん?となるものが非推奨化されていると思うので賛成です。 実務での開発で文字列での変数指定をあまり使ったことがないので、そこまで影響は大きくないかなーと感じています。

部分的にサポートされているcallableの非推奨化

部分的にサポートされる callable とは、 call_user_func($callable) を使用して呼び出すことができる callable のことで、 $callable() を直接呼び出すことはできません。 このようなcallableに当てはまるものはごく一部です。 このRFCでは、以下の構文をPHP8.2で非推奨に変更し、PHP9で非対応とする予定のようです。

"self::method"
"parent::method"
"static::method"
["self", "method"]
["parent", "method"]
["static", "method"]
["Foo", "Bar::method"]
[new Foo, "Bar::method"]

この変更の理由の1つ目は、callableの定義の矛盾を解決することです。 callbleであっても$callable()に渡すことができないものが存在しており、それが上記の構文でした。 この矛盾の解決方法としては、callableのサポートを廃止するか、$callable()の呼び出しをサポートするかの2つがあります。 しかし、上記の構文のうち2つを除いて、どこから呼び出されたかによって参照されるメソッドが変わってしまいます。 そのため、この変更の理由の2つ目はコールバックのコンテキスト依存性を減らすこととなっています。 例に挙げた構文では、最後の2つを除いてコンテキストに依存しています。

このRFCで非推奨とされる構文のほとんどは単純に置き換えることで解決できます。 以下がそれぞれの書き換え例です。

"self::method"       -> self::class . "::method"
"parent::method" -> parent::class . "::method"
"static::method" -> static::class . "::method"
["self", "method"] -> [self::class, "method"]
["parent", "method"] -> [parent::class, "method"]
["static", "method"] -> [static::class, "method"]

この書き換えにより、コールバックが作成された場所ではなく、呼び出す側のself/parent/staticスコープを参照してくれるようになります。 今回の変更により非推奨になる例と、$callableで呼び出せない場合のコードと実行結果は以下のようになります。

class Hoge
{
public function a(): void
{
$callable = 'self::b';
call_user_func($callable);
$callable();
}

public function b()
{
var_dump(__METHOD__);
}
}
Deprecated: Use of "self" in callables is deprecated in /var/www/html/index.PHP on line 5
string(7) "Hoge::b"
Fatal error: Uncaught Error: Class "self" not found in /var/www/html/index.PHP:6 Stack trace: #0 /var/www/html/index.PHP(15): Hoge->a() #1 {main} thrown in /var/www/html/index.PHP on line 6

call_user_funcで呼び出された場合はDeprecatedになり、$callable()で呼び出された場合はFatal errorとなります

感想

callableに関しては、ほとんどの場合array_map()などの関数にコールバックを渡す、みたいな関わり方しかしてきませんでした。そのため、意義はわかるけど業務で書いているコードにはほぼ影響がなさそうだなというのが正直な感想です。他のアップデートも含めて今回のバージョンアップでは、わかりづらさや矛盾点の解消といったところに重きが置かれているのかなと思いました。

部分的にサポートされているcallableのための非推奨の通知の範囲の拡大

部分的にサポートされているcallableの非推奨化のフォローアップとして追加されたRFCとなります。 部分的にサポートされるcallableをis_callable()に渡す際と、callable型で型検証がされる際に非推奨通知を追加することを提案するものです。 上記の2つの特殊な状況は、元のRFCでは明示的に除外されていました。

このRFCでは、PHP8.2で部分的にサポートされているcallableを非推奨とし、PHP9.0でそれらのcallbaleのサポートを削除することを提案しています。call_user_func()やarray_map()のようなcallableを呼び出そうとすると、非推奨の警告が表示されるようになります。 2つの例を用いて非推奨の警告が表示される場合について説明します。

class Foo {
public function bar() {
// Do something conditionally if a child class has implemented a certain method.
if (is_callable('static::methodName')) {
// 有効なコールバックが渡されていた場合この呼び出しはPHP8.xでは実行されます
// しかしPHP9.xではコールバックの内容に関わらずもう実行されません
static::methodName();
}
}
}

(new Foo())->bar();

上記のコードサンプルは、現在のところ PHP 8.x では非推奨の通知は行われませんが、 9.xでは実行されなくなってしまいます。この変更はテストカバレッジが十分でないプロジェクトでは特に危険なものになると考えています。

class Foo {
public function __construct() {
$this->callMe('self::methodName');
}

public function callMe(callable $callback) { // Does not generate a deprecation notice.
call_user_func($callback); // Generates a deprecation notice.
}

public function methodName() {}
}

new Foo();

部分的にサポートされているcallableは、型宣言をした関数内でコールバック関数の呼び出しに使用するのが一般的でしょう。 しかし、必ずしもそうとは限りません。特にフレームワークでは、コールバックを登録しておくとPHP9.0以降では限られた状況下でしか実行できなくなる可能性があります。 非推奨の通知がなければ、それらの「限定された状況でのコールバック」は PHP 9.0 に間に合うように更新する必要があることが発見されないかもしれません。

感想

部分的にサポートされているcallableの非推奨化と同じく、あまり実務的には関わりがなさそうだなと感じました。

Trait内で定数を定義できるようになる

現在のバージョンでは、Trait内にはメソッドとプロパティを定義することは可能ですが、定数を定義することはできません。 そのため、Traitのために必要になる定数を定義したい場合、Traitを利用するインターフェースやクラス側で定数を定義しなければなりません。 そこで、PHP8.2ではTrait内で定数を定義できるように変更されています。 8.1以前と8.2以降の比較は以下の通りです。

# 以前までの場合
trait Hoge {
public function doHoge(int $value): void {
if ($value > self::MAX_VALUE) {
throw new \Exception('out of range');
}
}
}

class Hoge {
private const MAX_VALUE = 42;
use Hoge;
}

# PHP8.2以降の場合
trait Foo {
public const FLAG_1 = 1;
protected const FLAG_2 = 2;
private const FLAG_3 = 2;

public function doFoo(int $flags): void {
if ($flags & self::FLAG_1) {
echo 'Got flag 1';
}
if ($flags & self::FLAG_2) {
echo 'Got flag 2';
}
if ($flags & self::FLAG_3) {
echo 'Got flag 3';
}
}
}

感想

Traitを利用するクラスやインターフェース内に、定数が定義されているより見通しがよくなりそうです。 変更自体を意識してコードを書くというよりは、自然な感覚でコードを書いていれば活用できそうな変更だなと思います。

iterator_*系のメソッドの引数として全てのiterableなオブジェクトを受け入れられるようにする

現在のiterator_*系のメソッドは、Traversablesしか受け付けていません。(arrayを受け付けていない) これは不必要に制限されており、特にiterator_to_arrayメソッドとiterator_countメソッドがその例に当てはまります。 iterator_to_arrayメソッドでの例を以下に示します。

# 8.1以前
function before(iterable $foo) {
if (!is_array($foo)) {
$foo = iterator_to_array($foo);
}

return array_map(strlen(...), $foo);
}

# 8.2以降
function after(iterable $foo) {
$foo = iterator_to_array($foo);

return array_map(strlen(...), $foo);
}

iterator_to_arrayメソッドがiterableなオブジェクトを受け取れるようになったため、is_arrayメソッドでの判定が不要になっています。

感想

arrayを使うことはあってもiteratorはなかなか使うことがないので、変更を実感するのが少し難しそうです。 ただ、iterator_*という命名のメソッドがiteratableなオブジェクトを受け取れないのは、かなり違和感があるのでありがたい変更だなと思いました。

選言標準形が使えるようになる

選言標準形とはなんぞやというところを先に説明します。原文ではDisjunctive Normal Type(以降DNFと表記)と書かれており、論理式を正規化する標準的な方法の1つです。RFCに記載されていた例としては以下のようになります。

interface A {}
interface B {}
interface C extends A {}
interface D {}

class W implements A {}
class X implements B {}
class Y implements A, B {}
class Z extends Y implements C {}

// AとBの両方を実装しているかDを実装しているかの判定式
(A&B) | D

// Cを実装しているかXをしているかつDを実装しているかnullであるかの判定式
C | (X&D) | null

AとBとDを実装しているかintかnullであるかの判定式
(A&B&D)| int | null

また、DNFに則った書式になっていない場合はパースエラーとなります。 以下のような場合です。

// (A&B)|(A&D)と書くのが正しい
A&(B|D)

// A|(B&D)|(B&W)|nullと書くのが正しい
A|(B&(D|W)|null)

各AND/ORセクション内の型の順序は関係ないので、以下の型宣言はすべて等価になります。

(A&B)|(C&D)|(Y&D)|null
(B&A)|null|(D&Y)|(C&D)
null|(C&D)|(B&A)|(Y&D)

すべての型宣言にDNFを要求することで、エンジンにとって容易で、人間や静的解析者にとって理解しやすい標準的な方法で行われます。 また、DNF型ではAND式を囲む括弧は必須です。単一のAND式のみであれば括弧は必要ありません。

 

返り値の共変性について

まずは共変性について説明します。 PHP7.4でサポートされた概念で、子クラスのメソッドが親クラスの戻り値よりも、より特定の狭い型を返すことを許すことです。 次にDNFと共変性の関係について説明します。 DNFによってANDとORが入り混じった型定義の書き方が整理されましたが、共変性の概念に注意しなければなりません。 以下の例をもとに説明すると、ANDの追加や型の範囲がより狭い型との入れ替えによって範囲を狭めることはできますが、範囲が広がってしまうためORの追加はできません。

範囲が狭まっておりOKな例

// A型かつB型、もしくはD型
interface ITest {
public function stuff(): (A&B)|D;
}

// D型は返さない定義に変更
// 狭まったのでOK
class TestOne implements ITest {
public function stuff(): (A&B) {}
}

// A型とB型は返さない定義に変更
// 狭まったのでOK
class TestTwo implements ITest {
public function stuff(): D {}
}

//A型とB型を返さない代わりにC型を返す定義に変更
// OK CはA&Bより狭い
class TestThree implements ITest {
public function stuff(): C|D {}
}

範囲が広がってしまいNGな例


// NG A型であり、B型でないものが通ってしまう
class TestFour implements ITest {
public function stuff(): A|D {}
}

interface ITestTwo {
public function things(): C|D {}
}
// NG A型かつB型であり、C型ではないものが通ってしまう
class TestFive implements ITestTwo {
public function things(): (A&B)|D {}

 

引数の反変性について

こちらもまずは反変性という概念について説明します。 反変性という概念も共変性と同様にPHP7.4からサポートされており、こちらは親クラスのものよりもより抽象的な広い型を引数に指定することを許すものです。 次にDNFと反変性の関係について説明します。 引数についてもDNFによってANDとORが入り混じった型定義が整理されましたが、同じく反変性の概念に注意しなければなりません。 以下の例をもとに説明すると、ORの追加によって型の範囲を広げることができますが、範囲が狭まってしまうためANDを追加することはできません。

// A型かつB型、もしくはD型
interface ITest {
public function stuff((A&B)|D $arg): void {}
}

// Z型まで広がったのでOK
class TestOne implements ITest {
public function stuff((A&B)|D|Z $arg): void {}
}

// A型まで広がったのでOK
class TestOne implements ITest {
public function stuff(A|D $arg): void {}
}

// NG D型が通らなくなった
class TestOne implements ITest {
public function stuff((A&B) $arg): void {}
}

interface ITestTwo {
public function things(C|D $arg): void;
}
// OK A型かつB型であり、C型ではないものまで広がった
class TestFive implements ITestTwo {
public function things((A&B)|D $arg): void;
}

 

プロパティの不変性

次にプロパティの不変性について説明します。 オブジェクトのプロパティの型は継承元と同じである必要があり、このRFCではその仕様に変更は加えません。 ただし、論理的に同一なものであれば順序の入れ替わりは許容します。

 

重複した型と冗長な型

型宣言での簡単なバグを捕捉するために、クラス読み込みをせずに検出できる冗長な型はコンパイル時エラーとなります。 この仕組みは既にIntersection型とUnion型に適用されているロジックと同様です。 DNF型の各セグメントは一意である必要があります。例えば、(A&B)|(B&A)の型宣言のような、OR化された2つのセグメントが論理的に等価であるものは冗長と判断され無効な式となります。 他のセグメントの部分集合であるセグメントは許可されません。 例えば、(A&B)|Aという型定義は冗長です。なぜなら、Aの全てのインスタンスはBも実装しているかどうかにかかわらず、既に許可されています。そのため、無効な判定式となります。 ただし、この仕組みはその型が「最小」であることを保証するものではありません。なぜなら、その保証のためにはすべてのクラス型を読み込む必要があるからです。

まとめ

PHP8.2のいくつかの変更点について、RFCを翻訳して紹介させていただきました。 今回初めてRFCの内容を読みましたが、今後のPHPの目指す方向性などがなんとなく見えてとても面白かったです。 また、今まで知らなかった概念や単語などが出てきてそういった面でも勉強になったと思います。 みなさんも一度こういった英語の公式ドキュメント・規格などを翻訳してみてはいかがでしょうか!