PHP 8.0の下位互換性とLaravelでの対応状況を調べる

PHPerのみなさんこんにちは。去る2020年11月26日、 [PHP 8.0] (https://www.php.net/releases/8.0/en.php) がリリースされましたね。

今回はそんなPHP 8.0の下位互換性について調べてみました。Webアプリケーション開発に利用されることの多いフレームワーク[Laravel](https://github.com/laravel/laravel)でのサポート状況についても調べています。

なお、記事を書いているのは2020年12月初旬であり、この記事に書かれている内容はその時点のものです。

## PHP 8.0の後方互換性

PHP8.0の後方互換性についてはPHP公式サイトの [下位互換性の無い変更点](https://www.php.net/manual/ja/migration80.incompatible.php
) のページにまとまっているので、気になった変更点について拾い上げます。

### PHPコア
#### 文字列と数値の比較

> (厳密でないやり方で)数値と非数値文字列を比較する場合、 数値を文字列にキャストし、文字列と比較するようになりました。 数値と数値形式の文字列の比較は、以前と同じ振る舞いをします。 注意すべきなのは、これによって、 0 == "not-a-number" が FALSE と見なされるようになったことです。


|比較|変更前|変更後
|---|-----|-----
|0 == "0"|	TRUE|	TRUE
|0 == "0.0"|	TRUE|	TRUE
|0 == "foo"|	TRUE|	FALSE
|0 == ""|	TRUE|	FALSE
|42 == " 42"|	TRUE|	TRUE
|42 == "42foo"|	TRUE|	FALSE

よくネタにされる比較にメスが入りましたね。今回非互換となっている比較のうち、 `0 == "foo"` はさすがに無さそうですが、 `0 == ""` や `42 == "42foo"` で`TRUE` を期待するようなコードはどこかに埋まっているかもしれないですね。心当たりのある方は震えてください。

#### その他の下位互換性のない変更

> match が予約語になりました。

[match式] (https://www.php.net/manual/ja/control-structures.match.php) の影響ですね。

PHPのクラス名は大文字小文字を区別しないので、 `Match` というクラスやトレイト、インターフェイスを作っているエラーになるはずです。簡単なコードで実際に検証してみます。


```php:hello.php
<?php

class Match {
    public function hello()
    {
        echo "hello";
    }
}
(new Match())->hello();
```


PHP 7での実行結果。

```bash:php7での実行結果
hello
```

PHP 8での実行結果。

```bash:php8での実行結果
PHP Parse error:  syntax error, unexpected token "match", expecting identifier in hello.php

Parse error: syntax error, unexpected token "match", expecting identifier in hello.php
```

パース時にエラーが出るようになっていますね。これは大きな非互換性。私は震えています。

> クラス名と同じ名前のメソッドは、コンストラクタと解釈されなくなりました。 __construct() メソッドを代わりに使って下さい。

PHP 5以降のコードは `__construct()` にコンストラクタの処理を記載しているはずなので、ここ数年以内に書かれたコードであれば問題無さそうです。PHP 4時代から生き残っている猛者コードはやばいかもしれません。

> 静的でないメソッドを静的に呼ぶことができる機能が削除されました。 静的でないメソッドをクラス名を使ってチェックした場合、 is_callable() は失敗します。 (オブジェクトのインスタンスを使ってチェックしなければいけません)

ふつうのアプリケーションコードでは影響無さそうですね。ライブラリ等には影響が出るかもしれません。

> (real) と (unset) キャストが削除されました。

今回調べていて初めて存在を知ったキャストです。 それぞれ `float` と `NULL` へのキャストとのことです。後者は用途が思いつかない :thinking: 

> create_function() 関数は削除されました。 無名関数が代わりに使えます。

一部コードにはまだ残っていそうなので要注意。PHP 5.3で無名関数が来たときの喜びと変数をキャプチャするには `use` しなければいけない事を知った時の悲しみは未だに覚えています。R.I.P.

> オブジェクトに対して array_key_exists() 関数を使える機能は削除されました。 isset() または property_exists() を代わりに使えます。

これもふつうのアプリケーションコードでは影響無さそうですね。ライブラリ等には影響が出るかもしれません。

> デフォルトの error_reporting のレベルは E_ALL になりました。 これより前のバージョンでは、 E_ALL から E_NOTICE と E_DEPRECATED が除かれていました。

どうせ `E_ALL` か `0` にするのでこれはよいですね。

> display_startup_errors は、 デフォルトで有効になりました。

本番環境構築時要注意ですね。諸々の変更を鑑みるに、php.iniのデフォルト設定が開発向けになったように見えますね。

> 親クラスがないクラスの内部で parent を使うと、 致命的なコンパイルエラーが発生するようになりました。

いいね！

> @ 演算子は、致命的なエラー (E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR, E_PARSE) を隠さなくなりました。 @ を使う時に、 error_reporting が 0 であることを期待しているエラーハンドラは、 代わりにマスクチェックを調整すべきです:

> ```
<?php
// こういうエラーハンドラは
function my_error_handler($err_no, $err_msg, $filename, $linenum) {
    if (error_reporting() == 0) {
        return; // Silenced
    }
    // ...
}
// 以下に置き換えましょう
function my_error_handler($err_no, $err_msg, $filename, $linenum) {
    if (!(error_reporting() & $err_no)) {
        return; // Silenced
    }
    // ...
}
?>
```

> さらに、実運用環境で表示されていなかった、 情報のリークに繋がるエラーメッセージにも注意を払うべきです。 エラーのロギングと併せて、 display_errors=Off となっていることを確認するようにして下さい

今でも@がついているサンプルコードを結構見かけるので要注意です。私も以前はよく@つけてました。昔@を使ってやんちゃしてた人は震えてください。

> 非互換なメソッドのシグナチャによる継承エラー(リスコフの置換原則違反)については、 常に致命的なエラーが生成されるようになりました。 これより前のバージョンでは、警告が生成される場合がありました。

継承時のエラーが把握しやすくなったのは良い変更ですね。

> 連結演算子の優先順位は、減算の演算子と同様に、 ビットシフトや加算の演算子に関連するように変更されました。

> ```
<?php
echo "Sum: " . $a + $b;
// 上記は、以前のバージョンでは以下のように解釈されていました:
echo ("Sum: " . $a) + $b;
// PHP 8.0.0 からは、以下のように解釈されます:
echo "Sum:" . ($a + $b);
?>
```

ごーーくまれに前者のコードを書いてしまうので、そのままでも期待する動作になるのは良いことです。とはいえ演算子の優先順位を覚えているオタクばかりではないので、これまで通り、後ろにかかれている演算子を先に処理したい場合は `()` をつけたほうが読みやすそうですね。

> 例外がキャッチされなかった場合、 "クリーンなシャットダウン" が行われます。 これは、例外がキャッチされなかった後、 デストラクタが呼ばれるということです。

私はPHPではデストラクタ自体あまり使いませんが、何はともあれ後処理がちゃんと動く可能性が増えるのはいことですね。

> 名前空間に含まれる名前には、 ホワイトスペースを含めることができなくなりました。 つまり、Foo\Bar は名前空間の名前として認識されますが Foo \ Bar は認識されなくなったということです。 逆に、予約語のキーワードは名前空間の一部として許されるようになりました。 これによって、コードの解釈が変わるかもしれません。 new\x は constant('new\x') と同じですが、 new \x() とは異なります。

今まで出来たのが驚きである。

> 三項演算子をネストする場合、明示的に括弧が必要になりました。

激ヤバいコードに遭遇して悩んでいた方にとっては朗報ですね。とはいえ激ヤバいコードがふつうのヤバいコードになるくらいだと思います。

> マジックメソッドは引数と戻り値の型を持ち、 宣言された場合はチェックが行われるようになりました。 シグナチャは次の一覧と一致させるべきです:

> ```
__call(string $name, array $arguments): mixed
__callStatic(string $name, array $arguments): mixed
__clone(): void
__debugInfo(): ?array
__get(string $name): mixed
__invoke(mixed $arguments): mixed
__isset(string $name): bool
__serialize(): array
__set(string $name, mixed $value): void
__set_state(array $properties): object
__sleep(): array
__unserialize(array $data): void
__unset(string $name): void
__wakeup(): void
```

これもふつうのアプリケーションコードでは影響無いような気がします、ライブラリ等には影響が出るかもしれません。

ちなみに私はアプリケーションコードにマジックメソッドを使ったクラスを数年前に書いた記憶があり盛大に震えています。


### リフレクション

> メソッドシグナチャに関する変更

> ```
ReflectionClass::newInstance($args)
ReflectionFunction::invoke($args)
ReflectionMethod::invoke($object, $args)
```

> は、以下のように変更されました:

> ```
ReflectionClass::newInstance(...$args)
ReflectionFunction::invoke(...$args)
ReflectionMethod::invoke($object, ...$args)
```

> PHP 7 と PHP 8 の間で互換性を保たなければならないコードは、 両方のバージョンで互換性を取るために、以下のようなシグナチャが使えます:

> ```
ReflectionClass::newInstance($arg = null, ...$args)
ReflectionFunction::invoke($arg = null, ...$args)
ReflectionMethod::invoke($object, $arg = null, ...$args)
```

互換性のあるシグネチャがあるのでまぁ大丈夫そうです。

### 標準ライブラリ

> strpos(), strrpos(), stripos(), strripos(), strstr(), strchr(), strrchr(), stristr() 関数の needle 引数は常に文字列として解釈されるようになりました。 これより前のバージョンでは、 文字列でない引数は ASCII コードポイントと解釈されていました。 chr() 関数を明示的に呼ぶことで、 以前の振る舞いを復元できます。

一部のおしゃれなコードに影響がありそうです。ちなみに私はそんなおしゃれなコード書いたこと無いので平気です。

PHP 8.0の互換性についてはここまでとします。

## LaravelでのPHP 8サポート状況

LaravelでのPHP 8サポート状況についてです。

公式ブログで[PHP 8サポート状況についての記事](https://blog.laravel.com/laravel-php-8-support)が出ていますね。Laravel本体及びファーストパーティーパッケージの一部はPHP8に対応しているとのことです。圧倒的感謝。

なお、PHP 8に対応する際はFaker及びPHPUnitの最小バージョン指定も上げる必要もあるそうです。

> There are also a couple of commonly used dependencies you'll need to update in your composer.json file:
> 
> * PHP to php:^8.0
> * Faker to fakerphp/faker:^1.9.1
> * PHPUnit to phpunit/phpunit:^9.3

### PHP8で動作検証

私が参画中しているプロジェクトの中にちょうど PHP 7とLaravel 8で開発しているものがあるので、PHP 8に上げて動作するか検証してみました。以下の手順で確認しています。

* 開発としてDockerを使用しているので、Dockerfileのベースイメージを `php:8.0-fpm-alpine` へ変更しました
* デバッグ用にはXdebugも使用していますが、がPHP 8サポートす xdebug 3.0.0からなので、こちらも併せてバージョンを上げました
* Laravelの公式ブログの記載に則り `composer.json` の修正と `composer update` の実施を行いました
* `composer`にはDocker Hubのイメージを使っていたのですが、 `composer:2.0` のベースイメージがPHP 7だったので、開発マシンのMacにbrewで `php@8.0` を入れて `composer update` しました

が、使用しているcomposerパッケージのいくつかがPHP 8に対応しておらず `composer update` が失敗しました。各パッケージにPHP 8対応のPR投げるか正座しながら待つかすることにします。


### まとめ

今からLaravel 8.xプロジェクトを開始するのであればPHP 8は選択肢に入りますが、PHP 8に対応していないcomposerパッケージも結構あるので使用したいパッケージがある場合は調査すべきです。


以上。