PHP 8が2020年11月26日に正式にGAリリースされました!

今回の新しいメジャーアップデートでは、多くの最適化と強力な機能が導入されます。本記事では、より良いコードを書き、より堅牢なアプリケーションを構築することができる最も興味深い変更点をご紹介します。

PHP 8.0のリリース発表
PHP 8.0のリリース発表

PHP JIT(ジャストインタイム・コンパイラー)

PHP 8の最も高く評価されている新機能は、ジャストインタイム(JIT)コンパイラーです。 JITとは何でしょうか?

RFC提案では、JITについて次のように説明しています。

PHP JITはOPcacheの一部として、しかしほぼ独立したものとして実装されます。PHPのコンパイル時に有効無効を設定します。有効にした場合、PHPファイルのネイティブコードがOPCacheの共有メモリに保存されるようになり、op_array→opcodes[].handler(s)はJITされたコードのエントリポイントへのポインターを保持します

では、どのようにしてJITに到達したのでしょうか?また、JITとOPcacheの違いは何ですか?

PHPのJITについて理解を深めるために、PHPのソースコードから最終結果までどの実行プロセスを簡単に見てみましょう。

PHPの実行は4段階のプロセスです。

  • 字句解析/トークン化:まず、インタープリターがPHPコードを読み取り、トークンのセットを作成します。
  • 構文解析:インタープリターは、スクリプトが構文規則に一致するかどうかを確認し、トークンを使用して、ソースコードの構造の階層を示す抽象構文ツリー(AST)を構築します。

次の画像は、基本的なPHP実行プロセスを表したものです。

基本的なPHP実行プロセス
基本的なPHP実行プロセス

では、OPcacheはどのようにしてPHPを高速化しますか?そして、実行プロセスのJITによる変更点は何でしょうか?

OPcache拡張機能

PHPはインタプリタ言語です。つまり、PHPスクリプトが実行されると、インタプリタはリクエストごとにコードを構文解析してコンパイルして実行します。これにより、CPUリソースを無駄に使用して、追加の時間が発生する場合があります。

ここでOPcache拡張機能が登場します。

OPcacheは、プリコンパイルされたスクリプトのバイトコードを共有メモリに保管する為、リクエストごとにスクリプトを読み込んでパースせずに済むことによりPHPのパフォーマンスを向上させます

OPcacheを有効にすると、PHPインタープリターは、スクリプトが初めて実行されるときにのみ、上記の4ステップのプロセスを実行します。PHPバイトコードは共有メモリに保存されるため、低レベルの中間表現としてすぐに利用でき、Zend VMですぐに実行できます。

OPcacheを有効にしたPHP実行プロセス
OPcacheを有効にしたPHP実行プロセス

PHP 5.5以降、Zend OPcache拡張機能はデフォルトで利用可能です。サーバー上のスクリプトからphpinfo()を呼び出すか、php.iniファイルをチェックするだけで、正しく構成されているかどうかを確認できます(OPcache構成設定を参照)。

参照文献:WordPressのPHPのメモリ制限を改善する方法.

phpinfoページのZend OPcacheセクション
phpinfoページのZend OPcacheセクション

プリロード

OPcacheは最近、PHP 7.4で追加された新しいOPcache機能であるプリロードにより改善されました。プリロードは、「アプリケーションコードが実行される前に」指定されたスクリプトをOPcacheメモリに保存する機能ですが、通常のWebベースのアプリケーションの幅広いパフォーマンスの向上はもたらしません。

プリロードの詳細については、当社のPHP 7.4についての記事をご覧ください。

JITでは、PHPは一歩前進します。

JIT —ジャストインタイム・コンパイラー

オペコードが低レベルの中間表現であっても、マシンコードにコンパイルする必要があります。JITは「IR(中間表現)の新し形を導入しません」が、DynASM(コード生成エンジン用の動的アセンブラ)を使用して、PHPバイトコードから直接にネイティブコードを生成します。

つまり、JITは中間コードの重要な部分をマシンコードに変換します。コンパイルをバイパスするおかげで、パフォーマンスとメモリ使用状況を大幅に改善させるでしょう。

PHP JIT提案の共著者であるZeev Suraskyが、JITを使用すると計算がどれほど速くなるかを次のビデオで示しています。

一方、JITはWordPressのパフォーマンスを効果的に向上させるのでしょうか?

ライブウェブアプリ用のJIT

JIT RFCによると、ジャストインタイムのコンパイラー実装により、PHPのパフォーマンスが向上するようです。しかし、WordPressなどの実際のアプリで改善が実際に見られるのでしょうか?

初期のテストでは、JITがCPU消費の高いワークロードの実行を大幅に高速化することが示されていますが、RFCは次のように述べています

…以前の試みと同様に、WordPressなどの実際のアプリは大幅に改善されないようです(opcache.jit=1235 326リクエスト/秒対 315リクエスト/秒)

プロファイリングと投機的な最適化を使用して、実際のアプリのJITを改善するための追加の取り組みは予定されています。」

JITを有効にすると、コードはZend VMではなくCPU自体により実行されるため、計算速度が向上します。WordPressなどのウェブアプリのパフォーマンスは、 TTFBデータベースの最適化HTTPリクエストなどのその他の要素にもよるものです。

PHP 8パフォーマンスグラフ
JITのPHP8のパフォーマンスへの影響(画像の出典:PHP 8.0のリリース発表)

したがって、WordPressなどのアプリに関しては、PHPの実行速度が大幅に向上することは期待できません。それにもかかわらず、JITは開発者に多くの利点をもたらす可能性があります。

Nikita Popovは次のように述べています

JITコンパイラーの利点は次の(RFCで既に概説されている)とおりです。

  • 数値コードのパフォーマンスが大幅に向上します。
  • 一般的なPHP ウェブアプリケーションコードのパフォーマンスがやや向上します。
  • PHPが十分に高速になるため、さらに多くのコードをCからPHPに移動できるようになります

したがって、JITはWordPressのパフォーマンスに大きな改善をもたらすことはありませんが、PHP自体が大幅に改善し、多くの関数を直接に書き込むことができる言語になります。

一方、より複雑なものになるため、 メンテナンス、安定性、およびデバッグのコストの増加につながる可能性があるという短所があります。Dmitry Stogovは次のように述べています。

JITは非常に単純なものですが、いずれにしても、PHP全体の複雑さが高まる為、新しいバグのリスクと、開発と保守のコストが増加する可能性があります

JITの導入についての提案は50対2の投票で可決しました。

PHP 8の改善点と新機能

JIT以外にも、PHP 8には多くの新機能と改善が期待できます。次のリストは、PHPの信頼性と効率を高める追加と変更を厳選したものです。

コンストラクタのプロパティ昇格機能

PHPにおけるオブジェクトの使いやすさを向上させるための議論の結果として、 コンストラクタのプロパティ昇格機能のRFCでは、プロパティ宣言を単純化し、 冗長性をなくした新しい簡潔な構文が提案されました。

この提案は、昇格されたパラメータ、つまり publicprotectedprivateの可視性キーワードが前に付いたメソッドパラメータにのみを対象にしていします。

現在、プロパティをオブジェクトで使用できるように、何度か(少なくとも4回)繰り返して記入する必要があります。RFCの次の例を見てみましょう。

class Point {
    public int $x;
    public int $y;
    public int $z;

    public function __construct(
        int $x = 0,
        int $y = 0,
        int $z = 0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

RFCの著者であるNikita Popov氏によると、プロパティ宣言、コンストラクタのパラメータ、プロパティの割り当ての3カ所で、プロパティ名を少なくとも4回は書かなければならないそうです。この構文は、特に多くのプロパティのある長いクラス名のクラスでは、使い勝手が悪いです。

このRFCでは、コンストラクタとパラメータの定義を統合することが提案されています。したがって、PHP 8では、パラメータの宣言のより使いやすい方法が採用されます。上記のコードは以下のように変更できます。

class Point {
    public function __construct(
        public int $x = 0,
        public int $y = 0,
        public int $z = 0,
    ) {}
}

以上です!このように、短く、読みやすく、エラーが起きにくいプロパティ昇格機能ができました。Nikitaは次のように述べています

単純な構文変換なんです。一方、そのおかげで、特にバリューオブジェクトのために書かなければならないボイラープレートコードが減ります…

プロパティ宣言は、それらのプロパティを明示的に宣言したように変換され、リフレクションAPIを使って実行前にプロパティ定義をイントロスペクトすることができます(脱糖を参照)。

リフレクション(およびその他のイントロスペクション機構)は、 脱糖の後の状態を観察します。つまり、昇格されたプロパティは、明示的に宣言されたプロパティと同じように表示され、昇格されたコンストラクタの引数は、通常のコンストラクタの引数と同じように表示されます

// before desugaring
class Point {
    public function __construct(public int $x = 0) {}
}

// after desugaring
class Point {
    public int $x;

    public function __construct(int $x = 0) {
        $this->x = $x;
    }
}

継承

昇格されたパラメータと一緒に継承を使用することに、特に制限はありません。いずれにしても、親クラスと子クラスのコンストラクタには特別な関係はありません。Nikitaは次のように述べています

通常、メソッドは必ず親メソッドと互換性がなければならないと言われています。[…] しかし、このルールはコンストラクタには適用されません。つまり、あるコンストラクタは1つだけのクラスに属していて、親クラスと子クラスのコンストラクタには何の互換性も必要ないのです

下記はその一例です。

class Test {
    public function __construct(
        public int $x = 0
    ) {}
}

class Child extends Test {
    public function __construct(
        $x, 
        public int $y = 0,
        public int $z = 0,
    ) {
        parent::__construct($x);
    }
}

昇格されたプロパティで許可されていないもの

昇格されたプロパティは、abstractコンストラクタやトレイトで使用できますが、いくつかの制限がありますので、ここで紹介します。

abstractコンストラクタ

昇格されたプロパティは、abstractクラスやインターフェイスでは使用できません。

abstract class Test {
    // Error: Abstract constructor.
    abstract public function __construct(private $x);
}
 
interface Test {
    // Error: Abstract constructor.
    public function __construct(private $x);
}
NULL可能な型

最も注目すべき制約の1つは、「NULL可能な型」に関するものです。これまでは、NULL可能でない形を使用していました。しかし、デフォルト値がnullの場合、その型は暗黙的にnull可能になります。しかし、プロパティ型では、昇格されたパラメータにはプロパティ宣言が必要であり、NULL可能な型は明示的に宣言しなければならないため、この暗黙の動作はありません。RFCの次の例をご覧ください。

class Test {
    // Error: Using null default on non-nullable property
    public function __construct(public Type $prop = null) {}

    // Correct: Make the type explicitly nullable instead
    public function __construct(public ?Type $prop = null) {}
}
呼び出し可能な型

呼び出し可能な型はプロパティでサポートされていないため、昇格されたプロパティで呼び出し可能な型を使用することはできません。

class Test {
    // Error: Callable type not supported for properties.
    public function __construct(public callable $callback) {}
}
var キーワードは使用不可

昇格されたパラメータに使用できるのは visibility キーワードだけなので、コンストラクタのプロパティを var キーワードで宣言することはできません(RFC の次の例を参照)。

class Test {
    // Error: "var" keyword is not supported.
    public function __construct(var $prop) {}
}
重複は使用不可

昇格されたプロパティと明示的なプロパティを同じクラスで組み合わせることはできますが、プロパティを2回宣言することはできません。

class Test {
    public string $prop;
    public int $explicitProp;

    // Correct
    public function __construct(public int $promotedProp, int $arg) {
        $this->explicitProp = $arg;
    }

    // Error: Redeclaration of property.
    public function __construct(public string $prop) {}
}
可変パラメータは使用不可

宣言された型が、実際には配列である可変パラメータとは異なるためです。

class Test {
    // Error: Variadic parameter.
    public function __construct(public string ...$strings) {}
}

関連記事

コンストラクタのプロパティ昇格機能について詳しく知りたい方は、Nikita Popov氏へのインタビューをお聞きください。PHPにおけるオブジェクトの使いやすさの詳細については、こちらの記事Larry Garfield氏へのインタビューをご覧ください。

トレイトの抽象メソッドの検証

トレイトは、「PHP のような単一継承言語でコードを再利用するための仕組みのひとつです。」通常、複数のクラスで使用できるメソッドを宣言するために使用されます。

トレイトには、抽象メソッドを含めることもできます。これらのメソッドは、単にメソッドのシグネチャを特定しますが、メソッドの実装は、トレイトを使用してクラス内で行う必要があります。

PHPのマニュアルによると、

トレイトでは、静的なメンバーやメソッドを定義できます

したがって、メソッドのシグネチャが一致する必要があります。つまり、必要な引数の種類と数は同じである必要があります

とにかく、RFCの作者であるNikita Popovによれば、シグネチャの検証は現在、限定的に強制されています。

  • メソッドの実装がhttps://3v4l.org/SeVK3クラスの使用によって提供される最も一般的な場合では、シグネチャの検証が強制されません。
  • 実装がhttps://3v4l.org/4VCIp親クラスからのものである場合は、強制されます。
  • 実装がhttps://3v4l.org/q7Bq2子クラスからのものである場合は、強制されます

次のNikitaの例は、1番の(シグネチャが強制されない)場合の例です。

trait T {
	abstract public function test(int $x);
}
 
class C {
	use T;

	// Allowed, but shouldn't be due to invalid type.
	public function test(string $x) {}
}

そうは言っても、このRFCでは、実装メソッドがその起源に関係なく、トレイトの抽象メソッドと互換性がない場合、常に致命的なエラーを出すことが提案されています。

Fatal error: Declaration of C::test(string $x) must be compatible with T::test(int $x) in /path/to/your/test.php on line 10

互換性のないメソッドシグネチャ

このRFCは満場一致で承認されました。

PHPでは、互換性のないメソッドシグネチャによる継承エラーは、エラーの原因に応じて、致命的なエラーまたは警告メッセージのいずれかを返します。

クラスがインターフェイスを実装している場合、互換性のないメソッドシグネチャは致命的なエラーを返します。オブジェクト インターフェイスのドキュメントによると:

インターフェイスを実装したクラスには、 そのインターフェイスで定義されているメソッドと、リスコフの置換原則(LSP)と互換性が取れたメソッドシグネチャを使う必要があります。そうしなければ致命的なエラーが発生します

以下は、インターフェースの継承エラーの例です。

interface I {
	public function method(array $a);
}
class C implements I {
	public function method(int $a) {}
}

PHP 7.4では、上記のコードは次のエラーを返します。

Fatal error: Declaration of C::method(int $a) must be compatible with I::method(array $a) in /path/to/your/test.php on line 7

互換性のないシグネチャのある子クラスの関数は警告メッセージを返します。RFCの次のコードを参照してください。

class C1 {
	public function method(array $a) {}
}
class C2 extends C1 {
	public function method(int $a) {}
}

PHP 7.4では、上記のコードは単に警告メッセージを返します。

Warning: Declaration of C2::method(int $a) should be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

そこで、このRFCでは、互換性のないメソッドシグネチャに対して常に致命的なエラーを返すことが提案されています。PHP 8を使用すると、上記のコードで次の内容が表示されます。

Fatal error: Declaration of C2::method(int $a) must be compatible with C1::method(array $a) in /path/to/your/test.php on line 7

マイナススタートの配列インデックス

PHPでは、配列の最初のインデックスが負(start_index < 0)の場合は、それ以降のインデックスはゼロから始まります。(詳細については、array_fillのドキュメントをご参照ください。)次の例をご覧ください。

$a = array_fill(-5, 4, true);
var_dump($a);

PHP 7.4では、結果は次のようになります。

array(4) {
	[-5]=>
	bool(true)
	[0]=>
	bool(true)
	[1]=>
	bool(true)
	[2]=>
	bool(true)
}

そこで、このRFCは、start_indexの値に関係なく、2番目のインデックスがstart_index + 1になるように変更することが提案されています。

PHP 8では、上記のコードは次の配列になります。

array(4) {
	[-5]=>
	bool(true)
	[-4]=>
	bool(true)
	[-3]=>
	bool(true)
	[-2]=>
	bool(true)
}

PHP 8では、マイナススタートのインデックスの配列の動作が変わります。下位互換性の詳細については、RFCをご参照ください。

union型2.0

union型(union型) は、単一の型ではなく、複数の異なる型の値を受け入れます。PHPは既に、?Type構文と特別なiterable型を除いて、union型をサポートしています。

PHP 8以前のバージョンでは、次のRFCの例にのように、union型はphpdoc注釈でのみ指定できました

class Number {
	/**
	 * @var int|float $number
	 */
	private $number;

	/**
	 * @param int|float $number
	 */
	public function setNumber($number) {
		$this->number = $number;
	}

	/**
	 * @return int|float
	 */
	public function getNumber() {
		return $this->number;
	}
}

そこで、union型2.0の RFCでは、関数のシグネチャにunion型のサポートを追加することが提案されています。これにより、 インラインドキュメントに頼ることなく、union型を構文T1|T2|...で表できるようになります。

class Number {
	private int|float $number;

	public function setNumber(int|float $number): void {
		$this->number = $number;
	}

	public function getNumber(): int|float {
		return $this->number;
	}
}

RFCでは、Nikita Popovが次のように述べています。

言語でunion型をサポートすることにより、より多くの型情報をphpdocに頼ることなく関数シグネチャに移動することができ、多数の利点が得られます。

  • 型は実際に強制されるため、ミスを早期に発見できる。
  • エッジケースを見逃したり、仕様変更の際にドキュメントを更新し忘れたりする可能性が減る。
  • 継承時にもリスコフの置換原則を適用できる。
  • リフレクションから利用できる。
  • phpdocより分量が減らせる

union型は、いくつかの例外がありますが、現在PHPでサポートされている全ての型をサポートします。

  • voidは関数が値を返さないことを意味するため、void型がunion型の一部となることは決してできません。
  • null型はunion型の一部としてのみ有効な型で、単独で使うことはできません。
  • null許容型表記(?T)は、T|nullの省略形として今後も有効な構文ですが、union型と?T表記を混ぜて使用することはできません。(?T1|T2は全て不正な文法で、この場合はT1|T2|nullを使う必要があります。)
  • 多くの関数(例えば、strpos()strstr()substr()など)では、可能な戻り値の型の中にfalseがあるため、false疑似型もサポートされています。

union型2.0の詳細については、RFCをご参照ください。

内部関数のTypeErrorの一貫性

不正なパラメーターが渡された場合、内部関数ユーザー定義関数の動作は異なります。

ユーザー定義関数ではTypeErrorが返されていますが、内部関数の動作は複数の条件に応じてそれぞれ異なります。最もよくある動作は警告を出してnullを返すことです。PHP 7.4の次の例を参照してください。

var_dump(strlen(new stdClass));

これにより、次の警告が表示されます。

Warning: strlen() expects parameter 1 to be string, object given in /path/to/your/test.php on line 4
NULL

strict_typesが有効になっている場合、または引数情報が型を指定している場合、動作は異なります。この場合では、型エラーが発生し、TypeErrorが返されます。

この状況は、RFCの「課題」セクションで十分に説明されているように、多くの問題につながります。

これらの不整合をなくすために、このRFC proposesでは、パラメーター型が一致しない場合に内部パラメーター解析APIを常にThrowErrorを生成するようにすることが提案されています。

PHP 8では、上記のコードは次のエラーを返します。

Fatal error: Uncaught TypeError: strlen(): Argument #1 ($str) must be of type string, object given in /path/to/your/test.php:4
Stack trace:
#0 {main}
  thrown in /path/to/your/test.php on line 4

throw式

PHPでは、throwステートメントであるため、しか使用できない場所では使用できません。

このRFCでは、式が許可されているすべての場合で使用できるように、throwステートメントを式に変換することが提案されています。たとえば、矢印関数null結合演算子三項演算子およびelvis演算子など。

RFCの次の例を参照してください。

$callable = fn() => throw new Exception();

// $value is non-nullable.
$value = $nullableValue ?? throw new InvalidArgumentException();
 
// $value is truthy.
$value = $falsableValue ?: throw new InvalidArgumentException();

弱いマッピング

弱いマッピングは、キーが弱く参照される一連のデータ(オブジェクト)のことです。つまり、オブジェクトがガベージコレクションの対象となることが予防されません。

PHP 7.4では、オブジェクト自体の破棄を妨げないオブジェクトへの参照を保持する方法として、弱いリファレンスが導入されました。Nikita Popovが指摘したように、

生の弱いリファレンスの用途が限られており、実際には弱マッピングの方がはるかに一般的に使用されます。破棄時のコールバックが提供されていないため、PHPの弱いリファレンスの上に効率的な弱いマッピングを実装することはできません

したがって、このRFCではWeakMapクラスが導入され、キーオブジェクトへのリファレンスがそれ以上ない場合、破棄してウィークマッピングから削除できるウィークマップキーとして使用されるオブジェクトが作成されます。

実行時間の長いプロセスでは、これによりメモリリークが防止され、パフォーマンスが向上します。RFCの次の例を参照してください。

$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 42;
var_dump($map);

PHP 8を使用すると、上記のコードの結果は次のようになります。(実行中のコードについてはこちらを参照)

object(WeakMap)#1 (1) {
	[0]=>
	array(2) {
		["key"]=>
		object(stdClass)#2 (0) {
		}
		["value"]=>
		int(42)
	}
}

オブジェクトの指定を解除すると、キーはウィークマッピングから自動的に削除されます。

unset($obj);
var_dump($map);

これでは、結果は次のようになります。

object(WeakMap)#1 (0) {
}

弱いマッピングの詳細については、RFCをご参照ください。提案は満場一致で承認されました。

パラメータリストの末尾のカンマ

末尾のカンマは、あらゆるコンテキストのアイテムのリストに追加されるカンマです。PHP 7.2ではリスト構文の末尾のカンマPHP 7.3では関数呼び出しの末尾のカンマが導入されました。

PHP 8では、次の例に示すように、関数、メソッド、およびクロージャーを含むパラメータリストの末尾のカンマが導入されています。

class Foo {
	public function __construct(
		string $x,
		int $y,
		float $z, // trailing comma
	) {
		// do something
	}
}

このRFCは58対1の投票で可決しました。

::class構文がオブジェクトで使用できるようになる

クラス名を取得するには、Foo\Bar::class構文を使用します。このRFCでは、同じ構文をオブジェクトに展開して、以下の例のように、特定のオブジェクトのクラス名を取得できるようにすることが提案されています。

$object = new stdClass;
var_dump($object::class); // "stdClass"
 
$object = null;
var_dump($object::class); // TypeError

PHP 8では、$object::classはget_class($object)と同じ結果を返します。PHP 8では、$object::classget_class($object)と同じ結果を返します。$objectがオブジェクトでない場合は、TypeError例外が返されます。

提案は満場一致で承認されました。

属性v2

注釈とも呼ばれる属性は、オブジェクト、要素、またはファイルのプロパティを指定する構造化メタデータです。

PHP 7.4まで、クラスや関数などの宣言にメタデータを追加する唯一の方法はdoc-commentsでした。今回の属性v2のRFCでは、クラス、プロパティ、関数、メソッド、パラメーター、定数などの宣言に追加できる構造化された構文メタデータとして定義されるPHPの属性が導入されます。

属性は、参照する宣言の前に追加されます。RFCの次の例を参照してください。

<<ExampleAttribute>>
class Foo
{
	<<ExampleAttribute>>
	public const FOO = 'foo';

	<<ExampleAttribute>>
	public $x;

	<<ExampleAttribute>>
	public function foo(<<ExampleAttribute>> $bar) { }
}

$object = new <<ExampleAttribute>> class () { };

<<ExampleAttribute>>
function f1() { }

$f2 = <<ExampleAttribute>> function () { };

$f3 = <<ExampleAttribute>> fn () => 1;

属性は、doc-blockコメントの前にみ後にも追加できます。

<<ExampleAttribute>>
/** docblock */
<<AnotherExampleAttribute>>
function foo() {}

各宣言には1つ以上の属性があり、各属性には1つ以上の関連する値があります。

<<WithoutArgument>>
<<SingleArgument(0)>>
<<FewArguments('Hello', 'World')>>
function foo() {}

PHPの属性、使用例、および代替構文の詳細については、RFCをご参照ください。

名前付き引数

名前付き引数は、引数を渡すことを可能にするための新しい方法です。

名前付き引数は、位置ではなく、名前ベースで引数を渡すことを可能にします

関数に名前付きの引数を渡すには、その値の前にパラメータ名を加えるだけです。

callFunction(name: $value);

また、以下の例のように、予約済みのキーワードを使用することもできます。

callFunction(array: $value);

しかし、パラメータ名を動的に渡すことはできません。パラメータは識別子でなければならないため、次のような構文は使用不可です

callFunction($name: $value);

このRFCの著者であるNikita Popov氏によると、名前付き引数にはいくつかの利点がありそうです。

まず第一に、名前付き引数は、その意味が記録されるため、より理解しやすいコードを書けます。RFCの以下の例を見れば、一目瞭然です。

array_fill(start_index: 0, num: 100, value: 50);

名前付き引数は、渡す順番は関係ありません。つまり、関数のシグネチャと同じ順序で関数に引数を渡さなくてもかまいません。

array_fill(value: 50, num: 100, start_index: 0);

名前付き引数は、位置を指定した引数と組み合わせることが出来ます。

htmlspecialchars($string, double_encode: false);

名前付き引数のもう一つの大きな利点は、変更したい引数だけを指定できることです。デフォルトの値を上書きしたくない場合は、デフォルトの引数を指定する必要はありません。RFCの次の例を見れば一目瞭然です。

htmlspecialchars($string, default, default, false);
// vs
htmlspecialchars($string, double_encode: false);

次のRFCの例のように、名前付き引数は、PHPの属性と組み合わせて使うことができます。

<<MyAttribute('A', b: 'B')>>
class Test {}

しかし、名前付き引数の後に位置付き引数を渡すことはできず、コンパイル時にエラーになります。同じパラメータ名を2回渡した場合も同様です。

名前付き引数はクラス宣言の際に便利です。コンストラクタは通常多くのパラメータを持つため、名前付き引数はより使いやすい方法でクラスを宣言することができます。

名前付き引数の詳細、制約事項、後方互換性、そしていくつかの例については、名前付き引数のRFCを参照してください。

null安全オペレータ

このRFCでは、完全なショートサーキット評価が可能なnull安全オペレータ$->を提案します。

ショートサーキット評価では、最初の演算子がnullと評価されない場合に限り、2 番目の演算子が評価されます。チェインの中の演算子がnullと評価された場合、チェイン全体の実行が停止し、nullと評価されます。

RFCの次の例を参照してください。

$foo = $a?->b();

$aが null の場合は、メソッド b()は呼び出されず、$foonullに設定されます。

その他の例や例外、今後の範囲については、 null安全オペレータのRFCを参照ください。

文字列と数値の比較の合理化

以前のバージョンの PHP では、非厳密な比較演算子を用いた文字列と数値の比較は、文字列を数値にキャストし、その後整数か浮動小数の比較を行っています。この動作は非常に便利な場面もありますが、間違った結果を生むこともあり、バグやセキュリティ上の問題につながる可能性もあります。

RFCの次の例を参照してください。

$validValues = ["foo", "bar", "baz"];
$value = 0;
var_dump(in_array($value, $validValues));
// bool(true)

PHP 8 では、文字列と数値の比較をより合理的に行うことを目的とした文字列の数値の合理化が導入されました。Nikita Popov氏の言葉は次のとおりです。

このRFCでは、文字列と数値の比較をより合理的にすることを意図しています。 数値文字列を数値と比較する際には、文字列を数値に変換してから比較し、これは現在と同じ動作です。それ以外の場合は、数値を文字列に変換して文字列比較を行います

次の表は、以前のバージョンの PHP と PHP 8 での文字列と数値の比較の動作を比較したものです。

Comparison    | Before | After
------------------------------
 0 == "0"     | true   | true
 0 == "0.0"   | true   | true
 0 == "foo"   | true   | false
 0 == ""      | true   | false
42 == "   42" | true   | true
42 == "42foo" | true   | false

この変更による多くの影響や、文字列と数値の比較が PHP 8 でどのように変わるのかについては、Nikita Popov による公式 RFC を参照してください。

数値を含む文字列の合理化

PHPでは、数字を含む文字列は3つのカテゴリに分類されます。

  • 数値文字列: 数値を含む文字列。前には空白がある場合があります。
  • 先頭が数字の文字列: 最初の文字が数字の文字列で、最後の文字が非数字の文字列。
  • 非数値文字列:前記のカテゴリーのいずれにも該当しない文字列。

数値文字列と先頭が数字の文字列は、実行する操作によって扱いが異なります。例えば、以下のようになります。

  • 文字列から数値への明示的な変換(例:(int)および(float)型のキャスト)は、数値文字列と先頭が数字の文字列を数値に変換します。数字でない文字列を明示的に数字に変換すると、0が生成されます。
  • 文字列から数値への黙示的な変換(例:strict_type宣言なし)は、数値文字列と非数値文字列で異なる結果になります。数字ではない文字列から数字への変換を行うと、TypeErrorが発生します。
  • is_numeric()は、数字の文字列に対してのみtrueを返します。

文字列のオフセット、算術演算、増分・減分演算、文字列同士の比較、ビット演算でも結果は異なります。

このRFCでは、以下が提案されています。

さまざまな数値文字列モードを統一します。先頭と末尾の両方に空白を含む数字のみが許可されます。他のタイプの文字列は非数値文字であり、数値のコンテキストで使用するとTypeErrorが発生します

つまり、現在、E_NOTICE「A non well-formed numeric value encountered」が発しているすべての文字列は、先頭が数字の文字列に末尾のホワイトスペースしか含まれていない場合を除き、E_WARNING「A non-numeric value encountered」に再分類されます。また、現在E_WARNINGが発している様々なケースでは今後、TypeErrorが発生するようになります

PHP 8 における数値文字列関連の変更の詳細、コード例、例外の状況、後方互換性の問題などについては、RFCを参照してください。

match式2

新しいmatch式はswitchとよく似ていますが、より安全なセマンティクスを備えて、戻り値を可能にしています。

この2つの制御構造の違いを理解するために、RFCに掲載されている次のswitchの例を考えてみましょう。

switch (1) {
	case 0:
		$result = 'Foo';
		break;
	case 1:
		$result = 'Bar';
		break;
	case 2:
		$result = 'Baz';
		break;
}
 
echo $result;
//> Bar

次のmatch式で、上のコードと同じ結果を得ることができます。

echo match (1) {
	0 => 'Foo',
	1 => 'Bar',
	2 => 'Baz',
};
//> Bar

新しい match式を使用することの大きな利点は、switch文は緩やかな比較 (==) を使い、予期しない結果になる可能性がありますが、match式は厳密な比較(===)で比較します。

また、match式には複数のコンマ区切りの式を含めることができますので、構文がより簡潔になります(出典)。

$result = match ($x) {
	// This match arm:
	$a, $b, $c => 5,
	// Is equivalent to these three match arms:
	$a => 5,
	$b => 5,
	$c => 5,
};

その他の例やユースケースについては、match式 v2 のRFCおよび PHP のドキュメントを参照してください。

算術演算子とビット演算子における厳密な型チェック

以前のバージョンの PHP では、配列、リソース、オーバーロードされていないオブジェクトに算術演算子ビット演算子を適用することができました。しかし、その動作には一貫性がない場合がありました。

このRFCでは、Nikita Popov氏が簡単な例を挙げて、その動作がいかに変であるかを示しています。

var_dump([] % [42]);
// int(0)

配列やリソース、オーバーロードしていないオブジェクトに算術演算子やビット演算子を適用すると、異なる結果になることをNikita氏が次のように説明しています。

演算子 +, -, *, /, **

  • 配列の演算対象でエラー例外を発生させます。(両方の演算対象が配列の場合は+を除く)
  • リソースの演算対象を静かにリソースIDに変換して、整数化します。
  • オブジェクトの演算対象を整数に変換しますが、警告を発生させます。

演算子 %, <<, >>, &, |, ^:

  • 配列の演算対象を、空の場合は整数ゼロに、空でない場合は整数1に静かに変換します。
  • リソースの演算対象を静かにリソースIDに変換して、整数化します。
  • オブジェクトの演算対象を整数に変換しますが、警告を発生させます。

演算子~

  • 配列、リソース、オブジェクトの演算対象に対してエラーの例外を発生させます。

演算子 ++ および –

  • 演算対象が配列、リソース、オブジェクトの場合、静かに何もしません

PHP 8 では事情が変わり、すべての算術演算子およびビット演算子が適用された場合に同じ動作となります。

配列、リソース、オブジェクトの演算対象に対して TypeErrorの例外を発生させます。

新しいPHP関数

PHP 8は、いくつかの新しい関数をもたらします。

str_contains

PHP 8以前のバージョンでは、開発者が特定の文字列内で needleを検索したときに最もよく使用されたものはstrstrstrposでした。一方、どちらの関数も直感的でないもので、その使用が新人のPHP開発者にとって難しい場合があります。次の例をご覧ください。

$mystring = 'Managed WordPress Hosting';
$findme = 'WordPress';
$pos = strpos($mystring, $findme);

if ($pos !== false) {
	echo "The string has been found";
} else {
	echo "String not found";
}

上記の例では、2つの値が同じ型であるかどうかもチェックする!==比較演算子を使用しました。これにより、needleの位置が0の場合にエラーが発生しなくなります。

この関数は論理値 FALSE,を返す可能性がありますが、FALSEとして評価される値を返す可能性もあります。[…]この関数の返り値を調べるには ===演算子 を 使用してください

さらに、特定の文字列内の値を検索するヘルパー関数を提供するフレームワークが多いです。(例えば、Laravel Helpersのドキュメントをご参照ください。)

そこで、このRFCでは、文字列内を検索できる新しい関数str_containsの導入が提案されています。

str_contains ( string $haystack , string $needle ) : bool

使い方はかなり簡単です。str_containsは、$needle$haystackにあるかどうかを確認し、それに応じてtrueまたはfalseを返します。

したがって、str_containsのおかげで、次のコードを書くことができるようになります。

$mystring = 'Managed WordPress Hosting';
$findme   = 'WordPress';

if (str_contains($mystring, $findme)) {
	echo "The string has been found";
} else {
	echo "String not found";
}

上記は、より読みやすく、エラーが発生しにくいものです。(実行中のコードについてはこちらを参照)。
本記事の執筆時点で、str_containsは大文字と小文字を区別しますが、この特徴が将来的に変更される可能性があります。

str_containsの提案は43対9の投票で可決しました。

str_starts_with()およびstr_ends_with()

str_contains関数に加えて、str_starts_with 及びstr_ends_withの2つの新しい関数を使用しても、文字列内でneedleを検索ができます。

これらの新しい関数は、指定された文字列が別の文字列で始まるか/終わるかどうかをチェックします。

str_starts_with (string $haystack , string $needle) : bool
str_ends_with (string $haystack , string $needle) : bool

$needle$haystackよりも長い場合、どちらの関数もfalseを返します。

このRFCの作者であるWill Hudginsは次のように述べています。

str_starts_with 及びstr_ends_with機能は非常に一般的に必要とされるものであるため、SymfonyLaravelYiiFuelPHPまたはPhalconなどの多くの主要なPHPフレームワークで既にサポートされています

これらのおかげで、substrstrposなどの不十分で直感的でない関数の使用を回避できるようになります。どちらの関数でも大文字と小文字が区別されます。

$str = "WordPress";
if (str_starts_with($str, "Word")) echo "Found!";

if (str_starts_with($str, "word")) echo "Not found!";

実行中のコードについてはこちらをご参照ください。

このRFCは51対4の投票で可決しました。

get_debug_type

get_debug_typeは、変数の型を返す新しいPHP関数です。新しい関数はgettype 関数と非常によく似ていますが、get_debug_typeはネイティブの型名を返し、クラス名を解決します。

gettype()は型チェックにあまり役立たないため、これは便利な改善点です。

RFCには、新しいget_debug_type()関数とgettype()の違いをよりよく理解できるために役立つ2つの例が用意されています。最初の例は、実行中のgettypeを示しています。

$bar = [1,2,3];

if (!($bar instanceof Foo)) { 
	throw new TypeError('Expected ' .Foo::class .', got ' .(is_object($bar) ? get_class($bar) : gettype($bar)));
}

PHP 8では、代わりにget_debug_typeを使用できます。

if (!($bar instanceof Foo)) { 
	throw new TypeError('Expected ' .Foo::class .' got ' . get_debug_type($bar));
}

次の表は、get_debug_type及びgettypeの戻り値を示しています。

gettype() get_debug_type()
1 integer int
0.1 double float
true boolean bool
false boolean bool
null NULL null
“WordPress” string string
[1,2,3] array array
「Foo \ Bar」という名前のクラス object Foo\Bar
匿名クラス object class@anonymous

その他のRFC

PHP 8ので導入されるその他の承認された改善点の一覧を以下に示します。

  1. StringableインターフェースこのRFCでは、__to String() メソッドを実装するクラスに自動的に追加されるStringableインターフェースの導入が提案されています。ここでの主な目的は、string|Stringableunion型を使用することです。
  2. ext/domでのDOM Living Standardの新しいAPIこのRFCでは、新しいインターフェースとパブリックプロパティを導入することにより、現在の DOM Living StandardPHPのDOM拡張機能に組み込むことが提案されています。
  3. 静的な戻り型:PHP 8では、self型 とparent型の並びに、static型も戻り型として使用できるようになります。
  4. 可変構文の微調整このRFCは、PHPの可変構文の古くからの不整合をなくします。

PHP 8 のパフォーマンスベンチマーク

PHP 8がどのくらい速いのか気になる方のために、答えを用意しました。20のPHPプラットフォームや構成を対象に、7つの異なるPHPバージョン(5.6、7.0、7.1、7.2、7.3、8.0)をベンチマークしました。

WordPressやLaravelなど、PHPをサポートするほとんどのプラットフォームでPHP 8.0がチャンピオンでした。

上位プラットフォームのベンチマーク結果のまとめ
上位プラットフォームのベンチマーク結果のまとめ

PHP 8.0上のWordPressはPHP 7.4上よりも、1秒あたり18.4%多くのリクエストを処理できます。そしてLaravelも、PHP 7.3と比較して1秒間に8.5%多くリクエストを処理できます。

もし、あなたのウェブサイトやアプリがPHP 8.0に完全に対応しているのであれば、できるだけ早くサーバーの環境をPHP 8.0にアップデートした方が良いでしょう。あなたも、そしてあなたのユーザーも間違いなく、そのパフォーマンス上のメリットを享受できるでしょう。ただし、アップデートする前には、サイトを徹底的にテストしてください。

詳細なパフォーマンスデータ、その分析、またはきれいなグラフなどについては、当社のPHPベンチマークの記事をお読みください。

まとめ

うわぁ!本記事では、PHP 8で導入される最も興味深い最適化や新機能を取り上げました。最も期待されている新機能は、確かにジャストインタイム・コンパイラーですが、PHP 8にはそれ以外にも多くの機能が含まれるようです。

本記事を必ずブックマークしてください。🤓

次はあなたの番です。PHPの予定されている機能を試験する準備はできていますか?お好みの新機能は何ですか?下記のコメント欄でお知らせください。

Carlo Daniele Kinsta

ウェブデザインとフロントエンド開発をこよなく愛し、WordPress歴は10年以上。イタリアおよびヨーロッパの大学や教育機関とも共同研究を行う。WordPressに関する記事を何十件も執筆しており、イタリア国内外のウェブサイトや雑誌に掲載されている。詳しい仕事情報はXとLinkedInで公開中。