PHP8.2 / PHP8.1 / PHP8.0 / PHP7.4
2021/11/26にリリースされました
2021/07/20、PHP8.1がフィーチャーフリーズしました。
言語機能に関わるような機能の追加・変更が締め切られたということです。
今後はデバッグを繰り返しながら完成度を高めていき、2021/11/25にPHP8.1.0がリリースされる予定です。
というわけでPHP8.1で実装されるRFCを見てみましょう。
RFC
Fibers
賛成50反対14で受理。
Fiberです。
PHPで非同期コードを書けるようになります。
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('fiber');
echo "レジュームした。$value: ", $value, "\n";
});
$value = $fiber->start();
echo "一時停止した。$value: ", $value, "\n";
$fiber->resume('test');
// 実行結果
一時停止した。$value: fiber
レジュームした。$value: test
イベントループにPromiseといった、これまでのPHPでは困難だった処理がついに書けるようになります。
基本的に一般ユーザ向けの機能ではないので、ライブラリが対応するのを待つ必要がありますが、ノンブロッキングなコードをPHPでも書けるようになりそうですね。
Pure intersection types
賛成30反対3で受理。
交差型です。
PHP8.0で追加されたUNION型に引き続き、交差型も使用可能になります。
class A {
// UNION型 $aはTraversableもしくはCountableである
private Traversable|Countable $a;
// 交差型 $bはTraversableかつCountableである
private Traversable&Countable $b;
}
これで好きなだけ型パズルが作れるようになりますね。
Enumerations
賛成44反対7で受理。
列挙型です。
enum Suit {
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
$val = Suit::Diamonds;
まずはENUMが実装されましたが、元々のRFCは最終的に代数的データ型の実装を目指す巨大な計画です。
ENUMはクラスを下敷きに作られているため、個別にメソッドを実装したりとかなり複雑な作り込みもできるようになっています。
個人的にはENUMがなくてどうにもならないという事態に遭遇したことがないのでどのくらい有用かはよくわかりませんが、古来よりENUMをPHPに実装する試みが何度も行われているくらいなので、きっと需要も大きいのでしょう。
noreturn type
賛成42反対11で受理。
never型です。
function foo():never{
exit;
}
途中でexitしたり常に例外を出すような、呼び出し元に返らない関数において返り値として書ける型never
が追加されます。
Readonly properties 2.0
class Test {
public readonly string $prop;
public function __construct(string $prop) {
$this->prop = $prop;
}
}
$test = new Test("foobar");
// 読み込みはOK
var_dump($test->prop); // string(6) "foobar"
// 書き込みはNG
$test->prop = "foobar"; // Error: Cannot modify readonly property Test::$prop
一度だけ初期化が可能で、その後変更されないことを保証するプロパティです。
protected
やprivate
を使う理由の98%は『読まれてもいいけど書き込ませたくない』なので、そのユースケースを万事解決してくれます。
ちなみに上のクラスTest
は、今ではここまで省略して書くことが可能です。
class Test {
public function __construct(public readonly string $prop) {}
}
Deprecate implicit non-integer-compatible float to int conversions
賛成29反対0で受理。
float
からint
への暗黙の型変換にE_DEPRECATEDが発生するようになります。
int
からfloat
、float
からstring
といった変換は、基本的に情報の損失が発生しません。
しかしfloat
からint
への変換はそうではありません。
function toInt(int ...$arg){
var_dump($arg);
}
function toFloat(float ...$arg){
var_dump($arg);
}
function toString(string ...$arg){
var_dump($arg);
}
toInt(1, '1', 1.5, '1.5'); // [1, 1, 1, 1]
toFloat(1, '1', 1.5, '1.5'); // [1, 1, 1.5, 1.5]
toString(1, '1', 1.5, '1.5'); // ['1', '1', '1.5', '1.5']
int
に変換したところだけ情報が抜け落ちてしまっています。
そのため、この暗黙的な変換については今後警告を出すようにします。
明示的な変換(int)1.5
やintval(1.5)
については、これまでどおり問題なく変換可能です。
もっとも、現在でもdeclare(strict_types=1)
を付けていればこの変換はTypeErrorになるので、strictな書き方をしている限りは何の影響もありません。
Final class constants
賛成29反対4で受理。
finalクラス定数です。
class FOO{
final public const HOGE = 1;
}
継承したクラスで上書き変更できないことが保証される定数を書けるようになります。
これによって、クラス、メソッド、定数にfinalを書けるようになりました。
プロパティには書けませんが、上記のreadonlyがちょうど代替になるでしょう。
New in initializers
賛成43反対2で受理。
引数デフォルト値でnewできるようになります。
class Test {
// PHP8.0まで
private Logger $logger;
public function __construct(
?Logger $logger = null,
){
$this->logger = $logger ?? new NullLogger;
}
// PHP8.1以降
public function __construct(
private Logger $logger = new NullLogger,
){}
}
これまで引数デフォルト値にはスカラ値しか置けなかったので、一度nullで受けてからメソッド内で分岐するという不自然な書き方をしないといけなかったのですが、これが自然に書けるようになります。
Array unpacking with string keys
賛成50反対0で受理。
配列アンパックの文字列キー対応です。
PHP7.4でアンパックによる配列のマージが実装されました。
$array1 = [10=>1, 2];
$array2 = [10=>3, 4];
$array = [...$array1, ...$array2]; // [1, 2, 3, 4]
array_mergeや配列結合とは違い、単純に全要素を順にくっつけるだけというもので、キーも0から振りなおされます。
どうせキーは振りなおされるのに、何故か文字列キーには対応していませんでした。
$array1 = ['a'=>1, 2];
$array2 = ['a'=>3, 4];
$array = [...$array1, ...$array2]; // Fatal Error: Cannot unpack array with string keys
これを改善し、文字列キーでも配列アンパックできるようにします。
ただ何故か文字列キーについてはキーが振りなおされず、同じキーは後勝ちの上書きになるようです。
$array1 = ['a'=>1, 2];
$array2 = ['a'=>3, 4];
$array = [...$array1, ...$array2]; // ['a'=>3, 2, 4]
[1, 2, 3, 4]
ではないみたいです。
よくわかりませんね。
Deprecate passing null to non-nullable arguments of internal functions
賛成46反対0で受理。
nullを許容しない内部関数への引数nullをE_DEPRECATEDにします。
たとえばstrlenのシグネチャはstrlen(string $string): int
であり、引数はnull許容型ではありません。
しかし、普通にnullを渡せるしエラーも出ません。
strlen(null); // エラー出ない
function my_strlen(string $string): int{
return strlen($string);
}
my_strlen(null); // TypeError Argument 1 passed to my_strlen() must be of the type string, null given
ユーザ定義関数だときちんとTypeErrorになります。
しかし内部関数は、歴史的理由もあって引数に甘い判定をしがちです。
この矛盾を解消するためのRFCです。
ただ、いきなりTypeErrorにすると大混乱なので、PHP8.1ではE_DEPRECATEDとし、PHP9かそれ以降でユーザ定義関数と同じくTypeErrorにします。
Add return type declarations for internal methods
賛成17反対7で受理。
内部クラスをextendsした際に、返り値の型宣言をチェックするようにします。
class MyDateTime extends DateTime
{
public function modify(string $modifier):int
{
return 1;
}
}
$dt = new MyDateTime();
$dt->modify('modifier');
DateTime::modifyの返り値はDateTime|false
ですが、extendsした際に全く無視してint
とか付けても普通に動きますしエラーも出ません。
これを互換しない型についてはE_DEPRECATEDを出すようにします。
PHP9ではTypeErrorになります。
さきほどのRFC同様、組み込み関数は甘めの判定になっているところが多いです。
ユーザ定義クラスをextendsした場合は、このコードは当然TypeErrorになります。
First-class callable syntax
賛成44反対0で受理。
Closure::fromCallableの新文法です。
// 同じ
$fn = Closure::fromCallable('strlen');
$fn = strlen(...);
// 同じ
$fn = Closure::fromCallable([$this, 'method']);
$fn = $this->method(...)
// 同じ
$fn = Closure::fromCallable([Foo::class, 'method']);
$fn = Foo::method(...);
...
は省略記号とかではなく、このままの文字です。
まあ、ただの糖衣構文です。
これで何がうれしいのかというと、文字列で渡さないので静的解析が可能になるというところです。
しかしこの構文がいきなり出てきたら、アンパックや普通の関数呼び出しと見間違って混乱する自信がある。
Add IntlDatePatternGenerator
賛成10反対0で受理。
ローカライズされた日付フォーマッタを導入します。
$skeleton = "YYYYMMdd";
$today = \DateTimeImmutable::createFromFormat('Y-m-d', '2021-04-24');
$dtpg = new \IntlDatePatternGenerator("de_DE");
$pattern = $dtpg->getBestPattern($skeleton);
echo "de: ", \IntlDateFormatter::formatObject($today, $pattern, "de_DE"), "\n"; // de: 24.04.2021
$dtpg = new \IntlDatePatternGenerator("en_US");
$pattern = $dtpg->getBestPattern($skeleton), "\n";
echo "en: ", \IntlDateFormatter::formatObject($today, $pattern, "en_US"), "\n"; // en: 04/24/2021
これは…どうなんだ?
まあPHPのintlはICUのそれをラップしているだけなので、PHPだからどうとか言われても困りますけどね。
getBestPattern
とかいう名前も実は元のままだったりします。
しかしintlの情報とか使用例とかって全然出てこないので、ja_JP
だったら2021/04/24
になるのか2021年4月24日
になるのか、そこらへんもよくわからないな。
Add array_is_list(array $array): bool
賛成41反対1で受理。
関数array_is_listを追加します。
キーが0始まりで抜けの無い配列である場合にのみ、trueになります。
array_is_list([1, 2]); // true
array_is_list([1=>1, 2]); // false
array_is_list([1, 2=>2]); // false
array_is_list(['a'=>1, 2]); // false
PHPで配列と呼ばれているものは実際は順序付きハッシュであり、他言語で言うところの配列というものは存在しません。
この関数を使えば、他言語で言うところの配列と同じものであるか否かを判定できるようになります。
まあ純粋な配列が使いたければSplFixedArrayとか使えばいいですし、そもそもPHPでは純粋な配列と抜けのある配列を区別する意味が全くないので、ユーザランドでの出番は多くないと思います。
Explicit octal integer literal notation
賛成33反対0で受理。
8進数の基数表示です。
$a = 16; // 10進数
$a = 0x10; // 16進数
$a = 0b00010000; // 2進数
$a = 020; // 8進数
8進数だけ接頭辞がなくてわかりにくいですね。
そこで0o
による8進数表記をサポートします。
$a = 0o20; // PHP8.1以降OK
既存の書き方については、当面は削除される予定はありません。
Restrict $GLOBALS usage
賛成48反対0で受理。
$GLOBALS
の内部処理を変更するRFCです。
$a = 1;
$globals = $GLOBALS; // 値をコピー
$globals['a'] = 2;
var_dump($a); // int(2)
値をコピーしただけのはずなのに、何故か元の値も変わってしまっています。
このように$GLOBALS
には色々と特殊な仕様が存在しており、そのせいで内部構成が複雑になり、パフォーマンスの問題も発生しているそうです。
そこで、このような特殊な仕様をなるべく排除します。
通常の使い方での互換性は概ね維持されますが、リファレンスを駆使した変なコードは動かなくなるみたいです。
foreach ($GLOBALS as $var => $_) $$var =& $GLOBALS[$var]; // 動かなくなる
まあ、普通はそもそも$GLOBALS
が目に入った時点で拒否反応が出ますよね。
Change Default mysqli Error Mode
賛成21反対9で受理。
mysqliのエラーモードのデフォルトが例外になります。
PHP8.0までデフォルト値はMYSQLI_REPORT_OFFであり、あえて設定変更しないかぎり黙って死ぬという困った仕様でした。
PHP8.1以降デフォルト値はMYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT
になります。
PHP8.0でPDOのエラーモードデフォルトも例外になったので、それに合わせたものとなります。
Add fetch_column method to mysqli
賛成18反対2で受理。
mysqli_result::fetch_column
メソッドです。
mysqliにはfetch関数が色々あるのに、何故かfetch_columnがなかったので追加されます。
ただ個人的にはPDOでもfetchColumnを使ったことないし、ひとつのカラムだけ取り出すという需要がよくわかりません。
Mysqli bind in execute
賛成32反対0で受理。
mysqli::executeが実行時引数渡しに対応します。
// PDO 事前渡し
$stmt = $pdo->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->bindParam(0, $id);
$stmt->bindParam(1, $name);
$stmt->execute();
// PDO 実行時渡し
$stmt = $pdo->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->execute([$id, $name]);
// mysqli 事前渡し
$stmt = $mysqli->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->bind_param('ss', $id, $name);
$stmt->execute();
mysqliは何故か引数の実行時渡しに対応していませんでした。
PHP8.1以降は、PDO同様実行時に渡せるようになります。
// mysqli 実行時渡し PHP8.1以降
$stmt = $mysqli->prepare('INSERT INTO users(id, name) VALUES(?,?)');
$stmt->execute([$id, $name]);
個人的には完全にPDOしか使わないので、mysqliにいまさら色々追加されてもなあって感じが否めない。
fsync() Function
賛成30反対1で受理。
関数fsync
を追加します。
ソースを見てみると単にC言語のfsyncのラッパーでした。
よくわからないので参考URLとかを読んでみるに、fwriteなどの書き込み関数は、実は呼んだ時点でディスクに書き込み完了しているわけではなく、どこかにキャッシュされているだけなので、直後にプログラムが落ちたら書き込まれずに消滅してしまう危険性がある。
そこでキャッシュを出力する関数としてfflush
やfsync
やfdatasync
が存在し、fsync
であれば実際にディスクに書き込み完了したところまで確認する。
ということのようです。
PHPにはfflushは存在しましたが、これまでは無かったfsync
を追加します。
あとRFCにははっきり書かれていませんが、プルリクにはしれっとfdatasync
も入ってました。
Phasing out Serializable
賛成36反対0で受理。
SerializableをE_DEPRECATEDにします。
class HOGE implements Serializable{
// Serializable
public function serialize() {
return serialize($this->data);
}
public function unserialize($data) {
$this->data = unserialize($data);
}
// マジックメソッド
public function __serialize() {
return serialize($this->data);
}
public function __unserialize($data) {
$this->data = unserialize($data);
}
}
PHP7.4でマジックメソッド__serialize/__unserialize
が実装され、Serializable
より優先されるようになりました。
上記コードはPHP7.4以降__serialize/__unserialize
だけが実行され、serialize/unserialize
は無視されます。
今回E_DEPRECATEDになる条件は、Serializable
だけが実装されていて、__serialize/__unserialize
が実装されていない場合です。
両方とも実装されている場合は、PHP8.1でも警告は出ないようです。
両方とも実装されていない場合は、もちろん今までどおり何も出ません。
Static variables in inherited methods
賛成38反対0で受理。
静的変数を継承したときの挙動が微妙に変更になります。
class A {
public static function counter() {
static $i = 0;
return ++$i;
}
}
class B extends A {}
var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter()); // <PHP8.1:int(1) >=PHP8.1:int(3)
var_dump(B::counter()); // <PHP8.1:int(2) >=PHP8.1:int(4)
静的変数は、同じクラスであれば同じ変数、異なるクラスであれば別の変数になる、というよくわからない状態でした。
これを継承に関わらず常に同じ変数を見るようにします。
変更後は、クラス変数と同じ挙動になります。
class A {
static $i = 0;
public static function counter() {
return ++self::$i;
}
}
class B extends A {}
var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter()); // int(3)
var_dump(B::counter()); // int(4)
静的変数とクラス変数でなぜか動作が微妙に異なっていたので、この動作を合わせるためのものです。
そもそもなんで異なっていたんだ。
Make reflection setAccessible() no-op
賛成31反対0で受理。
ReflectionPropertyおよびReflectionMethodにおいて、デフォルトでsetAccessible(true)
になります。
class HOGE{
private static int $foo = 1;
private static function bar()
{
return 2;
}
}
// ReflectionProperty
$p = new ReflectionProperty(HOGE::class, 'foo');
$p->setAccessible(true);
$p->getValue(); // 1
// ReflectionMethod
$p = new ReflectionMethod(HOGE::class, 'bar');
$p->setAccessible(true);
$p->invoke(); // 2
このsetAccessible
、どうせ毎回必ず指定するんだから最初からデフォルトでええやん、という主張。
これによって、同じコードが(new ReflectionProperty(HOGE::class, 'foo'))->getValue();
みたいにさくっと書けるようになります。
そしてsetAccessible
は実質的に何もしなくなります。
まあ便利といえば便利だけど、もっと評価が分かれてもよさそうな提案なのに、全員賛成ってのは少々びっくり。
Deprecate autovivification on false
賛成34反対2で受理。
false
から配列の自動生成を禁止します。
// undefinedから
$arr[] = 'some value';
$arr['doesNotExist'][] = 2; // ['some value', 'doesNotExist'=>[2]]
// nullから
$arr = null;
$arr[] = 2; // [2]
// falseから
$arr = false;
$arr[] = 2; // [2]
PHPでは、このように何もないところにいきなり配列値を突っ込むと、自動的に配列ができあがります。
このうち、false
だけはPHP8.1から自動生成を禁止します。
これだけだと何でfalse
だけ?って思いますが、実は''
や0
やtrue
からの配列自動生成は既に前から禁止されているので、そちらのスカラー型の動作に合わせたということです。
むしろundefined
とnull
が例外です。
// trueから
$arr = true;
$arr[] = 2; // Warning: Cannot use a scalar value as an array
Deprecations for PHP 8.1
様々な古い機能や書き方について、PHP8.1でDepreatedにし、PHP9で削除するという提案。
詳細は別記事で解説しますが、ほとんどは聞いたこともない関数や元より動いていなかった設定などです。
たとえばdate_sunriseとdate_sunsetを削除してdate_sun_infoに集約します。
個人的に気になったのはstrftimeくらいですかね。
ほとんどのユーザにとっては影響はないと思われます。
感想
PHP8.0やPHP7.4に比べるとそこまで多いというわけではありませんが、それでも様々な機能追加や改善がなされています。
今回の目玉はFiberと型関連でしょうか。
Fiberは『上から順に実行する』というPHPの原則を破る、大きな可能性を秘めた存在です。
今後のライブラリの対応次第では、有力な機能となることでしょう。
また型については、交差型・never型・列挙型の追加、継承時の挙動厳格化などが含まれています。
PHPは7以降、型関連の実装・機能追加が相次いで行われてきました。
今ではもはや、下手な静的型付け言語よりずっと厳格な型運用を行うことすら可能になっています。
あとArray unpacking with string keysは、これまでと書式ががらっと変わるのでPHP8.1以前と両用していると導入は難しそうですが、使い慣れると非常に便利になりそうです。
ということで、年末にリリース予定のPHP8.1をみんなも是非やりましょう。