こんにちは。大規模なWebシステムの設計やパフォーマンスチューニングで、日々コードと向き合っていますか?
他のモダンな言語、例えばJavaやGo、TypeScriptなどを経験してきた優秀なエンジニアほど、PHPのコードを書くときに「クラス定数(`const`)と静的プロパティ(`public static`)のどちらを使うべきか」という疑問にぶつかり、ふと立ち止まることがありますよね。「どちらもインスタンスを作らずにクラス名からアクセスできるのに、内部の挙動はどう違うんだろう?」と。
ネットの海を検索すると、「定数の方がなんとなく速いらしい」「静的プロパティは書き換えられるから遅い」といった、ふんわりとした噂話は見つかります。でも、私たちアーキテクトが知りたいのはそこじゃありませんよね。
「その違いが、PHPのエンジン(Zend VM)のメモリ空間や、JITコンパイラのコード生成、そして1リクエストのライフサイクルにおいて、具体的にどのようなコストを生んでいるのか」
ここを理解できれば、PHPの裏側がまるでスケスケのガラス細工のように美しく見えてきますよ。今回は、PHP 8.xの心臓部まで潜り込み、定数と静的プロパティの運命的な違いを一緒に紐解いていきましょう。
—
1. Zend VMのメモリ空間:HashTableとシンボルテーブルの裏側
まず、PHPのコードが実行されるとき、メモリ上で何が起きているのかをイメージしてみましょう。
PHP(Zend VM)の世界では、クラスや関数、変数といったあらゆるシンボルは、`HashTable`(ハッシュテーブル)という構造体で管理されています。C言語のレベルで最適化されたこのハッシュテーブルは、文字列のキー(変数名や定数名など)を受け取り、その実体(`zval`構造体)へのポインタを高速に引くためのものです。
しかし、「クラス定数」と「静的プロパティ」は、このハッシュテーブルの中での“扱われ方”が根本的に違います。
クラス定数:コンパイル時に確定する「不変の埋め込み値」
クラス定数は、その名の通り「定数」です。ソースコードがパースされ、オペコード(Opcode)にコンパイルされる段階で、その値は完全に固定されます。
Zendエンジンにとって、クラス定数はクラスのエントリー(`zend_class_entry`)が持つ定数用のハッシュテーブルに、キーと`zval`(値)のペアとしてガッチリと固定配置されます。
特筆すべきは、PHP 8.x以降、JITコンパイラが有効な環境において、プリミティブな定数(スカラー値)へのアクセスは、メモリ上のハッシュテーブルルックアップすらスキップされ、生成されるネイティブ機械語に「即値(Immediate Value)」として直接埋め込まれる点です。
静的プロパティ:実行時に解決される「可変の変数」
一方で、静的プロパティは「プロパティ」です。名前は静的(Static)であっても、本質は「クラスに紐づく変数」にすぎません。
変数は書き換えることができますよね。そのため、静的プロパティの値は、コンパイル時に値として確定させることはできません。
Zend VMは、静的プロパティにアクセスするたびに、以下のプロセスを踏みます。
1. クラスのエントリーから静的プロパティのプロパティテーブルを引く。
2. そのプロパティがどこに格納されているかを指すオフセットを解決する。
3. `zval`の型チェックや、変更に伴う参照カウント(Refcounting)の処理を行う。
つまり、定数が「ただの定数(メモリ上の固定値)」として扱われるのに対し、静的プロパティは「隠されたグローバル変数」としてのオーバーヘッドを常に背負っているのです。
—
2. 実際にコードで挙動の違いを脳内トレースしてみる
言葉だけだと抽象的になってしまうので、実際のコードをベースに、内部で何が起きているのかをイメージしてみましょう。
大規模なフレームワークやドメイン駆動設計(DDD)などで、ステータスコードや設定値を定義するシーンを想定してください。
オペコードレベルでの違い
PHPのコンパイラが生成するオペコードを覗いてみると、その差は歴然です。
- クラス定数アクセス (`OrderStatus::STATUS_PENDING`)
コンパイル済みのオペコード(例: `FETCH_CONSTANT` や JITによる直接展開)において、`1` という整数値そのものが命令の中に埋め込まれます。CPUのレジスタに直接数値をロードするような感覚です。
- 静的プロパティアクセス (`OrderStatus::$legacyStatusPending`)
オペコードは `FETCH_STATIC_PROP` となり、実行時に必ずハッシュテーブルを引くための命令が発行されます。「クラス名からメモリ上のプロパティを探しに行く」という動的なルックアップが、実行のたびに発生するのです。
ほんのわずかな差に見えるかもしれませんが、数万件のループ処理や、フレームワークのコア層(ルーティングや依存性注入コンテナ)でこれが何百万回も繰り返されると、CPUキャッシュのヒット率や実行サイクルに確実に響いてきます。
—
3. なぜ「静的プロパティ」を使ってしまうのか、そしてその罠
「じゃあ、すべてクラス定数にすればいいじゃないか」と思いますよね。その通り、値が変化しない設定値やステータス、マジックナンバーの類は、迷わずクラス定数(あるいはPHP 8.1以降なら `enum`)を使うべきです。
しかし、実務の現場でよく見かけるアンチパターンとして、「配列やオブジェクトをキャッシュする目的で静的プロパティを使ってしまうケース」があります。
class ConfigLoader
{
// 静的プロパティで設定をキャッシュしようとする試み
public static array $cache = [];
public static function get(string $key): mixed
{
if (isset(self::$cache[$key])) {
return self::$cache[$key];
}
// 重い読み込み処理…
self::$cache[$key] = self::loadFromFile($key);
return self::$cache[$key];
}
}
この実装、一見すると賢くキャッシュできているように見えますよね。しかし、PHPの実行モデル(特に従来のPHP-FPM)を思い返してください。
PHP-FPMでは、リクエストごとにプロセスが独立している場合もありますが、OPcacheによってスクリプトのバイトコードは共有されます。そして静的プロパティは、リクエストを跨いでプロセス内に状態を残す(ステートフルにする)という性質を持っています。
もし、マルチテナントな環境や、非同期・常駐型PHPアプリケーション(Swoole、ReactPHPなど)でこのような静的プロパティのキャッシュを安易に使うと、「前のリクエストのデータが次のリクエストに漏れ出す(データ汚染)」という、極めてデバッグが困難なバグの温床になります。
定数は「イミュータブル(不変)」であるため、こうした状態汚染のリスクを構造的に排除してくれます。ここが、アーキテクトが静的プロパティよりも定数(またはイミュータブルなデータ構造)を愛する最大の理由です。
—
4. アーキテクトからの提言:PHP 8.x時代に選ぶべきベストプラクティス
ここまでの話をまとめましょう。PHP 8.xのJITコンパイラとZend VMの恩恵を最大限に引き出し、爆速かつ堅牢なアプリケーションを構築するための指針は以下の通りです。
1. 変わらない値はすべて「クラス定数(`const`)」か「Enum(列挙型)」にする
- JITやOPcacheによる最適化の恩恵(インライン展開やハッシュルックアップの回避)をダイレクトに受けられます。
- メモリ安全性が高く、意図しない書き換えを防げます。
2. 静的プロパティは「本当に状態をプロセス内で共有・変更する必要がある場合」に限定する
- ロガーのインスタンスや、コネクションプールの保持など、厳密に管理されたシングルトン的なユースケース以外では避けるのが無難です。
- 特に常駐型アプリケーション(Swoole等)では、静的プロパティのライフサイクルに細心の注意を払ってください。
「たかが定数、されど定数」。
言語の裏側でエンジンがどうメモリを割り当て、どうCPUに命令を送っているのかを解像度高くイメージできるようになると、コードを書くときの迷いがスッと消えていきます。
あなたの書くその1行のコードが、PHPのエンジンをいかに軽やかに走らせるか。ぜひ、今日の設計から意識してみてください。きっと、ワンランク上の美しいコードが書けるはずですよ。