PHPのmax/minの挙動に対する対応から考える主要OSSの思想

PHP開発者であれば、一度は関数の予期せぬ挙動に頭を悩ませたことがあるのではないでしょうか。

その中でも特に有名なのが、max()やmin()にNANが混ざったときの挙動です。

PHP 8.0未満では、「引数の順番」や「PHPのバージョン」によって結果が変わるという、まるでパズルのようなバグ(仕様)が存在していました。

では、WordPressやLaravel、Symfonyといった、世界中で使われている巨大なオープンソースソフトウェア(OSS)は、この挙動にどう対処しているのでしょうか? その防衛策をまとめました。

そもそも何が問題だったのか?

問題の本質を理解するために、PHP 8.0未満におけるmax()の挙動をおさらいしてみましょう。

// PHP 7.x までの恐怖の挙動
echo max(NAN, 5); // 結果: 5
echo max(5, NAN); // 結果: NAN

このように、「同じ値を比較しているのに、引数の順番を入れ替えるだけで結果が変わる」という致命的な不整合が起きていました(※PHP 8.0以降は、NANが含まれる場合は順序に関わらず常に NANを返すよう標準化されました)。

多様なサーバー環境で動作するセルフホスティング型のOSSにとって、このようなバージョン間の挙動差異は、計算バグや画面が真っ白になる(ホワイトスクリーン)原因になります。

巨大OSSたちが実践する4つの防衛アプローチ

OSSのコア開発における大原則は、言語(PHP)の気まぐれな仕様変更に対して、仕様通りに動くことを期待しないことです。各OSSはそれぞれの設計思想に基づき、以下の手法でこの問題を完全に封じ込めています。

1. バリデーションによる「NANの徹底排除」(WordPress型)

WordPressのような伝統的かつ互換性を最重視するOSSでは、計算処理や数値比較を行う前段階で、値が適切な数値であるかを泥臭くチェック(クレンジング)します。

is_nan()での事前検知

外部からの入力値や、不安定な計算結果の直後にis_nan()を噛ませ、デフォルト値(`0` や `null`)に差し替えます。

データバリデーションAPIの徹底活用

WordPressコアにはabsint()(絶対値を整数化)などの強力なヘルパー関数が多数用意されています。これらを経由させることで、そもそも `max()` や `min()` に `NAN` が到達できない設計にしています。

2. 静的解析(Static Analysis)によるコンパイルレベルでの排除(モダンフレームワーク型)

LaravelやSymfonyといったモダンなエコシステムでは、コードを実行する前(CI/CDのフェーズ)に PHPStanや Psalmといった静的解析ツールでバグを焼き尽くします。

strict_typesの導入

ファイルの先頭にdeclare(strict_types=1);を記述し、厳格な型定義を行います。

静的解析での型チェック

関数に渡される可能性のある型にNAN(実質 float ですが、計算結果として不安定な状態)が含まれる可能性がある場合、静的解析ツールが「不確実な数値が max()に渡されている」と警告を出します。開発者は実装段階で対処を強制されるため、バグが本番環境に混入しません。

3. エコシステム共通のポリフィル(互換ライブラリ)の活用

Drupal(10以降)やモダンなCMS/ライブラリは、自前で泥臭いラッパー関数を書くのをやめ、コミュニティ全体で共有されているポリフィル(互換ライブラリ)にその解決を委ねます。

symfony/polyfill-php80 などの強制

PHP 8.0未満の環境であっても、PHP 8.0と同じ安全な挙動(NANが含まれれば常にNANを返す、または安全に処理する)を保証するポリフィルを Composer 経由で強制インストールさせます。独自の「オレ流安全関数」を量産しないことで、コードの品質を担保しています。

4. アプリケーション層での「値オブジェクト(Value Object)」の採用

金融系やEC系のOSS(EC-CUBEやMagentoなど)や、ドメイン駆動設計(DDD)を取り入れたシステムでは、生の floatやintをそのままmax() やmin()に渡すこと自体を避ける傾向があります。

数値を PriceやScoreといった専用のオブジェクトの中に閉じ込めます。

class Price {
    private float $amount;
    public function __construct(float $amount) {
        if (is_nan($amount)) {
            throw new InvalidArgumentException("価格に NAN は指定できません。");
        }
        $this->amount = $amount;
    }
    public function compareMax(Price $other): Price {
        // オブジェクト生成時点で安全が保証されているため、安心して比較できる
        return new self(max($this->amount, $other->amount));
    }
}

このようにオブジェクトの生成時点で `NAN` の存在を完全に否定(ガード)するため、システム内部のどこで max()やmin()が使われようとも、バージョンの違いによる挙動の変化に怯える必要がなくなります。

究極の解決策:サポート要件(Minimum Requirements)の引き上げ

どれだけコード側で防御しても、古いPHPのバグをカバーするためのコード(技術負債)は肥大化していきます。そのため、主要なOSSは定期的に古いPHPバージョンのサポートを打ち切るというドラスティックな意思決定を行います。

事実、WordPressも現在は PHP 7.4 以上(推奨は8.1以上)を要求しており、多くのモダンフレームワークは PHP 8.1〜8.2 未満のサポートをすでに終了しています。これにより、古い仕様のバグ対応コードをコアから削除し、安全でクリーンな設計へとシフトしています。

まとめ

世界中で動くオープンソースから私たちが学べるのは、言語のバグや曖昧な仕様に運命を委ねないという徹底した防御的プログラミングの姿勢です。

1. 入力時点で防ぐ(バリデーションやガード節)

2. 型と静的解析で縛る(PHPStan / strict_types)

3. 自前のルールで包む、または標準ライブラリに任せる(値オブジェクト / ポリフィル)