PHP 8.4は2024年11月21日にリリースされたバージョンで、プロパティフックや非対称プロパティ可視性といった構文レベルの追加が入りました。2026年10月時点の8.4系の最新パッチは8.4.27(2026年10月21日)で、未認証のリモートコード実行につながる脆弱性(通称PHP2Shell)を修正しています。バグ修正が入るアクティブサポートは2026年12月31日で終わります。本記事では8.4で何が変わったのかをPHP 8.4.24(CLI)で実行した結果とともに整理し、8.4.21以降のパッチで直った問題、8.3からの移行で先に直す非推奨、2027年以降も8.4に残るかどうかの判断までを扱います。
まとめ:PHP 8.4で押さえる6点
- 8.4系は8.4.27へ上げる。8.4.26以前には、8.4.27で直ったPHP2Shell(CVE-2026-109618・CVE-2026-109619、CVSS v4 9.2)が残っている。
- アクティブサポートは2026年12月31日まで、セキュリティ修正は2028年12月31日まで。2027年からはバグ修正が入らない。
- 構文の目玉はプロパティフック(get/setのみ、isset/unsetフックは存在しない)と非対称プロパティ可視性(
public private(set))。遅延オブジェクトはnewLazyGhost()とnewLazyProxy()の2方式。 - 配列検索は
array_find()・array_find_key()・array_any()・array_all()の4関数が追加。空配列に対するarray_all()はtrueを返す。 - 移行で最初に踏むのは暗黙のnullable型の非推奨。
string $x = nullは?string $x = nullへ書き換える。URLにセッションIDを載せる設定やtrigger_error()へのE_USER_ERRORも非推奨。 - 新規プロジェクトは8.4で始めず8.5を選ぶ(PHP 8.5の変更点と新機能)。PHP 8.6は2026年11月19日の一般公開を予定しているRC段階。
以下、パッチとサポート期限を先に確認し、そのあと各機能の書き方と実行結果を順に見ていきます。
PHP 8.4のサポート期限と8.4.27までのパッチ
新機能より先に確認したいのが、そのバージョンをいつまで使えるかです。php.netのサポート状況ページに記載されている日付は次のとおりです。
| バージョン | 初回リリース | アクティブサポート終了 | セキュリティ修正終了 |
|---|---|---|---|
| PHP 8.3 | 2023年11月23日 | 2025年12月31日(終了済み) | 2027年12月31日 |
| PHP 8.4 | 2024年11月21日 | 2026年12月31日 | 2028年12月31日 |
| PHP 8.5 | 2025年11月20日 | 2027年12月31日 | 2029年12月31日 |
アクティブサポート期間中はバグ修正とセキュリティ修正の両方が入り、それ以降はセキュリティ修正のみになります。php.netの定義では、セキュリティ修正のみの期間は定期リリースが無く、重大な問題が出たときだけ必要に応じて出ます。8.2や8.3を含む全系列の終了日はPHPのサポート期限一覧にまとめています。
8.4.21〜8.4.27で直ったセキュリティ問題
8.4系のパッチは4週ごとに出ています。php.netのChangeLogから、直近7本のセキュリティ修正を抜き出すと次のとおりです(公開日はリリース一覧の日付)。
| 版 | 公開日 | セキュリティ修正 | 代表的な内容 |
|---|---|---|---|
| 8.4.27 | 2026年10月21日 | 2件 | PHP2Shell(リモートコード実行) |
| 8.4.26 | 2026年9月24日 | 11件 | FPMのIPv6アクセス制限の回避 |
| 8.4.25 | 2026年8月27日 | 0件 | PCREの\C禁止など |
| 8.4.24 | 2026年7月30日 | 4件 | bccomp()の範囲外書き込み |
| 8.4.23 | 2026年7月2日 | 1件 | openssl_encrypt()のメモリ破壊 |
| 8.4.22 | 2026年6月4日 | 0件 | バグ修正のみ |
| 8.4.21 | 2026年5月7日 | 11件 | FPMステータス画面のXSS |
8.4.27の2件は、php.netが2026年10月21日のセキュリティ告知で「Webリクエストを処理するすべてのPHP」を対象に即時更新を求めたものです。PHP-FPM、mod_php、php-cgi、FrankenPHP、LiteSpeedが含まれ、mod_phpではアプリケーションのコードが動く前の未認証リクエストで攻撃が成立する実証があると書かれています。GitHubのセキュリティアドバイザリでは、GHSA-w8hw-w9fx-j79wがCVE-2026-109618、GHSA-6wmf-4rv5-p3rpがCVE-2026-109619で、どちらもCVSS v4で9.2です。影響の詳細と修正版以外の手当てはPHP2Shellとは:CVE-2026-109618・109619の影響範囲と修正版で扱っています。
8.4.27へ上げたあとも1つ設定が残ります。php.netは10月23日の追加告知で、次の定期リリースまでsession.upload_progress.enabled=0にするよう勧めています(CVE-2026-115560)。成立するのはsession.serialize_handlerをphp_binaryかphp_serializeにしたうえで、実行時にini_set()でphpへ切り替えるアプリケーションだけです。Windows版は10月26日に、同梱のBrotliを1.2.1へ更新した8.4.27-1が再配布されました。PHP本体の版は8.4.27のままなので、php -vでは区別できません。
稼働中のPHPの修正版判定とCLI・Web間の確認
確認で間違えやすいのは、コマンドラインのphp -vとWebサーバーが使うPHPが別物の場合です。Homebrewやディストリのパッケージで複数の版が入っていると、CLIだけ新しくPHP-FPMは古いままということが起きます。同じ判定をWeb経由でも実行します。
<?php
// 稼働中のPHPが8.4系の修正版(8.4.27以降)か判定する
$fixed = '8.4.27';
printf("%s (%s)\n", PHP_VERSION, PHP_SAPI);
var_dump(PHP_MAJOR_VERSION === 8 && PHP_MINOR_VERSION === 4);
var_dump(version_compare(PHP_VERSION, $fixed, '>='));
8.4.24 (cli)
bool(true)
bool(false)
このスクリプトを置いたURLへアクセスすれば、PHP_SAPIがfpm-fcgiやapache2handlerになり、Webで使われている版が分かります。確認が済んだらファイルは消します。ディストリのパッケージは版番号を上げずに修正を取り込むこと(バックポート)もあるため、パッケージで入れたPHPは各ディストリのセキュリティ情報で判定します。8.4系では、Debian 13がDSA-6574-1で8.4.27-1~deb13u1を、RHEL 10がRHSA-2026:87071で8.4.27-1.el10_2を出しており、Docker公式イメージのphp:8.4タグも8.4.27を指しています(2026年10月29日確認)。
パッチ更新でも挙動が変わる例が8.4.25にあります。ChangeLogの「Using \C in UTF-8 patterns is now forbidden」のとおり、UTF-8モード(u修飾子)の正規表現で1バイトを表す\Cが使えなくなりました。8.4.24ではpreg_match('/^\C/u', 'あ')が1を返し、「あ」の先頭1バイト(0xe3)だけを切り出します。マルチバイト文字を途中で割る書き方なので、使っていたら.かmb_substr()へ置き換えます。
いま8.4を選ぶ場面と8.5へ飛ばす場面
依存ライブラリと実行基盤が8.5に対応している新規プロジェクトでは、8.4ではなく8.5を選びます。8.4のバグ修正は2026年12月31日で止まるため、本番公開が2027年以降なら、公開時点で「セキュリティ修正のみ」の運用になります。本記事で扱うプロパティフックも非対称可視性も8.5にそのまま残っており、8.4限定の機能を諦める必要もありません。
8.6はphp.netのリリース計画で2026年11月19日の一般公開を予定しており、10月21日にRC4が出た段階です。依存ライブラリとフレームワークの8.6対応が揃うまで、新規案件の既定は8.5にしておくのが無難です。
既存の8.4環境は、8.4.27へ上げたうえで2027年のどこかで8.5へ移る計画を立てます。セキュリティ修正は2028年末まで続くので急ぐ必要はありませんが、2027年以降は8.4固有のバグが見つかっても直りません。8.4に残る判断が成立するのは、依存ライブラリやフレームワークの対応表が8.4止まりのときです。Laravelの要件はLaravelのバージョン確認方法とサポート期限で、WordPressで使っている場合の切り替え手順はWordPressのPHPバージョンアップ手順で整理しています。8.2や8.3から上げるなら、8.4で一度止めるより8.5まで通しで検証したほうが、回帰テストや依存関係の更新範囲は増える可能性がありますが、セキュリティサポートの終了日は1年先になります。社内で版上げの工数を確保できない場合は、システムマイグレーション・リプレイスとして外部に任せる選択もあります。
プロパティフック:getter/setterを畳む構文と制約
プロパティフックは、プロパティの読み書きに処理を差し込む構文です。RFCが導入したのはgetとsetの2つのみで、issetやunsetのフックは意図的に見送られています。
getフック・setフックの基本形と仮想プロパティ
フックを持つプロパティのうち、フック内で自分自身($this->プロパティ名)を参照しないものは、値の格納先そのものが作られません。これが仮想プロパティで、ReflectionProperty::isVirtual()で判別できます。
<?php
class Temperature
{
public function __construct(private float $celsius) {}
// 仮想プロパティ: 格納先を持たず、都度計算される
public float $fahrenheit {
get => $this->celsius * 9 / 5 + 32;
set (float $f) { $this->celsius = ($f - 32) * 5 / 9; }
}
// 格納先を持つプロパティ: setフック内の $this->label は生の格納先を指す
public string $label = 'room' {
set (string $v) {
if ($v === '') {
throw new InvalidArgumentException('label must not be empty');
}
$this->label = strtolower($v);
}
}
public function celsius(): float { return $this->celsius; }
}
$t = new Temperature(25.0);
var_dump($t->fahrenheit);
$t->fahrenheit = 212.0;
var_dump($t->celsius());
$t->label = 'Server Room';
var_dump($t->label);
var_dump((new ReflectionProperty(Temperature::class, 'fahrenheit'))->isVirtual());
var_dump((new ReflectionProperty(Temperature::class, 'label'))->isVirtual());
float(77)
float(100)
string(11) "server room"
bool(true)
bool(false)
setフックの中に書いた$this->labelは、フックを経由しない生の格納先への代入になります。ここを普通の代入と同じつもりで書くと無限再帰になる、という誤解が多い箇所ですが、フック内では格納先へ直接書き込まれるため再帰しません。一方の$fahrenheitは格納先を持たないので、$t->fahrenheitの読み取りは毎回計算が走ります。ループ中に元の値が変わらず、計算コストが問題になる場合は、ループの前で変数に保存して再利用します。
フックを書けない条件(static・readonly・isset/unset)
プロパティフックには適用できない対象があります。実際にエラーを出させると次のようになります。
<?php
// 以下の3つはそれぞれ別ファイルで実行した結果((1)はパースエラーのため単独で確認)
// (1) unsetフックは構文として存在しない
class A { public string $x { get => 'a'; unset { } } }
// PHP Parse error: syntax error, unexpected token "unset", expecting identifier
// (2) readonlyプロパティにはフックを付けられない
class B { public readonly string $x { get => 'b'; } }
// PHP Fatal error: Hooked properties cannot be readonly
// (3) 静的プロパティは対象外
class C { public static string $x { get => 'c'; } }
// PHP Fatal error: Cannot declare hooks for static property
読み取り専用の値を返したいだけなら、readonlyではなくgetフックだけを持つ仮想プロパティにします。フックのないreadonlyと、getフックのみの仮想プロパティは、外から書き込めない点では同じ振る舞いです。
インターフェースでのプロパティ要求
インターフェースがプロパティを要求できるようになったのも8.4の変更点です。{ get; }で読み取り可能、{ set; }で書き込み可能、両方書けば双方を要求します。
<?php
interface HasSlug
{
public string $slug { get; }
}
class Article implements HasSlug
{
public function __construct(private string $title) {}
public string $slug { get => strtolower(str_replace(' ', '-', $this->title)); }
}
$a = new Article('PHP 8.4 Property Hooks');
echo $a->slug, PHP_EOL;
try { $a->slug = 'x'; } catch (Error $e) { echo get_class($e), ': ', $e->getMessage(), PHP_EOL; }
php-8.4-property-hooks
Error: Property Article::$slug is read-only
実装側は$titleという別のプロパティから値を組み立てているだけで、$slugという格納先は存在しません。インターフェースが要求するのは「読める」ことであって、実体のフィールドを持つことではない、という点がメソッドによる契約との違いです。
非対称プロパティ可視性:読み取りと書き込みで別の可視性
非対称プロパティ可視性は、読み取りの可視性と書き込みの可視性を分けて宣言する機能です。public private(set)と書けば、外からは読めるがクラス内からしか書けないプロパティになります。
<?php
class Order
{
public private(set) string $status = 'pending';
public protected(set) int $total = 0;
public function pay(int $amount): void
{
$this->status = 'paid';
$this->total = $amount;
}
}
$o = new Order();
$o->pay(1200);
var_dump($o->status, $o->total);
try { $o->status = 'cancelled'; } catch (Error $e) { echo get_class($e), ': ', $e->getMessage(), PHP_EOL; }
string(4) "paid"
int(1200)
Error: Cannot modify private(set) property Order::$status from global scope
readonlyとの違いは、クラス内であれば何度でも書き換えられる点です。注文ステータスのように状態が段階的に進む値は、readonlyでは表現できずgetterメソッドを足すことになりますが、private(set)ならプロパティのまま外部からの書き込みだけを封じられます。書き込み側の可視性は読み取り側と同じか、より狭い必要があります。読み取りより広い指定はコンパイルエラーです。対象は型付きプロパティに限られ、型のないプロパティに付けるとProperty with asymmetric visibility must have typeで落ちます。
遅延オブジェクト:初期化を触られるまで遅らせる仕組み
遅延オブジェクトは、オブジェクトの初期化をプロパティへのアクセスなどが発生するまで先送りする機能です。オブジェクトの状態に触れないメソッドを呼ぶだけでは初期化されません。DIコンテナのように「登録はするが使われないかもしれない」オブジェクトを扱う場面を想定しています。生成にはリフレクションを使います。
newLazyGhostによる自己初期化
コンストラクタが重いクラスを用意し、初期化がいつ走るかを出力の順序で確かめます。
<?php
class ReportBuilder
{
public array $rows;
public function __construct(public string $sql)
{
echo "[init] heavy setup for {$sql}", PHP_EOL;
$this->rows = ['dummy'];
}
public function count(): int { return count($this->rows); }
}
$r = new ReflectionClass(ReportBuilder::class);
$ghost = $r->newLazyGhost(function (ReportBuilder $o): void {
$o->__construct('SELECT * FROM sales');
});
echo "created", PHP_EOL;
var_dump($r->isUninitializedLazyObject($ghost));
echo "touch ->count()", PHP_EOL;
var_dump($ghost->count());
var_dump($r->isUninitializedLazyObject($ghost));
created
bool(true)
touch ->count()
[init] heavy setup for SELECT * FROM sales
int(1)
bool(false)
[init]の出力がcreatedの直後ではなくtouch ->count()の後に現れている点が要点です。ゴースト方式はオブジェクト自身が後から自分を初期化するため、生成されたインスタンスは最初からReportBuilderそのものであり、instanceofもクラス名の比較も通常のインスタンスと同じ結果になります。
newLazyProxyとの使い分け
同じことをプロキシ方式で書くと、初期化子の戻り値が実インスタンスになります。
<?php
class Conn
{
public function __construct(public string $dsn) { echo "[connect] {$dsn}", PHP_EOL; }
}
$r = new ReflectionClass(Conn::class);
$proxy = $r->newLazyProxy(fn() => new Conn('sqlite::memory:'));
echo "proxy created", PHP_EOL;
echo $proxy->dsn, PHP_EOL;
proxy created
[connect] sqlite::memory:
sqlite::memory:
プロキシ方式は初期化子が実インスタンスを返し、以降のアクセスがそちらへ転送されます。生成物が別インスタンスになるため、オブジェクトの同一性(===)に依存したコードとは相性が悪い一方、コンストラクタを外から呼べないクラスや、生成手順をファクトリにまとめている場合に向きます。既存のコードへ後付けするならゴースト方式、生成をファクトリ関数に閉じ込めているならプロキシ方式です。同一性を見るコードがあるならゴースト一択です。
配列検索関数4種:array_find・array_find_key・array_any・array_all
条件に合う要素を探す処理は、これまでarray_filter()で絞ってからreset()やarray_key_first()を重ねる書き方が定番でした。8.4はこれを4つの関数に置き換えます。コールバックは($value, $key)の順で引数を受け取ります。
4関数の戻り値と見つからなかったときの挙動
4関数がそれぞれ何を返すか、該当なしのときに何が返るかを一度に確認します。
<?php
$users = [
['name' => 'sato', 'role' => 'admin', 'active' => true],
['name' => 'suzuki', 'role' => 'editor', 'active' => false],
['name' => 'takada', 'role' => 'editor', 'active' => true],
];
$isEditor = static fn(array $u): bool => $u['role'] === 'editor';
var_dump(array_find($users, $isEditor)['name']); // 最初に一致した「値」
var_dump(array_find_key($users, $isEditor)); // 最初に一致した「キー」
var_dump(array_any($users, static fn($u) => $u['role'] === 'admin'));
var_dump(array_all($users, static fn($u) => $u['active']));
var_dump(array_find($users, static fn($u) => $u['role'] === 'guest'));
var_dump(array_find_key($users, static fn($u) => $u['role'] === 'guest'));
string(6) "suzuki"
int(1)
bool(true)
bool(false)
NULL
NULL
array_find()は値、array_find_key()はキーを返し、どちらも該当なしでnullを返します。値としてnullやfalseを含みうる配列では、array_find()の戻り値だけで「見つかったか」を判定できません。存在確認が目的ならarray_any()、位置が必要ならarray_find_key()を選びます。戻り値の意味が違う関数です。
空配列でarray_allがtrueを返す落とし穴
件数がゼロのときの戻り値を先に押さえておきます。
<?php
var_dump(array_any([], static fn($v) => true));
var_dump(array_all([], static fn($v) => false));
bool(false)
bool(true)
空配列に対するarray_all()はtrue、array_any()はfalseです。数学的にはどちらも妥当ですが、「全件が検証済みか」をこの関数で判定していると、配列が空のときに検証をすり抜けます。入力が空になりうる箇所では、$items !== [] && array_all(...)のように件数の確認を先に置きます。
DOM刷新:Dom\HTMLDocumentとCSSセレクタ
8.4はDom名前空間にDom\HTMLDocumentとDom\XMLDocumentを追加しました。従来のDOMDocumentはそのまま残るので、既存コードを書き換える必要はありません。新APIの実利はCSSセレクタと文字コードの扱いにあります。
<?php
$html = '<div class="card"><p class="price">1,280円<span>税込</span><ul><li>在庫あり</ul></div>';
$doc = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);
echo $doc->querySelector('.card .price')->textContent, PHP_EOL;
echo $doc->querySelector('li')->closest('div')->getAttribute('class'), PHP_EOL;
echo $doc->querySelectorAll('li')->length, PHP_EOL;
echo $doc->saveHtml($doc->querySelector('.card')), PHP_EOL;
1,280円税込
card
1
<div class="card"><p class="price">1,280円<span>税込</span></p><ul><li>在庫あり</li></ul></div>
閉じていない<p>と<li>がHTML5の規則どおり補われ、querySelector()とclosest()がそのまま使えています。DOMDocument側にはquerySelector()が存在せず(method_exists()はfalse)、同じ抽出をDOMXPathで書き直す必要がありました。
文字コードの扱いも変わります。DOMDocument::loadHTML()はメタタグでの文字コード指定がないとUTF-8を前提にしないため、日本語を含むHTMLをそのまま渡すと壊れます。
<?php
$html = '<div class="card"><p class="price">1,280円</p></div>';
$old = new DOMDocument();
@$old->loadHTML($html, LIBXML_NOERROR); // meta charset の指定なし
echo 'old: ', $old->getElementsByTagName('p')->item(0)->textContent, PHP_EOL;
$new = Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);
echo 'new: ', $new->querySelector('.price')->textContent, PHP_EOL;
old: 1,280å
new: 1,280円
旧APIの出力には、このあとに表示できない制御文字が2つ続きます(掲載時に除去しました)。「円」のUTF-8バイト列をISO-8859-1として解釈した結果です。従来は<meta charset="utf-8">を先頭に足す回避策が必要でしたが、Dom\HTMLDocument::createFromString()はUTF-8として解釈します。HTML5の解析規則やCSSセレクタが必要な処理は、新APIへの移行候補です。既存XPathを維持する場合はDom\XPathを使えるため、抽出条件をすべてCSSセレクタへ書き直す必要はありません。
PHP 8.3から8.4への変更点と非推奨対応
移行作業で実際に手を動かすのは、新機能の採用より非推奨と破壊的変更の対応です。php-srcのUPGRADINGに載っている項目のうち、アプリケーション側で踏みやすいものを挙げます。
暗黙のnullable型の非推奨と書き換え
nullを含まない型を宣言した引数にデフォルト値nullを指定し、暗黙にnullを許容させる書き方が非推奨になりました。型宣言のない引数は、この非推奨の対象ではありません。
<?php
// 8.4 で Deprecated
function findUser(string $name = null): string { return $name ?? 'guest'; }
// Deprecated: findUser(): Implicitly marking parameter $name as nullable is deprecated,
// the explicit nullable type must be used instead
// 書き換え後(警告なし)
function findUserFixed(?string $name = null): string { return $name ?? 'guest'; }
掲載例は型の前に?を足せば修正できます。ただし、union型には|nullを追加し、後ろに必須引数が続く場合はデフォルト値nullの削除も必要です。一括で直すなら、文字列置換ではなく構文を解析するツールを使います。PHP-CS-Fixerのnullable_type_declaration_for_default_null_valueルールは、単一の型には?を、union型には|nullを付けます。件数が多くても一括で直せるため、8.4へ上げる前にこの1点だけ先に潰しておくとログが読みやすくなります。
セッション設定・lcg_value()・E_USER_ERRORの非推奨
アプリケーション本体より設定ファイルや古い共通処理に潜みやすいのがこのグループです。php-srcのUPGRADINGに載っている項目を、1本のスクリプトで順に踏みます。
<?php
// 8.4で非推奨になったセッション設定・定数・関数を順に踏む
ini_set('session.sid_length', '48');
ini_set('session.use_only_cookies', '0');
session_start();
var_dump(SID);
lcg_value();
trigger_error('stop', E_USER_ERROR);
Deprecated: ini_set(): session.sid_length INI setting is deprecated in /app/n2_depr.php on line 3
Deprecated: ini_set(): Disabling session.use_only_cookies INI setting is deprecated in /app/n2_depr.php on line 4
Deprecated: Constant SID is deprecated in /app/n2_depr.php on line 6
string(58) "PHPSESSID=847fe2481acc719044c1cf0003e5a8db6b5562614fd9e704"
Deprecated: Function lcg_value() is deprecated since 8.4, use \Random\Randomizer::getFloat() instead in /app/n2_depr.php on line 7
Deprecated: Passing E_USER_ERROR to trigger_error() is deprecated since 8.4, throw an exception or call exit with a string message instead in /app/n2_depr.php on line 8
Fatal error: stop in /app/n2_depr.php on line 8
実行はphp -d error_reporting=E_ALL -d display_errors=stderr n2_depr.phpで行い、表示上のパスは/appに置き換えています(セッションIDは実行ごとに変わります)。session.use_only_cookiesを0にする設定とSID定数は、CookieではなくURLにセッションIDを載せる運用のためのものです。session.use_trans_sid・session.trans_sid_tags・session.trans_sid_hosts・session.referer_checkの変更も同じく非推奨になりました。session.sid_lengthとsession.sid_bits_per_characterの変更も非推奨ですが、出力のPHPSESSID=に続くIDが指定どおり48文字になっているとおり、8.4では警告が出るだけで値はまだ効きます。lcg_value()は警告文のとおり\Random\Randomizer::getFloat()へ、E_USER_ERRORは例外の送出へ置き換えます。MySQLを使う環境ではmysqli_ping()とmysqli_refresh()も非推奨です。
移行前に確認する破壊的変更
| 項目 | 8.3まで | 8.4での変更 |
|---|---|---|
| opcache.jit | tracing(既定) | disable(既定) |
| opcache.jit_buffer_size | 0(既定) | 64M(既定) |
| E_STRICT定数 | 有効 | 非推奨・エラーレベルは削除 |
| exit / die | 言語構造 | 関数相当・型エラーを投げうる |
| PHP_DEBUG / PHP_ZTS | int | bool |
| password_hash のbcryptコスト | 10(既定) | 12(既定) |
| imap / oci8 / PDO_OCI / pspell | 本体同梱 | PECLへ分離(同梱終了) |
| dba_open / dba_popen / odbc_connect / odbc_pconnect / odbc_exec | resource | オブジェクト(is_resourceが不成立) |
ただし、どちらのバージョンでもJITは既定で無効のままです。影響例には、opcache.jit_buffer_sizeだけで有効化していた環境があります。逆に、opcache.jitだけを有効なモードに設定し、バッファサイズを既定値に任せていた環境では、8.4でJITが有効になる場合があります。8.3以前はopcache.jit_buffer_sizeにサイズを指定するだけでJITが有効になりましたが、8.4はopcache.jitがdisableに変わったため、この書き方の設定ファイルを持ち込むとJITが無効のまま動きます。有効化するにはopcache.jitにモードを明示します。OPcache自体の設定はOPcacheとは?読み方とPHP 8.5の変更点・設定・確認方法で扱っています。is_resource()による戻り値チェックも、DBAやODBCを使っていれば必ず落ちるので、falseやnullとの比較へ置き換えます。bcryptのコスト既定値が10から12へ上がった点も見落としがちです。ハッシュ計算の所要時間が約4倍になるため、ログイン処理のレスポンスを実測しておきます。imap・oci8・PDO_OCI・pspellはPECLへ分離されたので、これらを使っている環境は移行前にPECL版の導入可否を確認します。
静的解析での洗い出し
非推奨の検出は実行時の警告任せにせず、静的解析で先に洗い出すほうが確実です。実行時に発生する非推奨は、テストで実行しない経路では検出できないためです。ただし、暗黙のnullable型の警告はコンパイル時に発生し、関数を呼ばなくても検出できます。PHPStanやPsalmは対象PHPバージョンを指定して解析でき、暗黙のnullableのような構文レベルの問題はコードを動かさずに列挙できます。ツールの選び方はPsalmの使い方とPHPStanとの違いにまとめています。
小粒でも効く追加:#[\Deprecated]属性・new式の括弧省略・数値と文字列の新API
この章では、非推奨属性、new式とBCMath、丸めモード、マルチバイト文字列関数、PDOサブクラスの変更を扱います。
#[\Deprecated]属性による段階的な廃止
<?php
#[\Deprecated(message: "use formatPrice() instead", since: "2.3.0")]
function format_price(int $yen): string { return number_format($yen) . '円'; }
echo format_price(1280), PHP_EOL;
Deprecated: Function format_price() is deprecated since 2.3.0, use formatPrice() instead in /private/tmp/demo/legacy.php on line 5
1,280円
#[\Deprecated]属性を付けた関数やメソッドを呼ぶと、PHP自身の非推奨と同じ形式でE_USER_DEPRECATEDが出ます。docblockの@deprecatedと違い実行時に検出できるため、社内ライブラリの段階的な廃止に使えます。messageとsinceはどちらも省略できます。
new式の外側括弧の省略とBcMath\Number
省略できるのは式全体を囲む括弧で、コンストラクタ引数の括弧は従来どおり必要です。
<?php
class Cfg
{
public function __construct(private array $v) {}
public function get(string $k): ?string { return $this->v[$k] ?? null; }
}
// 8.3までは (new Cfg([...]))->get('env') と括弧が必要だった
echo new Cfg(['env' => 'prod'])->get('env'), PHP_EOL;
$a = new BcMath\Number('0.1');
$b = new BcMath\Number('0.2');
var_dump((string)($a + $b), ($a + $b) == new BcMath\Number('0.3'));
var_dump(bcround('2.345', 2), bcceil('2.1'), bcfloor('-2.1'), bcdivmod('17', '5'));
prod
string(3) "0.3"
bool(true)
string(4) "2.35"
string(1) "3"
string(2) "-3"
array(2) {
[0]=>
string(1) "3"
[1]=>
string(1) "2"
}
コンストラクタ引数付きのnew式がそのままメソッド呼び出しにつながるようになり、外側の括弧が不要になりました。BcMath\Numberは演算子オーバーロードを備えた値オブジェクトで、0.1 + 0.2が0.3と等しくなる点が浮動小数点数との違いです。金額計算でbcadd()の文字列引数を積み重ねていた処理は、この形に置き換えると読みやすくなります。bcround()・bcceil()・bcfloor()・bcdivmod()も8.4での追加です。
round()の丸めモード指定
8.3までのround()でも、PHP_ROUND_HALF_EVENなどの定数で4種類の丸めモードを指定できました。8.4はRoundingMode enumで指定できるようになり、ゼロ方向・ゼロから遠い方向・負の無限大方向・正の無限大方向への丸めが加わりました。
<?php
// モード省略時の既定動作。8.3以前も第3引数で丸め方を指定できる
var_dump(round(2.5), round(3.5));
// 8.4 は RoundingMode enum で丸め方を選べる
var_dump(round(2.5, 0, RoundingMode::HalfEven));
var_dump(round(3.5, 0, RoundingMode::HalfEven));
var_dump(round(-2.5, 0, RoundingMode::TowardsZero));
var_dump(round(2.1, 0, RoundingMode::PositiveInfinity));
float(3)
float(4)
float(2)
float(4)
float(-2)
float(3)
用意されている定数はHalfAwayFromZero・HalfTowardsZero・HalfEven・HalfOdd・TowardsZero・AwayFromZero・NegativeInfinity・PositiveInfinityの8つです。会計処理で使う銀行家丸め(HalfEven)を自前で実装していたなら、その関数は不要になります。
マルチバイト対応のtrim・ucfirst
日本語を扱う実装で効くのがこの追加です。trim()は全角スペースを落としませんが、mb_trim()は落とします。
<?php
var_dump(mb_trim(" 全角スペース混じり "));
var_dump(mb_ucfirst('php 8.4'));
var_dump(function_exists('mb_ltrim'), function_exists('mb_rtrim'), function_exists('mb_lcfirst'));
string(27) "全角スペース混じり"
string(7) "Php 8.4"
bool(true)
bool(true)
bool(true)
フォーム入力の前後にある全角スペースをpreg_replace()で削っていた処理は、mb_trim()に置き換えられます。mb_ltrim()・mb_rtrim()・mb_lcfirst()も同時に入りました。
PDOのドライバ別サブクラス
接続にPdo\Sqlite::connect()のような静的メソッドを使うと、ドライバ固有のサブクラスが返ります。
<?php
$pdo = Pdo\Sqlite::connect('sqlite::memory:');
var_dump(get_class($pdo));
var_dump(get_class(new PDO('sqlite::memory:')));
string(10) "Pdo\Sqlite"
string(3) "PDO"
従来のnew PDO()はPDOのままなので、既存コードの挙動は変わりません。Pdo\Sqlite・Pdo\Mysql・Pdo\Pgsqlなどにはドライバ固有のメソッドが載るため、ドライバ判定のifを減らせます。
よくある質問
PHP 8.4はいつまでサポートされますか?
アクティブサポート(バグ修正とセキュリティ修正)は2026年12月31日まで、セキュリティ修正のみの期間は2028年12月31日までです。2027年1月以降はバグ修正が入らず、重大な脆弱性への修正だけが必要に応じて出ます。
PHP 8.4系はどのパッチまで上げればよいですか?
2026年10月29日時点では8.4.27(2026年10月21日)です。8.4.26以前にはPHP2Shell(CVE-2026-109618・CVE-2026-109619)が残っています。8.4系のパッチはアクティブサポート中は4週ごとに出るため、参照する時点の版はphp.netのダウンロードページで確認してください。8.4系ではなくPHP 8.5の変更点を探している場合はPHP 8.5の変更点と新機能が対象です。
PHP 8.3と8.4の違いは何ですか?
書き方が変わるのはプロパティフック、非対称プロパティ可視性、new式の外側括弧の省略です。関数ではarray_find系4関数、mb_trim()、round()の丸めモード、BcMath\Numberが加わりました。移行で影響が大きいのは暗黙のnullable型の非推奨と、opcache.jitの既定値がdisableに変わった点です。サポート期限は8.3のアクティブサポートが2025年末に終わっているのに対し、8.4は2026年末までです。
PHP 8.3から8.4へ上げるとき、最初に直すのはどこですか?
暗黙のnullable型(string $x = null)の非推奨です。件数が多い代わりに?string $x = nullへの機械的な置換で済みます。次に確認するのはopcache.jitの既定値変更と、is_resource()でDBA・ODBCの戻り値を判定している箇所です。
array_allは空配列で何を返しますか?
trueを返します。条件を満たさない要素が1つも無いためです。反対にarray_any()は空配列でfalseを返します。「全件が検証済みか」の判定に使うと空配列で検証をすり抜けるので、$items !== [] && array_all(...)のように件数の確認を先に置きます。