PHP 8.2は、PHP 8.0とPHP 8.1で確立された最新の基盤の上に構築された新たなメジャーアップデートです。この記事では、PHP 8.2のリリースに伴い、PHP 8.2の新機能や改善点、非推奨機能、変更点などを詳しくご紹介していきます。
※この記事は、PHP 8.2のリリース前に執筆されていますが、2022年7月19日に内容が凍結されており、ご紹介する内容に違いはほとんどありません。
それでは、早速本題に入りましょう!
PHP 8.2の新機能と改善点
まずは、PHP 8.2の最新の機能をすべてご紹介します。以下の通り、かなり充実しています。
readonly
クラス
PHP 8.1では、クラスのプロパティにreadonly
(読み取り専用)機能が導入されましたが、PHP 8.2では、クラス全体をreadonly
で宣言することができます。
クラスをreadonly
で宣言した場合、そのすべてのプロパティが自動的にreadonly
機能を継承します。したがって、あるクラスをreadonly
で宣言すると、そのクラスのすべてのプロパティがreadonly
で宣言されることになります。
例えば、PHP 8.1では、すべてのクラスのプロパティをreadonly
として宣言するために、以下のような手間のかかるコードを記述する必要がありました。
class MyClass
{
public readonly string $myValue,
public readonly int $myOtherValue
public readonly string $myAnotherValue
public readonly int $myYetAnotherValue
}
さらにプロパティが増える場合を想像すると気が遠くなります。しかし、PHP 8.2では、以下のように記述するだけでOKです。
readonly class MyClass
{
public string $myValue,
public int $myOtherValue
public string $myAnotherValue
public int $myYetAnotherValue
}
抽象クラスや最終クラスをreadonly
で宣言することも。 キーワードの順番を意識する必要はありません。
abstract readonly class Free {}
final readonly class Dom {}
また、プロパティを指定せずにreadonly
クラスを宣言することも可能です。この場合、依然として子クラスは明示的にreadonly
のプロパティを宣言できますが、実質、動的なプロパティが許可されなくなります。
readonly
クラスは型付きプロパティのみ持つことができます。これは、個々の読み取り専用プロパティを宣言する場合と同じルールです。
厳密に型付けされたプロパティを宣言できない場合は、mixed
型を使用してください。
型付けされたプロパティを持たないreadonly
クラスを宣言すると、「Fatal error」が生じます。
readonly class Type {
public $nope;
}
Fatal error: Readonly property Type::$nope must have type in ... on line ...
また、以下のPHPの機能に対してもreadonly
を宣言できません。
- 列挙型(プロパティが含まれないため)
- トレイト
- インターフェース
上記機能をreadonly
で宣言すると、同様に「Parse error」が生じます。
readonly interface Destiny {}
Parse error: syntax error, unexpected token "interface", expecting "abstract" or "final" or "readonly" or "class" in ... on line ...
すべてのキーワードと同様、readonly
は大文字と小文字を区別しません。
PHP 8.2では、動的なプロパティも非推奨です(後ほど詳しくご説明します)。動的なプロパティの定義を禁止することはできませんが、readonly
クラスで定義すると、「Fatal error」が起こるようになっています。
Fatal error: Readonly property Test::$test must have type in ... on line ...
true
/false
/null
が独立した型で使用可能に
PHP には、元々int
、string
、bool
のようなスカラー型がありましたが、PHP 8.0でさらにunion型が追加され、異なる型の値を受け入れられるようになりました。同じRFCでは、false
とnull
をunion型の一部として使用することができましたが、独立した型(スタンドアロン型)としては認められていませんでした。
false
やnull
をunion型の一部としてではなく、独立した型として宣言すると、「Fatal error」が生じます。
function spam(): null {}
function eggs(): false {}
Fatal error: Null can not be used as a standalone type in ... on line ...
Fatal error: False can not be used as a standalone type in ... on line ...
これを回避するため、PHP 8.2ではfalse
とnull
を独立した型として使用できるようになっています。PHP型システムの表現の幅が広がり、完成度が上がりました。また、戻り値、パラメータ、プロパティも明示的に宣言できるようになります。
加えて、これまでなかったfalse
型と対をなすtrue
もサポートされています。false
の動作とまったく同じで、強制不可です。PHP 8.2ではそれが修正され、true
型のサポートが追加されました。しかし、false
型の挙動と同様に強制はできません。
true
とfalse
は、本質的にPHPのbool
のunion型。冗長性を排除するため、この3つを一緒にunion型で宣言することはできません。宣言しようとすると、コンパイル時に「Fatal error」が発生します。
DNF型
DNF型は、論理式を正規化する標準的手法。連言節(AND)の選言(OR)の形式で構成され、ブーリアン演算用語でいうところの選言標準形に相当します。
DNFで型宣言を行うことで、パーサが扱うことのできるunion型とIntersection型を組み合わせることができます。PHP 8.2で新たに登場したDNF型は、適切に使用すればシンプルかつ強力です。
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 {}
DNF型は、プロパティ、パラメータ、戻り値に対して、以下のような型宣言を行うことができます。
// Accepts an object that implements both A and B,
// OR an object that implements D
(A&B)|D
// Accepts an object that implements C,
// OR a child of X that also implements D,
// OR null
C|(X&D)|null
// Accepts an object that implements all three of A, B, and D,
// OR an int,
// OR null.
(A&B&D)|int|null
プロパティがDNF型でない場合、パースエラーが起こります。その際には、以下のように書き換えます。
A&(B|D)
// Can be rewritten as (A&B)|(A&D)
A|(B&(D|W)|null)
// Can be rewritten as A|(B&D)|(B&W)|null
なお、DNF型の各セグメントは一意でなければいけません。例えば、(A&B)|(B&A)
と宣言しても、OR式で結合された2つのセグメントは論理上同じものであるため、無効になります。
また、他のセグメントの部分集合であるセグメントも許可されません。これは、上位集合がすでにサブセットすべてのインスタンスを持っており、DNFを使用すると冗長であるためです。
バックトレースで機密性の高いパラメータの出力を制御
多くのプログラミング言語同様、PHPではコードの実行中の任意の時点でスタックトレースを表示・記録することができます。これによって、エラーやパフォーマンスの問題を修正するためのコードのデバッグが容易になります。余談ですが、WordPressサイト向けにKinstaが開発したパフォーマンス監視ツールである「Kinsta APM」のバックボーンでもあります。
スタックトレースを実行しても、プログラムが途中で停止することはありません。通常、スタックトレースはバックグラウンドで実行され、必要に応じて後で検証できるよう、ログが記録されます。
しかし、PHPの詳細なスタックトレースには、ユーザー名やパスワード、環境変数などの機密情報が含まれている可能性があります。そのため、サードパーティのサービス(エラーログの解析やエラーの追跡など)と共有する際には注意が必要です。
RFCの提案には、以下のようにあります。
「厄介な問題」を引き起こすので知られるのが、 PDO(PHP Data Objects)です。PDOは、純粋なコンストラクタと個別の ->connect()関数を持たずに、コンストラクタのパラメータとしてデータベースのパスワードを受け取り、コンストラクタ内ですぐにデータベースへの接続を試みます。そのため、データベース接続に失敗した場合、スタックトレースには、データベースのパスワードが含まれてしまいます。(英語原文の日本語訳)
PDOException: SQLSTATE[HY000] [2002] No such file or directory in /var/www/html/test.php:3 Stack trace: #0 /var/www/html/test.php(3): PDO->__construct('mysql:host=loca...', 'root', 'password') #1 {main}
PHP 8.2では、パスワードのような機密性の高いパラメータをSensitiveParameter
属性でマークすることができるようになりました。機密性が高いと定義されたパラメータは、バックトレースに表示されず、外部サービスに安心して共有することができます。
例として、1つのパラメータを機密性が高いものとしてマークしてみます。
<?php
function example(
$ham,
#[SensitiveParameter] $eggs,
$butter
) {
throw new Exception('Error');
}
example('ham', 'eggs', 'butter');
/*
Fatal error: Uncaught Exception: Error in test.php:8
Stack trace:
#0 test.php(11): test('ham', Object(SensitiveParameterValue), 'butter')
#1 {main}
thrown in test.php on line 8
*/
バックトレースを生成する際、SensitiveParameter
属性のパラメータは、SensitiveParameterValue
オブジェクトに置き換えられ、実際の値が保存されることはありません。何らかの理由で実際の値が必要になった方向けの情報として、SensitiveParameterValue
オブジェクトによって値がカプセル化されます。
mysqli_execute_query
関数とmysqli::execute_query
関数
パラメータ化したMySQLiクエリのために、過剰にエスケープ処理を施したユーザー値でmysqli_query()
関数を実行した経験はありませんか?
PHP 8.2では、mysqli_execute_query($sql, $params)
関数とmysqli::execute_query
関数でパラメータ化したSQL文の実行がより簡単に行えるようになっています。
この関数は、mysqli_prepare()
、mysqli_execute()
、mysqli_stmt_get_result()
関数を組み合わせたもの。SQL文の準備、バインド(パラメータを渡した場合)、実行がこの関数内で行われます。クエリが正常に実行されると、mysqli_result
オブジェクトが返され、失敗した場合はfalse
を返します。
RFCでは、以下のようなシンプルかつ便利な例が提示されています。
foreach ($db->execute_query('SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)', [$name, $type1, $type2]) as $row) {
print_r($row);
}
定数式(const
)でenum
プロパティを取得
こちらのRFCでは、定数式(const
)でのenum
プロパティ取得のため、->/?->
演算子の許可が提示されています。
この機能が導入された主な理由は、配列のキーなどで、enum
オブジェクトが使用できなかったことにあります。enum
の値を繰り返し使用する必要がありました。
enum
オブジェクトが使えない部分でenum
プロパティの取得を許可することで、この手順が簡略化できます。
つまり、以下のようなコードが有効に。
const C = [self::B->value => self::B];
また、このRFCでは、nullsafe 演算子?->
もサポートされています。
トレイトでの定数使用を許可
PHPには、トレイトと呼ばれるコードを再利用する仕組みがあり、クラス間でのコードの再利用に有用です。
8.2以前は、関数とプロパティのみ定義可能で、定数は定義することができませんでした。つまり、あるトレイトが、期待する不変量をそのトレイト自体で定義することはできないことを意味します。これを回避するには、トレイトの構成クラスか、その構成クラスが実装するインターフェースで定数を定義しなければなりません。
このRFCでトレイトでの定数の使用を許可することが提案されました。クラス定数を定義するのと同じように定義することができます。以下、わかりやすいRFCの例を引用します。
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';
}
}
}
トレイトの定数は、トレイトのプロパティやメソッドの定義と同じように、構成するクラスの定義にマージされ、そこにはプロパティと同様の制約があります。また、出発点としては興味深いものですが、RFCでも指摘されているように、この機能を具体化するにはさらなる改良が必要と言えます。
非推奨になる機能
続いて、PHP 8.2の非推奨事項も見ていきましょう。これについては、新機能と比較してそれほど多くはありません。
動的プロパティの禁止(代替の#[AllowDynamicProperties]
属性)
PHP 8.1までは、以下のように、宣言されていないクラスのプロパティを動的に設定、取り出すことができました。
class Post {
private int $pid;
}
$post = new Post();
$post->name = 'Kinsta';
上記では、Post
クラスでname
プロパティを宣言していません。しかし、動的なプロパティが許可されているため、クラス宣言外で設定することができます。これは、PHP最大(そして唯一)のメリットと言えるかもしれません。
動的なプロパティは、コードに予期せぬバグやおかしな挙動を引き起こす可能性があります。例えば、クラスのプロパティをクラス外で宣言する際、何かしらのミスがあると、そのプロパティが見落とされがちです(特にそのクラス内のエラーをデバッグする時)。
PHP 8.2以降は、動的なプロパティは非推奨になります。宣言されていないクラスプロパティに値を設定すると、 最初に設定された時点で非推奨の通知が表示されます。
class Foo {}
$foo = new Foo;
// Deprecated: Creation of dynamic property Foo::$bar is deprecated
$foo->bar = 1;
// No deprecation warning: Dynamic property already exists.
$foo->bar = 2;
PHP 9.0以降は、同じ設定を行うとErrorException
エラーが発生するようになります。
コードに動的なプロパティが多く含まれていて、PHP 8.2への切り替え後、この非推奨の通知を停止したい場合には、PHP 8.2の新しい属性#[AllowDynamicProperties]
を使用して、クラスの動的なプロパティを許可すればOKです。
#[AllowDynamicProperties]
class Pets {}
class Cats extends Pets {}
// You'll get no deprecation warning
$obj = new Pets;
$obj->test = 1;
// You'll get no deprecation warning for child classes
$obj = new Cats;
$obj->test = 1;
RFCによると、#[AllowDynamicProperties]
属性の付されたクラスとその子クラスでは、非推奨化や削除を心配することなく、動的プロパティを使い続けることができます。
ただし、PHP 8.2では、#[AllowDynamicProperties]
に分類されるビルトインクラス(定義済みの空のクラス)は、stdClass
だけであることにご注意ください。また、__get()
や__set()
のマジックメソッドでアクセスしたプロパティは、動的プロパティと見なされず、非推奨の通知は表示されません。
部分的にサポートされているcallable
大きな影響はありませんが、もうひとつの変更点として、部分的にサポートされているcallableが非推奨となることが挙げられます。
callableは$callable()
から直接操作することはできず、call_user_func($callable)
関数を使用して呼び出します。対象となるのは、以下の通り。
"self::method"
"parent::method"
"static::method"
["self", "method"]
["parent", "method"]
["static", "method"]
["Foo", "Bar::method"]
[new Foo, "Bar::method"]
PHP 8.2以降は、上記のようなcallableをcall_user_func()
やarray_map()
関数で呼び出すと、非推奨の警告が表示されます。
元のRFCでは、非推奨化の理由を以下の様に明示しています。
最後の2つを除いて、callableはすべてコンテキストに依存します。
"self::method"
が参照する関数は、呼び出しや呼び出し可能性チェックがどのクラスで実行されるかによります。実際、[new Foo, "parent::method"]
の形で使われる場合は、基本的に最後の2つも当てはまります。callableのコンテキスト依存性を低減するのがこのRFCの第二の目標です。この目標の実現後、残る変数のスコープの依存性は、関数の可視性のみ。
"Foo::bar"
は、あるスコープでは可視ですが、別のスコープでは不可視に。将来的にcallableがpublic関数に限定されるとすれば(public関数は第一級callableかClosure::fromCallable()
で、コンテキストに非依存である必要がある)、callableが明示的に定義され、プロパティとして使用できるようになるでしょう。しかし、このRFCで可視性処理の変更を提案しているわけではありません。(英語原文の日本語訳)
元々のRFCに従い、is_callable()
関数とcallable
タイプは、上記callableを除いて、引き続き使用できます。ただし、PHP 9.0以降でサポートが完全になくなるまでの間に限定されます。
混乱を避けるため、この非推奨通知の範囲は新しいRFCで拡大され、例外になっていたものも含まれるようになりました。
PHPが、明確に定義されたcallable
型のサポートに向かっているのは良い展開です。
#utf8_encode()
およびutf8_decode()
関数
PHPの組み込み関数であるutf8_encode()
およびutf8_decode()
は、 ISO-8859-1(Latin 1)でエンコードされた文字列をUTF-8に(または反対方向に)変換します。
ところが、この関数の名前は、実際の実装範囲を超え、一般的な用途を示唆するものになっています。Latin 1エンコーディングは、Windows Code Page 1252のような他のエンコーディングと混同されがちです。
さらに、この関数が文字列を正しく変換できなければ、文字化けが発生します。エラーメッセージなどの通知がないため、特に判読可能なテキストの中でこのエラーメッセージを見つけるのは至難の業。
PHP 8.2では、#utf8_encode()
とutf8_decode()
はどちらも非推奨になり、使用すると非推奨の通知が表示されます。
Deprecated: Function utf8_encode() is deprecated
Deprecated: Function utf8_decode() is deprecated
RFCでは、代替としてmbstring
、iconv
、intl
のようなPHPがサポートする拡張モジュールの使用が提案されています。
${}
文字列補間
PHPでは、ダブルクォーテーション("
)やヒアドキュメント(<<<
)による文字列への変数の埋め込みが可能です。
- 変数を直接埋め込む─
“$foo”
- 変数の外側に中括弧を付ける─
“{$foo}”
- ドル記号の後に中括弧を付ける─
“${foo}”
- 変数─
“${expr}”
((string) ${expr}
と同等)
最初の2つの方法には、メリットとデメリットがあり、最後の2つは構文が複雑かつ矛盾しています。PHP 8.2では、最後の2つの文字列補間は非推奨になっています。
今後は、この方法を選ばないようにしましょう。
"Hello, ${world}!";
Deprecated: Using ${} in strings is deprecated
"Hello, ${(world)}!";
Deprecated: Using ${} (variable variables) in strings is deprecated
PHP 9.0以降は、例外エラーを投げるように改善される予定です。
Base64/QPrint/Uuencode/HTML-ENTITIES(mbstring関数)
PHPのmbstring(マルチバイト文字)関数は、UnicodeやHTML-エンティティ、その他のレガシーのテキストエンコーディングに有用です。
しかし、主にその「レガシー化」が原因で、文字エンコーディングではないBase64、Uuencode、QPrintが関数の一部として残っていました(エンコーディングの個別の実装を含む)。
HTMLエンティティに関しては、組み込み関数(htmlspecialchars()
とhtmlentities()
)でうまく処理することができます。例えば、mbstringとは異なり、この組み込み関数は<
>
、&
の文字をHTMLエンティティに変換します。
また、PHP 8.1のHTMLエンコード/デコード関数のように、常に組み込み関数は改良されています。
したがって、PHP 8.2では、次のエンコーディングへのmbstringの使用が非推奨になっています(ラベルは大文字/小文字を区別)。
- BASE64
- UUENCODE
- HTMLエンティティ
- html(HTMLエンティティのエイリアス)
- Quoted-Printable
- qprint(Quoted-Printableのエイリアス)
PHP 8.2以降、mbstringを使用して上記のエンコード/デコードを行うと、非推奨の通知が表示されます。PHP 9.0では、サポートが完全に終了する予定です。
その他の細かな変更点
最後に、PHP 8.2の細かな変更点もご紹介します。
mysqliからlibmysqlのサポートを終了
PHP 8.2の前の段階で、mysqli
とPDO_mysql
の両方のドライバがmysqlnd
およびlibmysql
ライブラリに対してビルドできるようになっています。しかし、元々PHP 5.4以降のデフォルトおよび推奨ドライバはmysqlnd
です。
それぞれメリットとデメリットは多数ありますが、どちらかのサポートを終了(理想的にはlibmysql
を廃止)することで、コード、単体テストの簡素化が期待できます。
これについて、RFCではmysqlnd
のメリットが数多く挙げられています。
- PHPにバンドルされている
- PHPのメモリ管理を利用してメモリの使用状況を監視することで、パフォーマンスが向上
- 作業を効率化する関数がある(例:
get_result()
) - PHPのネイティブ型を使用した数値の返り値
- 外部ライブラリを必要としない機能性
- 任意でのプラグイン機能の利用
- 非同期クエリをサポート
また、libmysql
のメリットもいくつか挙がっています。
- 自動再接続が可能(
mysqlnd
は、簡単に悪用できてしまう恐れから意図的にこれをサポートしていない) - LDAPおよびSASL認証モード(
mysqlnd
にもこの機能が導入される可能性あり)
libmysql
については、PHPのメモリモデルとの非互換性、度重なるテストの失敗、 メモリリーク、バージョン間での機能の差など、数々のデメリットも挙がっています。
上記を考慮し、PHP 8.2ではmysqli
をlibmysql
にビルドする機能が非推奨になっています。
libmysql
の機能を利用したい場合は、機能の要求としてmysqlnd
に明示的に追加する必要があります。なお、自動再接続の機能は利用できません。
ロケールに依存しない大文字/小文字の変換
PHP 8.0より前のバージョンでは、PHPのロケールはシステム環境から継承されていましたが、一部エッジケースで問題を引き起こす懸念がありました。
Linuxのインストール時に言語を設定すると、その組み込みコマンドに適切なユーザーインターフェース言語が設定されます。しかし、これはCライブラリの文字列処理機能の動作方法が変更されてしまうことを意味します。
例えば、Linuxのインストール時に、トルコ語やカザフ語を選択した場合、大文字を取得するためにtoupper('i')
を呼び出すと、ドット付きの大文字「I」(U+0130、İ
)が取得されてしまいます。
PHP 8.0では、ユーザーがsetlocale()
で明示的に変更を加えない限り、デフォルトのロケールを「C」に設定し、この問題を回避しています。
PHP 8.2では、さらに大文字と小文字の変換時にロケールの区別をしないようになっています。このRFCでは、主にstrtolower()
、strtoupper()
および関連する関数を変更しています。影響を受ける関数の一覧は、RFCを参照してください。
国や地域の言語を鑑みた大文字と小文字の変換が必要な場合には、mb_strtolower()
を使って実行可能です。
乱数の改良
現在、PHPの乱数を生成する機能の見直しが計画されています。
この機能は、現状メルセンヌ・ツイスタ(乱数生成器)に大きく依存しています。これは、PHPのグローバル領域に保存されており、ユーザーがアクセスすることはできません。最初のシード生成から意図した利用までの間に乱数生成機能を導入すると、コードが壊れてしまいます。
コードが外部パッケージを使用している場合、このようなコードの保守はさらに難易度が上がります。
したがって、PHPの現在の乱数生成機能では、ランダムな値を一貫して再現できていません。TestU01のCrushやBigCrush のような、一様乱数生成器の経験的な統計テストさえ失敗しています。さらに、32ビットというメルセンヌ・ツイスタの制限が状況を悪化させることに。
暗号面で安全な乱数の生成が必要になる場合、PHPの組み込み関数、shuffle()
、str_shuffle()
、array_rand()
の使用は推奨されません。random_int()
や類似の関数を使った、新たな関数の実装が必要です。
しかし、RFCの投票が開始された後、このRFCに関していくつかの懸念点が指摘されたため、すべての問題を別のRFCに記録して、各問題に対して投票が行われることになりました。今後、合意形成の上で進められる予定です。
その他のRFC
PHP 8.2には、多くの新機能や細かな変更点があります。以下、上記でご紹介していないその他の改善点です。
curl_upkeep
関数の導入─Curl拡張モジュールに導入。基本的なCライブラリであるlibcurl内のcurl_easy_upkeep()
関数を呼び出すものです。ini_parse_quantity
関数の導入─php.iniディレクティブでは、データサイズに乗算の接尾辞をつけることができます(25メガバイトを25M
、42ギガバイトを42G
と記述するなど)。この接尾辞は、php.iniファイルでは一般的ですが、他ではあまり見られません。この関数は、php.iniの値をパースし、そのデータサイズをバイト単位で返します。memory_reset_peak_usage
関数の導入─memory_get_peak_usage
関数によって返されるピークメモリ使用量をリセットする関数。同じ処理を複数回実行し、それぞれの実行のピークメモリ使用量を記録するのに有用になります。-
preg_*
関数で修飾子/n
をサポート─正規表現では、メタ文字()
はグループ化を示し、括弧内の式に合致したものがすべて返されます。PHP 8.2 では、この挙動を回避する修飾子(/n
)がサポートされています。 iterator_*()
ですべての反復をサポート─PHP 8.2以前では、iterator_*()
パターンは、Traversables
しかサポートしていませんでした。これが不必要な制限であると判断され、全ての反復がサポートされるように改善されています。
まとめ
PHP 8.2は、PHP 8.0とPHP 8.1で行われた大規模な改良を土台としてリリースされたメジャーアップデートです。特に魅力的なのは、独立した型、読み取り専用プロパティ、そして多くのパフォーマンス面での改善でしょう。
KinstaでもPHP 8.2のベンチマークを様々なPHPフレームワークやCMSで行えることを楽しみにしています。
PHP 8.2のリリースにあたって、この記事がお役に立てたら幸いです。
PHP 8.2の一押し機能はありますか?また、どの非推奨機能に賛成ですか?以下のコメント欄でぜひお聞かせください。
コメントを残す