PHP 8.4新機能の実用ポイント—エンジニアが移行前に知っておきたい変更点

Published on: | Last updated:

デプロイ前の夜にさ、PHPのバージョンだけ上げる予定だったのに、テストで細かい警告がじわじわ出てきて、結局ログとにらめっこ……あるよね。しかも「動いてるからいいか」で放置したやつが、翌朝いちばん痛い顔して戻ってくる。

PHP 8.4は型と実行時チェックを強めてバグの芽を早めに鳴らす

PHP 8.4はZend Engine(PHPの実行エンジン)最適化と、typed class constants(型付きクラス定数)やproperty hooks(プロパティの読み書きフック)で、型安全性と可読性をまとめて押し上げる。

  • 速度まわり:再帰・配列・プロパティアクセス・JITの地味な改善
  • 入力チェック:json_validate()でJSON判定が一発
  • 設計:型付き定数、readonly付きプロパティ昇格でクラスが締まる
  • 事故防止:未定義プロパティへのunset()がWarning
  • 日常:配列ユーティリティarray_find系が増えてループが減る

この手の更新って、派手な機能より「後から効く」やつが多い。体感はジワジワ。だけど、チーム開発だとジワジワがいちばん強い。

静かに効くんだよね。

PHP 8.4の変更点をざっくり俯瞰する図
PHP 8.4の変更点をざっくり俯瞰する図

速度の話はZend EngineとJITの地味アップデートが中心

PHP 8.4の性能改善はZend Engineの最適化で、再帰のスタック効率やプロパティアクセス、配列操作、JIT(Just-in-Timeコンパイル:実行時に機械語最適化する仕組み)の挙動が手堅く整う。

「ベンチで爆速」みたいな話じゃなくて、高負荷のREST APIとか、ECとか、CMSみたいな“毎秒ちょっとずつ”が積み重なる場所で効いてくるやつ。

で、こういう話をすると「じゃあJITオンにしとけば?」ってなるんだけど、そこは環境とコード次第。速くなる箇所と、ならない箇所がある。結局、プロファイル取って、熱いところだけ触るのが近道だったりする。

眠いときにJITの話し始めると、だいたい迷子になる。ほんと。

json_validateはJSONの入り口チェックを読みやすくする

json_validate()は文字列が正しいJSONかを判定するネイティブ関数で、json_decode()json_last_error()の組み合わせを置き換えられる。

APIから来るペイロードとか、フォームの隠しフィールドとか、外部連携の「相手が何を投げてくるか分からん」やつに、まず置く。

$json = '{"name":"Andrea","role":"developer"}';

if (json_validate($json)) {
    echo "Valid JSON";
} else {
    echo "Invalid JSON";
}

昔の書き方だと、デコードして、エラー見て、でも戻り値の扱いが紛らわしくて、みたいな。判定の意図がそのままコードになるのは助かる。

あ、ここで余談。JSONって「壊れてる」より「一見正しそうで中身が期待と違う」の方が厄介。だから、validateの次はスキーマ検証とか、最低でもキー存在チェックに行くのが現実的。

話戻す。

型付きクラス定数で設定値とドメインのズレを早めに止める

typed class constantsは、クラス定数に型を宣言できる機能で、静的解析と実行時の意図ズレを減らす。

コンフィグとか、ドメインの固定値とか、「ここは変えない」って決めた場所ほど、型があると安心する。

class Config {
    public const string APP_ENV = 'production';
    public const int TIMEOUT = 30;
}

これ、地味だけど効く。後から誰かがTIMEOUTに文字列突っ込んで、「え、通ってる…?」みたいなやつ。静的解析ツールが拾いやすくなる。

正直、チームで回してると「説明不要なコード」ってご褒美なんだよね。レビューが早い。寝れる。

unsetのWarningと引数チェック強化はレガシー掃除の合図

PHP 8.4は未定義プロパティに対するunset()でWarningを出し、内部関数の引数個数チェックもより厳密にして、曖昧な呼び出しを実行時に止めやすくする。

class User {
    public string $name;
}

$user = new User();
unset($user->email); // Warning: Undefined property

これね、最初はうるさい。うるさいんだけど、沈黙するバグよりマシ。大きいコードベースほど、「どこかで作られた前提」が腐ってることがあるから。

strpos("example", "e", 0, "unexpected");

内部関数の引数が多すぎる/少なすぎる、みたいなやつも、エラーが分かりやすくなる。レガシーが多い現場ほど、アップグレード時にここが一気に表に出る。

しんどい。けど、後で効く。

アップグレード時に引っかかりやすいポイントの流れ
アップグレード時に引っかかりやすいポイントの流れ

readonly付きプロパティ昇格はDTOを短くして壊れにくくする

PHP 8.4はコンストラクタのプロパティ昇格でreadonlyを併用でき、DTO(Data Transfer Object:データ運搬用の不変オブジェクト)や設定クラスを短く安全に書ける。

class Product {
    public function __construct(
        public readonly string $name,
        public readonly float $price
    ) {}
}

「不変」に寄せたい設計だと、ここが一気に楽になる。ボイラープレート減るし、あとから値を書き換えられないのが分かりやすい。

DTOって、増えると地味に腕が死ぬからね。ファイル開くたびに同じコードが並んでるやつ。

Randomizerと日付時刻の改善はテストと境界条件で効く

PHP 8.4はRandom\Randomizer(乱数生成をオブジェクトで扱う仕組み)の安定性を上げ、DateTimeImmutableDateTimeInterfaceのマイクロ秒やタイムゾーン周りの扱いも堅くする。

乱数のほう。昔からrand()とかmt_rand()で済ませがちだけど、テスト書くときに「再現できない」って詰む。シード固定できるの、助かる。

use Random\Engine\Mt19937;
use Random\Randomizer;

$engine = new Mt19937(1234); // Seeded for repeatability
$randomizer = new Randomizer($engine);

echo $randomizer->getInt(1, 10); // Deterministic output with seed

日付時刻は、地味に事故が多い。マイクロ秒とかタイムゾーンって、普段は気にしないのに、決済の締めとかログ突合で突然殴ってくる。

DateTime::createFromInterface()が堅くなるのも、こういう「端っこ」で効くやつ。

日本だと、業務システムは年度や締め日が絡むし、タイムゾーンはJST固定で油断しがち。海外向けの機能が混ざった瞬間、ぐちゃっとなる。

ほんと、なる。

FFIと拡張の互換性は触る人だけ深く刺さる

PHP 8.4はFFI(Foreign Function Interface:PHPからCの関数や構造体を触る仕組み)の型安全性やメモリアラインメントを改善し、拡張モジュールの互換性も安定寄りに調整する。

FFIは、使う人は使う。暗号、画像処理、ネイティブライブラリ連携。ここが不安定だとセグフォで死ぬから、改善はありがたい。

拡張の互換性も、現場だとわりと切実。Xdebug、Redis、あと社内の謎モジュール。ビルドやロードのところでコケると、原因切り分けが長い夜になる。

長い。夜が。

property hooksと非対称な可視性はオブジェクト設計の表現力を増やす

property hooksはプロパティの読み書きに処理を挟める仕組みで、asymmetric visibility(非対称可視性)は同じプロパティに読み取りと書き込みで別のアクセスレベルを付けられる。

ここはちょっと、設計の匂いが強い。使いどころを間違えると魔術になる。でも、うまく使うと、冗長なgetter/setterを減らせる。

class User {
    private array $data = [];

    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }

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

    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }
}

原文の例はマジックメソッド寄りだけど、言いたいことは近い。動的スキーマっぽいモデルとか、ORM的な層で「読み書きの挙動を揃える」用途で出番がある。

非対称可視性の方は、読み取りはpublic、書き込みはprivate、みたいなやつ。公開APIで「見せるけど触らせない」って要件、あるでしょ。あれをプロパティで表現できるのは、わりと気持ちいい。

ただ、導入するときはチームの合意が要る。読めない人が出ると、速度より先に不満が出る。

配列の新関数はループを減らして意図を前に出す

PHP 8.4はarray_findarray_find_keyarray_anyarray_allを追加し、条件探索や真偽判定を関数で表現しやすくする。

$items = [1, 3, 5, 7, 9];

$hasEven = array_any($items, fn($n) => $n % 2 === 0); // false
$allPositive = array_all($items, fn($n) => $n > 0);   // true

ループを消すのが目的というより、「何がしたいか」を前に出す感じ。読み手が楽になる。レビューも楽。

ただし、何でもかんでも関数にすると、デバッグで追いにくくなる瞬間もある。そこは好みとチーム文化。

非推奨は放置すると次のメジャーで刺さる

PHP 8.4はmb_strrchr()$before_needle = trueや、curl_setopt()でのCURLINFO_HEADER_OUTなどを非推奨にして、将来の整理に備える。

このへんは「今すぐ壊れる」じゃなくて、次の更新で壊れる前フリ。だから、警告が出たらチケット切って潰すのが結局いちばん安い。

自分用のアップグレード自検リスト

スクショ用に置いとく。こういうの、当日焦ると抜けるから。

  • テストを回して、Warning/Deprecatedをログで拾う
  • unset($obj->undef)が潜んでないか検索する
  • 内部関数の引数ミスっぽい呼び出しを静的解析で洗う
  • JSON入口でjson_validate()に寄せられる箇所を決める
  • DTO/設定クラスはreadonly昇格で短くできるか見る
  • 乱数を使うテストはRandom\Randomizerへ寄せる計画を立てる
  • 拡張(Xdebug/Redis/社内モジュール)をステージングで先に起動確認
  • 本番はdisplay_errorsをオフ、開発はerror_reportingを厳しめにする
自検リストのミニ図解
自検リストのミニ図解

結局どこから触ると平和かって話

PHP 8.4は「すぐ嬉しい関数」と「後から守ってくれる厳格さ」が同居していて、移行の価値が説明しやすいリリースだ。

個人的なおすすめ順は、まずjson_validate()と配列関数で“読みやすさ貯金”を作る。次にWarning/Deprecatedを潰して、型付き定数とreadonlyで設計を締める。性能は最後に、プロファイル見てから。

まあ、理想はね。現実は、期限が先に来る。

でも、せめて警告だけは放置しない。未来の自分が泣くから。

最後に、純共有。公式の変更点を追うなら、探すキーワードはこれがいちばん早い:「PHP 8.4 RFC」

Related to this topic:

Comments