PHP アップグレード特集 | 起点 8.0 系
8.0.0〜8.0.30 の全 31 版に共通。7.x から 8.0 に上げたときのような大きな断絶は無く、Deprecated(非推奨の通知)を潰す作業が中心です。ただし、8.1 で変わった既定値のように、エラーを出さずに結果だけが変わるものが混じっています。
※2026年10月時点の公開情報に基づきます。項目数と内容は、この特集の変更カタログ(全 213 項目)と php.net の移行ガイドから作っています。
PHP 8.0 系のすべて、8.0.0 から 8.0.30 までの 31 版が対象です。PHP はパッチ版(3 つ目の数字)の間では後方互換性を保つので、8.0.7 で動いているコードも 8.0.30 で動いているコードも、新しい版へ移すときに直す箇所は同じです。8.0 系のパッチ版の途中で入った変更のうち、コードの修正が要るものは、この特集の資料では把握していません。細かなセキュリティ修正は ChangeLog の各パッチの項で確かめてください。
PHP 8.0.0 / PHP 8.0.1 / PHP 8.0.2 / PHP 8.0.3 / PHP 8.0.4 / PHP 8.0.5 / PHP 8.0.6 / PHP 8.0.7 / PHP 8.0.8 / PHP 8.0.9 / PHP 8.0.10 / PHP 8.0.11 / PHP 8.0.12 / PHP 8.0.13 / PHP 8.0.14 / PHP 8.0.15 / PHP 8.0.16 / PHP 8.0.17 / PHP 8.0.18 / PHP 8.0.19 / PHP 8.0.20 / PHP 8.0.21 / PHP 8.0.22 / PHP 8.0.23 / PHP 8.0.24 / PHP 8.0.25 / PHP 8.0.26 / PHP 8.0.27 / PHP 8.0.28 / PHP 8.0.29 / PHP 8.0.30
8.0 系の最後のリリースは 8.0.30(2023-08-03)です。8.0.31 以降の番号は php.net から出ておらず、php-src のリポジトリにもタグがありません(2026-10-23 に確認)。どこかで 8.0.31 や 8.0.33 といった番号を見かけても、8.0 系であればこのページの内容がそのまま当てはまります。
移行先として扱うのは、2026-10-21 に公開されたセキュリティ修正版の 4 つです。起点と移行先の組み合わせごとの項目数は次のとおりです。8.0.0 → 8.4.27 も 8.0.15 → 8.4.27 も、一覧は 8.0.30 → 8.4.27 と同じです。
8.0 起点なら、8.4.27 へ直行するのが第一候補です。8.1・8.2・8.3・8.4 の 4 版を経由しますが、7.x から 8.0 へ上げたときのように文法や比較の規則ごと変わる項目は入っていません。58 項目のうち 24 項目は非推奨で、Deprecated の通知がログに出るだけで処理は続きます。その一方で、8.1 の既定値の変更は 8.0 から上げる人だけが通る関門で、7.x 起点よりむしろこの起点で目立つ問題になります。
| 移行先 | 項目数 | サポート期限 | 向いている場合 |
|---|---|---|---|
| 8.4.27推奨 | 58 | 2028-12-31 | まず選ぶ版。8.1 の既定値の変更と、8.4 の暗黙の nullable の非推奨を 1 回の移行でまとめて片付けられる |
| 8.5.12 | 80 | 2029-12-31 | 使っているフレームワークと主要ライブラリが 8.5 対応を済ませていて、次の移行までの期間を 1 年延ばしたい場合。非推奨が 12 項目増える |
| 8.3.36 | 42 | 2027-12-31 | IMAP・OCI8・PSpell の拡張にまだ依存していて、8.4 での削除に代替が間に合わない場合の一時的な行き先 |
| 8.2.35選ばない | 28 | 2026-12-31 | サポートが 2 か月余りで終わる。移行作業が終わるころには、また EOL の版に乗っていることになる |
2026-10-21 の php.net の勧告は、「PHP 8.1 and older no longer receive updates from the PHP project」と書いたうえで、脆弱性の深刻さを理由に、PHP セキュリティチームが 7.4・8.0・8.1 向けの「minimal source-backports」を用意したと説明しています。対象として挙げているのは「people building from source or without access to an LTS vendor」、つまりソースから自分でビルドする人と、LTS ベンダーの延長サポートを使えない人です。
8.0 向けは php-src の PHP-8.0-CVE-2026-109618-109619 ブランチです。2026-10-23 に中身を見たところ、修正のコミットのメッセージは「Avoid state corruption via bailout / Minimal mitigation for EOL version.」で、main/php_version.h の版番号は 8.0.30 のままでした。8.0.31 というタグもリリースもありません。このブランチからビルドしても php -v は 8.0.30 と表示するので、修正を当てた PHP と当てていない PHP を版番号では見分けられない点にも注意が要ります。
このブランチは今回の 2 件の CVE(CVE-2026-109618 / CVE-2026-109619)を塞ぐための応急処置で、8.0 系の保守が再開したわけではありません。次に別の脆弱性が見つかったときに同じ手当てがあるとは、勧告のどこにも書かれていません。ディストリビューションのパッケージや LTS ベンダーの修正で今回をしのいだ場合も、そこで止めずに、サポートが続く 8.4 か 8.5 へコードを移す。このページはその前提で書いています。
8.0 起点で一番気をつけたいのは、8.1 で入った既定値と戻り値の型の変更です。8.1 は 8.0 から上げる人だけが通る版で、7.x 起点では 8.0 の大きな変更の陰に隠れがちな項目が、ここでは主役になります。以下は、エラーを出さずに結果が変わるものと、通知が大量に出て本当の問題をログに埋もれさせるものから選んだ 10 件です。
既定のフラグが ENT_COMPAT から ENT_QUOTES|ENT_SUBSTITUTE に変わりました。' が ' になり、不正なバイト列は空文字列ではなく U+FFFD に置き換わります。ブラウザの表示はほとんど変わらないので目では気づけず、出力 HTML を文字列で比べるテストや、エスケープ済みの値を保存して後で突き合わせる処理で差が出ます。
PDO MySQL でエミュレートプリペアドを使っている場合と PDO SQLite で、整数の列が "1" ではなく 1 で返ります。$row['status'] === '1' は常に false になり、分岐が黙って反対側に倒れます。json_encode() の出力も変わるので、API の受け手が文字列を前提にしていれば、壊れるのは相手側です。
strlen(null)、trim(null)、htmlspecialchars(null) などが通知の対象です。DB の NULL 列や未設定の $_GET をそのまま文字列関数に渡しているコードでは、1 回の画面表示で何十件も出ることがあります。動作は続きますが PHP 9 で TypeError になる予定で、件数が多いぶん、他の重要な通知を埋もれさせます。
既定のエラー報告モードが MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT になりました。mysqli_query(...) or die(...) や if (!$result) で書いたエラー処理には到達せず、mysqli_sql_exception が捕まえられないまま上がります。用意していたエラー画面ではなく、PHP の Fatal error の画面やログになります。
メソッドの中の static $cache を、子クラスごとのキャッシュとして使っていたコードが対象です。8.1 からは親クラスと子クラスが同じ変数を見るので、別のクラス向けに計算した値が返ります。エラーも通知も出ません。
8.0 時代に書いたモデルクラスや、配列の値をオブジェクトに詰め替えるヘルパーでよく出ます。プロパティの宣言を足すのが本来の直し方で、すぐには直せないクラスだけ #[\AllowDynamicProperties] を付けて先送りします。PHP 9 で Error になる予定です。
strtolower()・ucfirst()・stripos() などが ASCII の文字しか変換しなくなりました。setlocale() で Latin-1 などのロケールを設定し、アクセント付きの文字を変換していたコードでは、結果が黙って変わります。UTF-8 の日本語にはもともと効かない関数なので、影響を受けるのは主に多言語のサイトです。
内部の事前丸めが無くなり、たとえば 0.49999999999999994 は 1 ではなく 0 に丸められるようになりました。ほとんどの値では結果は同じですが、金額や税額の端数処理に round() を使っているなら、境界の値で旧版と結果を比べてから切り替えてください。
メールの受信に imap_open() を使っている、Oracle に OCI8 でつないでいる、といったコードは 8.4 では関数自体が無くなり Fatal error になります。壊れ方は分かりやすいものの、PECL でのビルドや純 PHP のライブラリへの置き換えには時間がかかるので、移行の最初に有無を調べておきます。
8.5.12 を選ぶ場合の項目です。PDO::FETCH_GROUP や PDO::FETCH_UNIQUE などの値が変わったため、定数名ではなく数値をそのまま書いているコードは、別の取得モードとして解釈されます。定数名で書いていれば影響はありません。
8.0 系から 8.5 までに入った削除・非推奨・挙動変更を、入った版ごとに 5 つのブロック([8.1]〜[8.5])で並べています。一覧は累積で、たとえば 8.4.27 へ移すなら [8.1]〜[8.4] のブロックをすべて確認します。下の移行先を選ぶと、選んだ版より後のブロックは折りたたまれ、件数の合計も選んだ移行先に合わせて変わります。
区分の見方は次のとおりです。削除は Fatal error や Error 例外になるか、機能や拡張そのものが無くなるもの。非推奨は E_DEPRECATED の通知が出るが動くもの(将来の版で削除の予定)。挙動変更は、エラーを出さずに結果やエラーの種類が変わるもの。各項目の id(v81-03 など)は、エージェントへの指示や作業記録で項目を指すときに使えます。
※以下の一覧に載せたコード・コマンド・プロンプトはあくまで例です。最終確認は php.net の公式 ChangeLog・移行ガイドで行ってください。内容の利用により生じた損害について当方は責任を負いません(詳しくはページ末尾の免責事項)。
移行先 8.4.27 まで
公式: PHP 8.1 移行ガイド(日本語) / 後方互換性のない変更 / 非推奨 / その他の変更 / ChangeLog 8.1.0 / UPGRADING
内部関数の非 nullable 引数への null 渡しを非推奨(strlen(null)、trim(null)、htmlspecialchars(null)、str_replace の subject に null、strpos(null, ...)、mb_strlen(null)、preg_match(..., null)、explode(',', null)、json_decode(null) 等)。PHP 9 で TypeError 予定
必要な修正$v ?? '' や (string) で正規化する。DB の NULL 列や未設定の $_GET を文字列関数に直接渡す箇所が大量に該当するので、ログで Deprecated を集計して潰す
mysqli の既定エラー報告モードが MYSQLI_REPORT_OFF から MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT に。接続失敗や SQL エラーで mysqli_sql_exception が投げられ、mysqli_query(...) or die(mysqli_error()) 式のエラー処理に到達しない
必要な修正try / catch (mysqli_sql_exception) に書き換える。暫定的に旧挙動へ戻すなら接続前に mysqli_report(MYSQLI_REPORT_OFF)
htmlspecialchars() / htmlentities() / htmlspecialchars_decode() / html_entity_decode() / get_html_translation_table() の既定フラグが ENT_COMPAT|ENT_HTML401 から ENT_QUOTES|ENT_SUBSTITUTE|ENT_HTML401 に。シングルクォートが ' に変換され、不正なバイト列は空文字列ではなく U+FFFD に置換される
必要な修正旧挙動が必要なら ENT_COMPAT を明示。出力の期待値(テスト・スナップショット・属性値内のクォート処理)を見直す
PDO MySQL(エミュレートプリペアド有効時)と PDO SQLite が整数・浮動小数点の列を文字列ではなく int / float で返す(ネイティブ型)
必要な修正=== '1' のような厳密比較、json_encode() の出力("1" → 1)、文字列連結前提のコードを修正。旧挙動は PDO::ATTR_STRINGIFY_FETCHES => true。mysqli でも mysqlnd はネイティブ型を返す点に注意
$GLOBALS 全体への書き込み($GLOBALS = [])、参照取得(&$GLOBALS、foreach ($GLOBALS as &$v))、配列関数による破壊的操作(array_pop($GLOBALS))はコンパイルエラー。$GLOBALS['x'] = 1 や $GLOBALS の読み取り・コピーは従来どおり
必要な修正個別キーの操作に書き換える
継承したメソッド内の static 変数が親クラスと子クラスで共有される(以前は子クラスごとに独立)
必要な修正子クラスごとの状態が必要なら static プロパティや配列に切り替える
リソースからオブジェクトへ移行:finfo_open() → finfo、ftp_connect() → FTP\Connection、imap_open() → IMAP\Connection、ldap_connect() → LDAP\Connection、ldap_search() → LDAP\Result、pg_connect() → PgSql\Connection、pg_query() → PgSql\Result、pg_lo_open() → PgSql\Lob、pspell_new() → PSpell\Dictionary、imageloadfont() → GdFont
必要な修正is_resource() 判定を !== false か instanceof に
内部クラス・インターフェースのメソッドをオーバーライドする際の戻り値型の非互換(ArrayAccess::offsetGet() / IteratorAggregate::getIterator() / Countable::count() / JsonSerializable::jsonSerialize() / Iterator::current() などを戻り値型なしで実装)
必要な修正戻り値型(: mixed / : Iterator / : int 等)を宣言するか、7.x 互換を保つなら #[\ReturnTypeWillChange] を付ける
Serializable インターフェースを __serialize() / __unserialize() なしで実装することを非推奨
必要な修正両マジックメソッドを追加実装する(Serializable は PHP 9 で削除予定)
精度を失う float から int への暗黙変換($arr[1.5]、"1.5" % 2、int 型引数への 1.5、1.5 << 1)を非推奨
必要な修正明示的に (int) キャスト
false からの自動配列化($a = false; $a[] = 1;)を非推奨(null / 未定義からは引き続き可)
必要な修正$a = []; で初期化する。関数が失敗時に false を返し、それに [] 代入するパターンに注意
トレイトの static メソッド・static プロパティへの直接アクセス(MyTrait::method() / MyTrait::$prop)を非推奨
必要な修正トレイトを使用するクラス経由でアクセス
date_sunrise() / date_sunset()、strftime() / gmstrftime()、strptime()、mhash*() 系(mhash / mhash_count / mhash_get_block_size / mhash_get_hash_name / mhash_keygen_s2k)、FILTER_SANITIZE_STRING / FILTER_SANITIZE_STRIPPED、odbc_result_all()、PDO::FETCH_SERIALIZE、mysqli::init()、key() / current() / next() / prev() / reset() / end() のオブジェクトへの使用、ctype_*() への非文字列引数、pgsql 関数の接続引数省略、auto_detect_line_endings INI、filter.default INI、oci8.old_oci_close_semantics INI、IntlCalendar::roll() への bool を非推奨
必要な修正date_sun_info()、date() / DateTime::format() / IntlDateFormatter(日本語の曜日・月名は IntlDateFormatter)、date_parse_from_format() / DateTime::createFromFormat()、hash() / hash_hmac()、htmlspecialchars()、odbc_fetch_array のループ、独自シリアライズ、new mysqli()、get_mangled_object_vars() / ArrayIterator、(string) キャスト、接続を明示、ファイル読み込みで改行を正規化
mysqli_fetch_fields() / mysqli_fetch_field_direct() の max_length が常に 0。version_compare() が未文書化の演算子省略形を拒否
必要な修正最大長が必要なら mysqli_result::fetch_field() 前に store_result、version_compare の第 3 引数は 'lt' / 'ge' 等の正式表記
readonly / never が予約語に(クラス名・関数名に使えない)。enum が宣言構文のキーワードになり、enum の直後に識別子が続くコードの解釈が変わる。新規組み込み関数の追加(array_is_list / enum_exists / fsync / fdatasync)で同名の自作関数は Fatal
必要な修正リネーム、function_exists() ガード
公式: PHP 8.2 移行ガイド(日本語) / 後方互換性のない変更 / 非推奨 / その他の変更 / ChangeLog 8.2.0 / UPGRADING
動的プロパティの生成(宣言していないプロパティへの $obj->foo = 1)を非推奨(stdClass と __get() / __set() 実装クラスは対象外)。PHP 9 で Error 予定
必要な修正プロパティを宣言する(推奨)。古いクラスを一括で延命するなら #[\AllowDynamicProperties] をクラスに付与(子クラスにも適用)。他人のオブジェクトにデータを付けるなら WeakMap
"${var}" / "${expr}" 形式の文字列補間を非推奨
必要な修正"$var" / "{$var}" / "{${expr}}" に書き換える。テンプレート系の古いコードは sed で一括置換しやすい
utf8_encode() / utf8_decode() を非推奨([9.0] で削除予定)
必要な修正mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1') / iconv()。Shift_JIS 変換の意図で utf8_encode を誤用しているコード(Latin-1 専用)は根本から直す
相対的なコールアブル:"self::method" / "parent::method" / "static::method" / ["self", "method"] / ["parent", "method"] / ["static", "method"] / ["Foo", "Bar::method"] / [new Foo, "Bar::method"] を非推奨
必要な修正[self::class, 'method'] / [$this, 'method'] / クロージャ($this->method(...))に置換
mbstring の QPrint / Base64 / Uuencode / HTML-ENTITIES「エンコーディング」を非推奨(mb_convert_encoding($s, 'HTML-ENTITIES', 'UTF-8') が典型)
必要な修正mb_encode_numericentity() / htmlspecialchars() / htmlentities()、base64_encode()、quoted_printable_encode()、convert_uuencode() に置換
SplFileInfo::_bad_state_ex() を非推奨
必要な修正呼ばない(内部用)
strtolower() / strtoupper() / stripos() / strripos() / lcfirst() / ucfirst() / ucwords() / str_ireplace() がロケール非依存(ASCII のみ変換)に
必要な修正マルチバイトやロケール依存の変換は mb_strtolower() / mb_strtoupper() / mb_convert_case() に
str_split('') が [''] ではなく [] を返す
必要な修正空配列を前提に修正(foreach は影響なし、count() や [0] 参照に注意)
ksort() / krsort() の SORT_REGULAR が数値文字列を PHP 8 の比較規則(文字列と数値の比較変更)で並べる
必要な修正順序依存のコードを確認。数値キーなら SORT_NUMERIC を明示
var_export() がクラス名を先頭 \ 付きの完全修飾名で出力(\Foo\Bar::__set_state(...))。strcmp() / strcasecmp() / strncmp() / strncasecmp() / substr_compare() が差分値ではなく -1 / 0 / 1 を返す
必要な修正var_export の出力を文字列比較・保存しているコードを修正。strcmp の戻り値を差分値として使うコード(=== 2 など)を < 0 / > 0 判定に
glob() は open_basedir で全ファイルが除外されると false ではなく [] を返す。FilesystemIterator のコンストラクタに flags を明示指定した場合、SKIP_DOTS が自動では付かなくなった(. と .. が列挙される)。ODBC / PDO_ODBC が接続文字列中のユーザー名・パスワードをエスケープする
必要な修正戻り値判定の修正、flags を渡す場合は FilesystemIterator::SKIP_DOTS を含める、DSN にユーザー名を埋め込んでいる場合は引数と二重指定しない
mysqli が libmysqlclient でビルドできなくなり mysqlnd 必須。mysqli.reconnect INI と自動再接続機能を削除
必要な修正接続切れは明示的に再接続する([8.4] で mysqli_ping() も非推奨)。ディストリビューションのパッケージ構成を確認
新規組み込み関数の追加(mysqli_execute_query / openssl_cipher_key_length / curl_upkeep / memory_reset_peak_usage / ini_parse_quantity / libxml_get_external_entity_loader 等)。同名の自作関数は Fatal
必要な修正function_exists() ガード
公式: PHP 8.3 移行ガイド(日本語) / 後方互換性のない変更 / 非推奨 / その他の変更 / ChangeLog 8.3.0 / UPGRADING
range() の変更:境界にオブジェクト・リソース・配列を渡すと TypeError、step に 0 や INF / NAN は ValueError、増加範囲に負の step は ValueError、int に解釈できる float の step は int 扱い、空文字列の境界は E_WARNING(0 扱い)、片方が数字の文字列ならもう片方も文字列として扱い文字の範囲を返す(range('9', 'A') が [9,8,...,0] から ['9', ':', ';', ..., 'A'] に)
必要な修正境界と step の型を揃える。文字列の数字を渡している箇所は (int) キャスト
空配列に負のインデックス $n を代入すると次の暗黙キーが $n + 1 になる(以前は 0)。例:$a = []; $a[-5] = 'x'; $a[] = 'y'; → キーは -5, -4
必要な修正負のキーの後に [] 追加するコードを確認
トレイトの static プロパティが親クラスから継承したものを再宣言する(クラスごとに独立したストレージ)。クラス定数の可視性分散をインターフェース継承でも検査
必要な修正共有を期待していたコードを修正
Date 拡張が DateError / DateException 階層の例外・エラーを投げる(new DateTime('不正') は DateMalformedStringException、new DateTimeZone('不正') は DateInvalidTimeZoneException、DateInterval の不正文字列は DateMalformedIntervalStringException など。以前は警告や汎用 Exception)
必要な修正catch (Exception $e) は引き続き捕捉できる。例外クラス名で分岐しているコードとメッセージのスナップショットを確認
proc_get_status() を複数回呼んでも正しい値を返す(結果をキャッシュ、"cached" キー追加)。呼び出しスタックの上限に近いコードは Error 例外(zend.max_allowed_stack_size)。WeakMap の自己参照キーが GC で回収される。ZTS ビルド(Linux)で Zend Max Execution Timers が既定で有効
必要な修正深い再帰のコードはスタック使用量を見直す
DOM:親のないノードへの after() / before() / replaceWith() が例外ではなく no-op、DOMParentNode / DOMChildNode のメソッドが document なしでも動作、createAttributeNS() の既定名前空間・接頭辞衝突の修正、DOM クラスに新メソッド・プロパティが追加されたため継承クラスの同名宣言は互換が必要
必要な修正DOMDocument 等を継承しているクラスの宣言を確認
opcache.consistency_checks INI を削除。FFI の void 関数は CData ではなく null を返す。Phar / SNMP のクラス定数に型を宣言
必要な修正php.ini から削除、FFI の戻り値判定を修正
number_format() が負の decimals を解釈する(num を abs(decimals) 桁で整数部側に丸める。以前は無視)。file() が無効なフラグ(FILE_APPEND など)を拒否
必要な修正負の decimals を渡しているなら 0 に、file() のフラグを確認
unserialize() のエラーが E_NOTICE から E_WARNING に。array_sum() / array_product() が + / * 演算子と同じ型変換規則で計算し、対応しない型(配列・オブジェクト)には E_WARNING を出す。highlight_file() / highlight_string() の HTML 出力形式が変更(<pre><code> でラップ、改行を \n に)
必要な修正エラーハンドラの扱いを確認、配列の合計に非数値が混ざっていないか確認、highlight 系の出力に CSS を当てている場合は調整
空文字列・非数値文字列・英数字以外の文字列への ++、空文字列・非数値文字列への -- を非推奨(Perl 風の文字列インクリメント "a"++ → "b" はソフト非推奨)
必要な修正数値は (int) キャストしてから演算、文字列の連番は str_increment() / str_decrement()
get_class() / get_parent_class() の引数なし呼び出しを非推奨
必要な修正static::class / self::class / get_class($this) / get_parent_class($this)
MT_RAND_PHP(mt_srand の旧実装モード)、assert_options()、ASSERT_ACTIVE / ASSERT_BAIL / ASSERT_CALLBACK / ASSERT_EXCEPTION / ASSERT_WARNING 定数、assert.* INI(assert.active / assert.warning 等)を非推奨
必要な修正zend.assertions INI で制御。assert_options(ASSERT_ACTIVE, 0) 等の呼び出しを削除
SQLite3::enableExceptions(false) を非推奨(将来は例外のみ)。U_MULTIPLE_DECIMAL_SEPERATORS 定数(→ U_MULTIPLE_DECIMAL_SEPARATORS)、NumberFormatter::TYPE_CURRENCY、ldap_connect($host, $port) の分離指定(→ URI 形式)、mb_strimwidth() の負の幅、Phar::setStub(resource, length)、FFI::new() / cast() / type() の静的呼び出し、ReflectionProperty::setValue() の 1 引数呼び出し(static は第 1 引数 null)、dba_fetch() の第 3 引数 dba、ZipArchive::FL_RECOMPRESS を非推奨
必要な修正括弧内の代替へ。SQLite3 は例外ベースのエラー処理に移行
新規組み込み関数の追加(json_validate / str_increment / str_decrement / mb_str_pad / stream_context_set_options / ldap_exop_sync 等)。同名の自作関数(json_validate のポリフィルが典型)は Fatal
必要な修正function_exists() ガード
公式: PHP 8.4 移行ガイド(日本語) / 後方互換性のない変更 / 非推奨 / 削除された拡張モジュール / その他の変更 / ChangeLog 8.4.0 / UPGRADING
IMAP(imap_*)、OCI8、PDO_OCI、PSpell 拡張を本体から削除(PECL へ)
必要な修正pecl install でビルドし直す(IMAP の PECL 版は libc-client / uw-imap 依存で入手が難しいため、Webklex/php-imap や ddeboer/imap など純 PHP ライブラリへの移行を推奨)。OCI8 / PDO_OCI は PECL 版、PSpell は enchant に移行
暗黙の nullable 引数 function f(T $x = null) を非推奨(PHP 9 で Error 予定)。null デフォルトの後に必須引数が続く function f(T $a, T2 $b = null, T3 $c) は、[8.0] 以来の「必須引数より前のオプション引数」非推奨にも該当
必要な修正?T $x = null か T|null $x = null に。必須引数より前なら ?T2 $b にしてデフォルト値を外す。Rector(ExplicitNullableParamTypeRector)で一括変換可
E_STRICT 定数を非推奨(E_STRICT エラーレベル自体は削除)。trigger_error() に E_USER_ERROR を渡すことを非推奨。クラス名 _ を非推奨。0 ** -1 と pow(0, -1)(0 の負乗)を非推奨
必要な修正error_reporting(E_ALL & ~E_STRICT) は E_ALL に、trigger_error(E_USER_ERROR) は例外か exit() に、fpow() を使う
exit() / die() が言語構造から関数相当に変更。exit; の形も引き続き可だが、配列など不正な型の引数は TypeError、strict_types や名前付き引数の影響を受ける
必要な修正exit(array) 等の誤用を修正
round() の内部実装を書き換え(事前丸めを廃止、0.49999999999999994 の丸めが 1 から 0 になるなど)。一部の境界値で結果が以前と変わる。無効な mode は ValueError
必要な修正金額計算など厳密な丸めは BCMath(bcround は 8.4 で追加)や intdiv を検討
password_hash() の bcrypt(PASSWORD_BCRYPT / PASSWORD_DEFAULT)の既定コストが 10 から 12 に。既存のハッシュに対して password_needs_rehash() が true を返し、ログイン時の再ハッシュで CPU 負荷が約 4 倍になる
必要な修正負荷を据え置くなら ['cost' => 10] を明示。再ハッシュ処理の実装を確認
リソースからオブジェクトへ移行:dba_open() / dba_popen() → Dba\Connection、odbc_connect() / odbc_pconnect() → Odbc\Connection、odbc_exec() 等 → Odbc\Result、SoapClient::$httpurl / $sdl / $typemap → 内部オブジェクト
必要な修正is_resource() 判定を修正
PHP_DEBUG / PHP_ZTS 定数が int から bool に。GMP クラスが final(継承不可)。MYSQLI_SET_CHARSET_DIR / MYSQLI_STMT_ATTR_PREFETCH_ROWS / MYSQLI_CURSOR_TYPE_FOR_UPDATE / MYSQLI_CURSOR_TYPE_SCROLLABLE / MYSQLI_TYPE_INTERVAL 定数を削除。比較中の再帰(相互参照するオブジェクトの ==)は Fatal ではなく Error 例外
必要な修正=== 1 比較の修正、GMP を継承したクラスの修正、定数の削除
MySQL の wait_timeout 超過時のエラーコードが 2006 から 4031 に(MySQL 8.0.24 以降のサーバー)。JIT の既定が opcache.jit=disable(有効化には opcache.jit=tracing と opcache.jit_buffer_size を明示)。PCRE2 10.44 により {,3} を量指定子と解釈(以前はリテラル)
必要な修正再接続判定で 2006 を見ているコードに 4031 を追加、php.ini の JIT 設定を確認、{,n} をリテラルで使う正規表現は \{,n} にエスケープ
不正な引数が警告や false から ValueError / TypeError に変わった関数が複数ある(例:unserialize() の allowed_classes オプションが配列でも bool でもない場合、mb_http_output() の encoding に NUL バイトを含む場合と mb_http_input() の type が不正な場合、str_getcsv() の separator / enclosure / escape の長さが不正な場合、GD の imagejpeg() / imagewebp() / imagepng() / imageavif() の quality や imagescale() / imagefilter() の一部引数)
必要な修正呼び出し前に引数を検証する
mysqli_ping() / mysqli::ping()、mysqli_kill() / mysqli::kill()、mysqli_refresh() / mysqli::refresh() と MYSQLI_REFRESH_* 定数、mysqli_store_result() の mode 引数と MYSQLI_STORE_RESULT_COPY_DATA を非推奨
必要な修正ping は削除(自動再接続は [8.2] で消えている。SELECT 1 で疎通確認)、KILL / FLUSH は SQL で実行
セッション:session_set_save_handler() の 3 引数以上の形式、session.sid_length / session.sid_bits_per_character の変更、session.use_only_cookies / session.use_trans_sid / session.trans_sid_tags / session.trans_sid_hosts / session.referer_check の変更、SID 定数を非推奨
必要な修正SessionHandlerInterface 実装クラスを 2 引数形式で登録、INI は既定値(32 文字 16 進のセッション ID)に戻しストレージ側を対応、URL へのセッション ID 埋め込み(trans_sid)は廃止して Cookie のみに
fputcsv() / fgetcsv() / str_getcsv() および SplFileObject::fputcsv() / fgetcsv() / setCsvControl() の escape 引数の省略(既定 "\\")を非推奨
必要な修正escape: '' を明示する(RFC 4180 準拠にするなら空文字列)。既存 CSV がバックスラッシュを含む場合は "\\" を明示して互換維持
xml_set_object() と xml_set_*() ハンドラ登録への非 callable 文字列(メソッド名だけの文字列)を非推奨
必要な修正[$this, 'startElement'] のような callable 配列かクロージャを渡す
lcg_value()(→ Random\Randomizer::getFloat())、unserialize() の大文字 S タグ、stream_context_set_option() の 2 引数形式(→ stream_context_set_options())、DatePeriod::__construct(string $isostr)(→ DatePeriod::createFromISO8601String())、SUNFUNCS_RET_* 定数、CURLOPT_BINARYTRANSFER、DOM_PHP_ERR と DOMDocument::$actualEncoding / $config、DOMEntity::$actualEncoding / $encoding / $version、hash 関数への不正なオプション、IntlCalendar::set() / IntlGregorianCalendar::__construct() の 3 引数以上、ldap_connect() の 3 引数以上(→ ldap_connect_wallet())、ldap_exop() の 5 引数以上(→ ldap_exop_sync())、pg_fetch_result() / pg_field_prtlen() / pg_field_is_null() の 2 引数形式(→ row に null を渡す 3 引数)、PDO_PGSQL のドル引用文字列内の ?? エスケープ、ReflectionMethod::__construct() の 1 引数形式(→ createFromMethodName())、SoapServer::addFunction() への int と SOAP_FUNCTIONS_ALL、SplFixedArray::__wakeup()、dba_key_split() への null / false を非推奨
必要な修正括弧内の代替へ
新規組み込み関数の追加(array_find / array_find_key / array_any / array_all / mb_trim / mb_ltrim / mb_rtrim / mb_ucfirst / mb_lcfirst / fpow / http_get_last_response_headers / request_parse_body / grapheme_str_split / bcfloor / bcceil / bcround 等)。同名の自作関数(array_find / array_any / mb_trim が典型)は Fatal
必要な修正function_exists() ガード
公式: PHP 8.5 移行ガイド(日本語) / 後方互換性のない変更 / 非推奨 / その他の変更 / ChangeLog 8.5.0 / UPGRADING
バッククォート演算子 `command`(shell_exec のエイリアス)を非推奨
必要な修正shell_exec() に置換(さらに escapeshellarg() を確認)
非正規のキャスト名 (boolean) / (integer) / (double) / (binary) を非推奨
必要な修正(bool) / (int) / (float) / (string) に一括置換
case 文をコロンではなくセミコロンで終える case 1; を非推奨
必要な修正case 1: に修正
null を配列オフセットに使うこと($arr[null]、isset($arr[$undefined]) の $undefined が null)と array_key_exists(null, $arr) を非推奨
必要な修正キーを '' に正規化するか、null にならないよう事前チェック。未初期化変数をキーに使う古いコードが典型
非数値文字列のインクリメント("a"++ → "b")を非推奨([8.3] のソフト非推奨が正式な E_DEPRECATED に)
必要な修正str_increment() に置換
$http_response_header(file_get_contents('http://...') 後に自動定義されるローカル変数)を非推奨
必要な修正http_get_last_response_headers()(8.4 で追加)に置換
register_argc_argv による非 CLI SAPI での $_SERVER['argv'] / argc のクエリ文字列からの導出を非推奨
必要な修正register_argc_argv=0 に設定し $_GET / $_SERVER['QUERY_STRING'] を使う
__sleep() / __wakeup() をソフト非推奨(E_DEPRECATED なし)。__debugInfo() の null 返却、定数の再宣言、report_memleaks INI、出力ハンドラ内での出力、不正なクロージャのバインド(static クロージャへのインスタンス束縛、無関係なクラスへのメソッド束縛、$this のアンバインド、内部クラスのスコープへの束縛、関数由来クロージャのスコープ変更)を非推奨
必要な修正__serialize() / __unserialize() を実装、空配列を返す、定数の二重定義を解消、Closure::bind の使い方を見直す
curl_close() / curl_share_close()、finfo_close()(と finfo_buffer() の context 引数)、imagedestroy()、xml_parser_free() を非推奨([8.0] / [8.1] 以降すでに no-op)
必要な修正呼び出し行を削除するだけでよい
MHASH_* 定数(MHASH_MD5 等)を非推奨(削除ではない)。mysqli_execute()(→ mysqli_stmt_execute())、socket_set_timeout()(→ stream_set_timeout())、DATE_RFC7231 / DateTimeInterface::RFC7231、intl.error_level INI、openssl_pkey_derive() の key_length、chr() への 0〜255 範囲外の値、ord() への複数バイト文字列、readdir() / rewinddir() / closedir() への null、LDAP の Oracle Instant Client 用の呼び出し(ldap_connect_wallet 等)と GSLC_SSL_* 定数を非推奨
必要な修正括弧内の代替へ。hash() に移行、chr($n % 256) のように明示
PDO のドライバ固有定数とメソッドを非推奨:PDO::MYSQL_ATTR_*(→ Pdo\Mysql::ATTR_*。例:PDO::MYSQL_ATTR_INIT_COMMAND → Pdo\Mysql::ATTR_INIT_COMMAND、MYSQL_ATTR_USE_BUFFERED_QUERY、MYSQL_ATTR_SSL_CA 等)、PDO::SQLITE_*、PDO::PGSQL_ATTR_DISABLE_PREPARES、PDO::ODBC_*、PDO::DBLIB_*、PDO::FB_*、PDO::sqliteCreateFunction() / sqliteCreateAggregate() / sqliteCreateCollation()(→ Pdo\Sqlite::createFunction() 等)、PDO::pgsqlCopyFromArray() 等(→ Pdo\Pgsql::copyFromArray() 等)。"uri:" 形式の DSN と PDO::PGSQL_TRANSACTION_* も非推奨
必要な修正PDO::connect() で Pdo\Mysql 等のサブクラスを取得し、新しい定数・メソッドに置換。接続オプション配列に PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4' を書いている箇所が典型
Reflection*::setAccessible()(8.1 以降は無効)、存在しない定数への ReflectionClass::getConstant()、デフォルト値のないプロパティへの ReflectionProperty::getDefaultValue()、spl_autoload_unregister(spl_autoload_call(...)) による全解除、SplObjectStorage::contains() / attach() / detach()(→ offsetExists() / offsetSet() / offsetUnset())、ArrayObject / ArrayIterator へのオブジェクト渡しを非推奨
必要な修正括弧内の代替へ。setAccessible() は行ごと削除
disable_classes INI を削除
必要な修正php.ini から削除。クラス単位の無効化が必要なら別の手段(オートローダーで弾く等)
Opcache が常に PHP バイナリに組み込まれ常にロードされる(opcache.enable / opcache.enable_cli は引き続き有効)。zend_extension=opcache.so / php_opcache.dll の INI 指定は警告
必要な修正php.ini / conf.d から zend_extension=opcache の行を削除。--enable-opcache / --disable-opcache の configure オプションも削除
class_alias() で "array" / "callable" を別名に使えない。比較不能なオブジェクト(enum、CurlHandle 等の内部クラス)と bool の緩い比較が常に (bool)$object の結果に統一。トレイトが親クラスより先にバインドされる。コンパイル時・クラスリンク時のエラーが常に遅延処理される。#[\Attribute] を抽象クラス・enum・インターフェース・トレイトに付けるとコンパイルエラー(#[\DelayedTargetValidation] で実行時に遅延可)。gc_collect_cycles() の戻り値に間接回収分を含まない。tick ハンドラの停止タイミングが shutdown 関数・デストラクタ・出力ハンドラ処理の後に
必要な修正該当コードを確認
null 以外の非配列(int / string / object)を [] や list() で分割代入すると E_WARNING。int で表現できない float(や float に見える文字列)の int キャストで警告、NAN のキャストで警告
必要な修正分割代入の前に is_array() で検査、桁あふれの計算を見直す
PDO:PDO::FETCH_GROUP / FETCH_UNIQUE / FETCH_CLASSTYPE / FETCH_PROPS_LATE / FETCH_SERIALIZE の定数値が変更(数値を直書きしていると壊れる)。FETCH_CLASS のコンストラクタ引数が call_user_func_array() 準拠(文字列キーは名前付き引数、参照渡しは [&$v])。fetch 中の setFetchMode() は Error。FETCH_PROPS_LATE を FETCH_CLASS 以外と併用、fetchAll() に FETCH_INTO は ValueError。PDO_SQLITE の quote() が NUL バイトを含む文字列で例外 / 警告
必要な修正定数名で参照する、コンストラクタ引数の配列を整数キーに
mysqli:構築済みオブジェクトでの mysqli コンストラクタ再呼び出し($db->__construct(...) による再接続)は Error。セッション:$_SESSION のキーにパイプ文字(|)を含むと警告(以前は保存を静かに失敗)、session_start() の options が連想配列でないと ValueError、read_and_close が int 互換でないと TypeError。SimpleXMLElement::xpath() がノード集合以外を返す式で警告+false(以前は空配列)
必要な修正再接続は新しい mysqli を生成、セッションキーからパイプ文字を除去、xpath の戻り値判定を修正
Intl は ICU 57.1 以上が必須。IntlDateFormatter::setTimeZone() が未初期化時に IntlException。Locale メソッドへの NUL バイトで ValueError。Collator::SORT_REGULAR の数値文字列の扱いを ext/standard の SORT_REGULAR に統一。mbstring の Unicode データが 17.0 に更新
必要な修正ビルド環境の ICU を確認、ソート順の期待値を確認
不正な引数で ValueError / TypeError になる関数の追加:bzcompress() の block_size / work_factor、finfo_file() の NUL バイト(TypeError → ValueError)、ldap_get_option() / ldap_set_option() の不正オプション、pcntl_exec() の NUL バイト、posix_kill() / posix_setpgid() / posix_setrlimit() の範囲外、snmpget() 等のホスト名・ポート・タイムアウト、socket_create_listen() / socket_bind() / socket_sendto() のポート範囲、socket_addrinfo_lookup() の hints、socket_set_option() のマルチキャスト、tidy のコンストラクタ・parseFile() / parseString() の設定値、setlocale() に整数 0 を渡す(TypeError)、PDO_FIREBIRD のカーソル名長
必要な修正呼び出し前に引数を検証
その他:SoapClient::__doRequest() に省略可能な第 6 引数 uriParserClass が追加(オーバーライドしている子クラスはシグネチャ互換の確認が必要)。ArrayObject が enum を受け付けない。SplFileObject::fwrite() の length が nullable で既定 null。DOMNamedNodeMap / DOMNodeList 等のクローンは失敗。printf 系で精度未指定時の扱いを修正(0 として扱う)。ODBC は ODBC 3.5 以上を前提(ODBCVER ビルドフラグと特定ドライバ向けビルドフラグを削除)。PCRE を PCRE2_EXTRA_ALLOW_LOOKAROUND_BSK なしでビルド
必要な修正SoapClient を継承したクラスの __doRequest 宣言を確認
新規組み込み関数の追加(array_first / array_last / get_error_handler / get_exception_handler / grapheme_levenshtein / locale_is_right_to_left / curl_multi_get_handles 等)。同名の自作関数(array_first / array_last はヘルパー名として頻出)は Fatal
必要な修正function_exists() ガードか名前空間付き関数に
8.0 起点の移行は、構文を書き直す作業より、通知と既定値の違いを一つずつ確かめる作業が大半を占めます。次の順で進めると、機械で拾えるものと人が判断するものを分けやすくなります。コマンドはリポジトリの直下で、Docker が使える端末から実行する前提です。
最初に、8.4 で関数ごと無くなる拡張と、8.1 の既定値の変更に引っかかりそうな書き方を数えます。ここで件数をつかんでおくと、移行先を 8.4.27 にするか、いったん 8.3.36 で止めるかを早めに決められます。grep の結果は候補の一覧で、該当しないものも混じります。
# 8.4 で削除される拡張(IMAP / OCI8 / PDO_OCI / PSpell)
grep -rnE '\b(imap_|oci_|pspell_)[a-z_]+\(' --include='*.php' --exclude-dir=vendor . | wc -l
# 8.1 の既定値の変更:フラグを省略した htmlspecialchars() / htmlentities()
grep -rnE '\b(htmlspecialchars|htmlentities)\(\s*[^,()]+\)' --include='*.php' --exclude-dir=vendor .
# 8.1 の mysqli の例外化:or die / or exit で書いたエラー処理
grep -rnE 'mysqli_[a-z_]+\(.*\)\s+or\s+(die|exit)' --include='*.php' --exclude-dir=vendor .
# 8.1 の PDO のネイティブ型:文字列の数字と厳密比較している箇所
grep -rnE "===\s*'[0-9]+'|'[0-9]+'\s*===" --include='*.php' --exclude-dir=vendor .Docker の公式イメージには、パッチ版まで固定したタグがあります。ホストの PHP は 8.0 のまま残し、解析とテストはコンテナの 8.4.27 で動かします。8.5.12 も候補に残すなら、同じ手順をタグだけ変えてもう一度実行します。
docker run --rm php:8.4.27-cli php -v
docker run --rm php:8.5.12-cli php -v
# 依存パッケージのうち、8.4.27 を拒んでいるものを一覧にする
docker run --rm -v "$PWD":/app -w /app composer:2 composer why-not php 8.4.27composer why-not が何か返したら、そのパッケージの更新を先に済ませます。フレームワークの版を上げる必要があるなら、PHP の移行とは別の作業として切り出したほうが、問題が起きたときに原因を切り分けられます。
PHPCompatibility は、指定した範囲の PHP で使えない関数や構文を指摘します。8.0 起点では範囲を 8.0-8.4 にすると、8.0 でも 8.4 でも動く書き方になっているかを見られます。Rector は機械的に直せるものを書き換えますが、版ごとのセットには「新しい書き方への置き換え」も含まれ、差分が膨らみます。8.0 起点では、8.1 → 8.2 → 8.3 → 8.4 と 1 版ずつ上げてコミットを分けると、レビューで何が変わったかを追えます。withPhpSets() の名前付き引数は「その版までの規則をすべて」適用する指定で、1 つの設定ファイルで 1 回しか呼べません。
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/app'])
// 1 回目は php81、コミットしたら php82、php83、php84 と上げていく
->withPhpSets(php81: true);composer require --dev phpcompatibility/php-compatibility rector/rector phpstan/phpstan
# 8.0 でも 8.4 でも動く書き方かを調べる
vendor/bin/phpcs -ps . --standard=PHPCompatibility --runtime-set testVersion 8.0-8.4 --ignore=*/vendor/*
# 書き換えの差分だけを見る(まだファイルは変えない)
vendor/bin/rector process --dry-run8.1 の「内部関数への null 渡し」には、Rector の NullToStrictStringFuncCallArgRector という個別の規則があり、引数を (string) でくるみます。ただしこれは PHP 8.1 のセットには入っていない任意の規則で、NULL と空文字列を区別していた処理まで一律に空文字列にしてしまいます。全体に当てるより、ログで件数の多い箇所を確かめてから、範囲を絞って使うほうが安全です。
このページの項目の 4 割は非推奨なので、移行の進み具合は「E_DEPRECATED が何件残っているか」でほぼ測れます。本番用の設定(php.ini-production)は E_DEPRECATED を報告しない既定になっているので、移行作業の間は、コンテナに専用の設定を足してすべて出します。
error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /app/var/php-upgrade.logmkdir -p var && : > var/php-upgrade.log
docker run --rm -v "$PWD":/app -w /app \
-v "$PWD/docker/php-upgrade/zz-upgrade.ini":/usr/local/etc/php/conf.d/zz-upgrade.ini \
php:8.4.27-cli vendor/bin/phpunit
# 種類ごとの件数(ファイル名と行番号を落として、同じ通知をまとめる)
grep 'PHP Deprecated' var/php-upgrade.log \
| sed -E 's/^\[[^]]*\] PHP Deprecated: +//; s/ in \/.* on line [0-9]+$//' \
| sort | uniq -c | sort -rn | head -30
# ファイルごとの件数(どこから直すかを決める)
grep -oE 'PHP Deprecated: .* in /app/[^ ]+' var/php-upgrade.log \
| sed -E 's/.* in //' | sort | uniq -c | sort -rn | head -30テストが pdo_mysql などの拡張を必要とするなら、公式イメージをそのまま使わず、下の「コーディングエージェントに一括で任せる」にある Dockerfile で拡張を足したイメージに置き換えてください。PHPUnit のテストが無い、あるいは画面を通らないと動かない処理が多い場合は、同じ設定をステージングの PHP-FPM に入れ、普段の操作を一通り流してからログを集計します。8.0 起点で件数が膨らみやすいのは Passing null to parameter(8.1 の null 渡し)と Creation of dynamic property(8.2 の動的プロパティ)です。どちらもごく普通の書き方から出る通知なので、件数の多い順に直し、0 件になるまで繰り返します。
php.ini の error_reporting では、ビット演算の | や &、~ の組み合わせも書けますが、移行作業中は E_ALL だけにしておくと、何かを除外したまま見落とす心配がありません。
8.1 の htmlspecialchars() の既定値、PDO のネイティブ型、8.4 の round() は、通知が出ないので 4. のログには現れません。8.0 の環境と 8.4.27 の環境に同じリクエストを送り、返ってきた HTML や JSON を比べるのが確実です。
# urls.txt に主要な画面と API の URL パスを 1 行ずつ書いておく
mkdir -p out/old out/new
while read -r path; do
name=$(echo "$path" | tr '/?=&' '____')
curl -s "https://old.example.test$path" > "out/old/$name.html"
curl -s "https://new.example.test$path" > "out/new/$name.html"
done < urls.txt
diff -r out/old out/new差分に ' が増えていれば 8.1 の ENT_QUOTES、JSON の "1" が 1 になっていれば PDO のネイティブ型です。差が出たら、新しい出力を正とするのか、フラグや PDO::ATTR_STRINGIFY_FETCHES で旧来の出力を保つのかを決めます。この判断はエージェントには任せず、画面や API の利用者を知っている人が行います。
ステージングの PHP を 8.4.27 に替え、4. のログが 0 件、5. の差分が説明のつくものだけになったら、本番を切り替えます。php.ini や FPM のプール設定は 8.0 のものを持ち込まず、8.4.27 の php.ini-production から作り直し、必要な値だけを移します。切り替え後もしばらくは log_errors を有効にしたまま、E_DEPRECATED と E_WARNING の件数を見ておくと、テストで通らなかった経路の問題を拾えます。
このページの一覧は、人が読むチェックリストであると同時に、Claude Code や Codex にそのまま渡せる作業指示の材料でもあります。8.0 系から 8.4.27 までの変更は項目数が多いので、全部を手で追うより、機械で拾えるものは静的解析と Rector に任せ、エージェントには「一覧と突き合わせて残りを探し、直した理由を項目 id で説明する」ところまでを頼むのが現実的です。
ただし、エラーを出さずに結果だけが変わる種類の変更は、ツールでもエージェントでも見落としが出ます。この節の最後にある「検収で人が見るところ」は、エージェントの報告を受け取ったあとに必ず人が確認してください。
※ツール名・設定の書き方・コマンドは、2026年10月時点の各公式ドキュメントで確認したものです。
git switch -c php-upgrade-8.4 のように移行専用のブランチを切り、エージェントには main を直接触らせない。php:8.4.27-cli のようにパッチ版まで固定したタグがあります。ホストの PHP ではなく、このコンテナで解析とテストを動かします。docs/php-upgrade/reference.html として保存しておくと、エージェントがネットワークに出られない設定でも一覧を読めます。FROM php:8.4.27-cli
RUN apt-get update && apt-get install -y --no-install-recommends git unzip \
&& rm -rf /var/lib/apt/lists/*
# アプリが使う拡張をここで足す(例: MySQL)
RUN docker-php-ext-install pdo_mysql mysqli
COPY --from=composer/composer:2-bin /composer /usr/bin/composer
WORKDIR /appgit switch -c php-upgrade-8.4
mkdir -p docs/php-upgrade
curl -sL -o docs/php-upgrade/reference.html "https://shinichi.noguchi.jp.net/feature/phpupgrade/8.0.html"
docker build -t php-upgrade:8.4.27 docker/php-upgrade
# 以降のコマンドはこのコンテナの中で動かす
docker run --rm -it -v "$PWD":/app php-upgrade:8.4.27 bashエージェントに頼む前に、決まった規則で拾える変更はツールで拾っておきます。エージェントにもこれらを実行させ、その結果を作業の起点にさせると、読み落としが減ります。
| ツール | 何をするか | 公式 |
|---|---|---|
| PHPCompatibility | PHP_CodeSniffer の規則集。指定した PHP の版で削除・非推奨になった関数や構文を静的に検出する。testVersion に移行先の版を渡す。 | GitHub |
| Rector | PHP の版ごとの書き換え規則を持つ自動リファクタリングツール。rector.php に移行先を指定し、差分を確認してから適用する。 | getrector.com |
| PHPStan | 静的解析。phpVersion で解析の前提にする PHP の版を、level(0〜10)で厳しさを決める。 | phpstan.org |
| Psalm | 静的解析。psalm.xml の phpVersion 属性で前提の版を指定する。未指定なら composer.json の php 指定を読む。PHPStan とどちらか一方で足ります。 | psalm.dev |
| PHPUnit | テストの実行。PHPUnit 自体にも対応する PHP の版があるので、移行先で動く版をサポート表で選ぶ。 | phpunit.de |
| Composer | composer why-not php 8.4.27(prohibits の別名)で移行先の PHP を拒んでいる依存パッケージを、composer outdated --direct で更新が出ている直接依存を一覧にする。 | getcomposer.org |
PHPCompatibility は、README が案内する 10 系を入れます。安定版の最新 9.3.5 は 2019年12月の版で、PHP 8.0 の正式リリースより前のものです。10 系は2026年10月時点でまだプレリリース(10.0.0-alpha2)で、PHP 7.2 以上と PHP_CodeSniffer 4.0.2 以上が必要です。古いアプリの composer.json に入れると依存が衝突することがあるので、その場合は tools/ のような別ディレクトリに解析用の composer.json を作って入れてください。
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"
# 移行先だけで動けばよい場合
vendor/bin/phpcs -ps . --standard=PHPCompatibility --runtime-set testVersion 8.4 --ignore=*/vendor/*
# 切り替えの間、旧版と移行先の両方で動かす必要がある場合は範囲で指定する
vendor/bin/phpcs -ps . --standard=PHPCompatibility --runtime-set testVersion 8.0-8.4 --ignore=*/vendor/*Rector は、composer.json の "php" 指定から移行先を読ませる書き方が公式の推奨です。先に composer.json の require.php を ^8.4 にしてから、引数なしの withPhpSets() を使います。composer.json をまだ変えられないときは、名前付き引数で版を 1 つだけ指定します(名前付き引数なので、Rector は PHP 8.0 以上で動かします)。差分が大きすぎて読めない場合は、規則を安全なものから 1 つずつ足していく withPhpLevel(0) から始め、数字を 1 ずつ上げる方法も公式に用意されています。
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/public',
])
// composer.json の "require": {"php": "^8.4"} を読み、その版までの規則をまとめて適用する
->withPhpSets();
// composer.json を先に変えられない場合は、上の行の代わりに版を 1 つだけ指定する
// (8.4 なら php84、8.5 なら php85。withPhpSets() は 1 回しか呼べない)
// ->withPhpSets(php84: true);composer require rector/rector --dev
vendor/bin/rector process --dry-run # 差分を見るだけ
vendor/bin/rector process # 確認してから適用PHPStan は、phpVersion を指定しないと PHPStan を動かしている PHP の版が前提になります。移行先のコンテナで動かすならそのままでも合いますが、設定ファイルに書いておくほうが取り違えがありません。古いコードにいきなり高いレベルを当てると指摘が多すぎて読めないので、0 から始めて上げていきます。
parameters:
level: 0
phpVersion: 80400 # PHP 8.4。8.5 なら 80500
paths:
- src
- publicClaude Code は、リポジトリ直下の CLAUDE.md を毎回読み込みます。移行の間だけ守らせたい約束をここに書いておくと、指示のたびに繰り返さずに済みます。
## PHP 移行のルール(8.0.30 → 8.4.27) - 参照資料は https://shinichi.noguchi.jp.net/feature/phpupgrade/8.0.html(保存版: docs/php-upgrade/reference.html)。8.0 系から 8.4 までに入った削除・非推奨・挙動変更の一覧で、各項目に id(例: v80-05)がある。 - 実行・解析・テストは Docker の php-upgrade:8.4.27 イメージ(docker/php-upgrade/Dockerfile)で行う。ホストの php は使わない。 - 変更の根拠は、参照資料の項目 id で示す。 - 1 コミットに 1 種類の変更。Rector の自動変換と手作業の修正を同じコミットに混ぜない。 - @ 演算子、error_reporting の変更、空の catch で警告やエラーを隠さない。 - テストの期待値を変えるときは、変える理由を必ず報告する。 - vendor/ 配下は編集しない。依存パッケージは composer で更新する。 - 次の作業の前には止まって確認を求める: composer.json の require の変更、DB のスキーマ・文字コードに関わる変更、php.ini・.htaccess・FPM 設定など本番環境の設定の変更、業務上の計算結果が変わりうる修正。
一括依頼のプロンプトです。段階ごとに止まって報告させる形にしています。一度に最後まで走らせると、途中の判断を人が確かめる機会がなくなるためです。
このリポジトリの PHP を 8.0.30(8.0 系)から 8.4.27 へ移行します。CLAUDE.md(Codex の場合は AGENTS.md)の移行ルールに従ってください。 参照資料: https://shinichi.noguchi.jp.net/feature/phpupgrade/8.0.html(保存版: docs/php-upgrade/reference.html) 参照資料の「起点から移行先までに入った変更の全一覧」が、8.0 系から 8.4 までに入った削除・非推奨・挙動変更のチェックリストです。移行先 8.4.27 より新しい版のブロックは対象外です。各項目の id(例: v80-05)を作業記録に使ってください。 作業は次の 6 段階で進め、段階ごとに止まって報告してください。私の返事があるまで次の段階に進まないでください。 ① インベントリ - composer.json と composer.lock、使っている PHP 拡張、php.ini の設定に依存している箇所、PHP ファイルの数、テストの有無と実行方法を調べ、docs/php-upgrade/inventory.md にまとめる。 - composer why-not php 8.4.27 と composer outdated --direct を実行し、移行先で動かない依存パッケージと、その更新先の版を一覧にする。 - この段階ではコードを変更しない。 ② Rector による自動変換 - rector.php を用意し、vendor/bin/rector process --dry-run の差分を規則ごとに要約して見せる。 - 私が承認した規則だけを適用する。規則ごとにコミットを分け、コミットメッセージに規則名を入れる。 ③ 参照資料の一覧との突き合わせ - 一覧の項目を上から順に 1 つずつ取り上げ、該当しそうな関数名・定数名・INI 名・構文をリポジトリ全体から grep で探す(vendor/ は除く)。 - 結果を docs/php-upgrade/checklist.md に表で残す。列は「項目 id・区分・内容・該当箇所(ファイル:行)・対応(該当なし/修正済み/要判断)」。全項目を 1 行ずつ埋め、飛ばさない。 - エラーにならず結果だけが変わる挙動変更(== の比較、文字コードの既定値、round() や range() の結果など)は、直す前に「要判断」として私に見せる。 ④ Deprecated と警告の全件収集 - php-upgrade:8.4.27 のコンテナで error_reporting=E_ALL、display_errors=stderr、log_errors=On にしてテストと主要な画面・バッチを動かし、E_DEPRECATED・E_WARNING・E_NOTICE を全件ログに出す。 - 1 件ずつ原因を直し、最後にログが 0 件になったことをログの実物で示す。@ 演算子や error_reporting の変更で隠すことは禁止。 ⑤ テスト - 既存のテストと、移行前の出力を記録したスモークテストを移行先で実行し、移行前と結果が一致することを確かめる。一致しないものは差分をそのまま見せる。 - vendor/bin/phpcs --standard=PHPCompatibility --runtime-set testVersion 8.4 と vendor/bin/phpstan analyse を実行し、残った指摘を一覧にする。 ⑥ 変更報告 - 変更内容を表で報告する。列は「項目 id・ファイル・変更前・変更後・理由・テストで確認したか」。 - 自信のない変更、業務ロジックの意味が変わりうる変更、DB や文字コードに関わる変更は別の表に分け、「人の確認が必要」と明記する。
差分ができたら、作業した会話とは別のセッションでレビューさせます。同じ会話の中だと、自分が下した判断をそのまま追認しがちだからです。
これは PHP 8.0.30 から 8.4.27 への移行作業の差分です。git diff main...HEAD を読み、レビューしてください。参照資料: https://shinichi.noguchi.jp.net/feature/phpupgrade/8.0.html(保存版: docs/php-upgrade/reference.html) 次の観点で問題のある箇所を探し、「ファイル:行・問題・根拠になる参照資料の項目 id・修正案」の表で返してください。観点ごとに、問題がなければ「なし」と書いてください。 1. 変更の前後で結果が変わる箇所: == と === の置き換え、文字列と数値の比較、round()・range()・文字列関数の戻り値、PDO で取得した値の型 2. 内部関数に null が渡る箇所を、(string) などのキャストや ?? '' で黙らせていないか。本来 null が来ないはずの値なら、null が来る原因の側を直すべき 3. 動的プロパティを #[\AllowDynamicProperties] で逃がしていないか。逃がした場合はその理由が書かれているか 4. @ 演算子、error_reporting の変更、空の catch で警告やエラーを隠していないか 5. 文字コード: htmlspecialchars() の既定値、mb_* の内部エンコーディング、DB 接続の charset が移行前と同じ結果になるか 6. テストが変更箇所を実際に通っているか。期待値を書き換えて通しただけのテストがあれば指摘する コードは変更せず、レビュー結果だけを返してください。
非対話で動かす場合は claude -p を使います。段階を区切って 1 段階ずつ実行し、出力を人が読んでから次を流します。--allowedTools に挙げたツールは確認なしで使われるので、最初は読み取りと書き出しだけに絞っておくと安全です。
# ③の突き合わせだけを、コードを変更させずに実行する
claude -p "docs/php-upgrade/reference.html の変更一覧(8.0 系から 8.4 まで)を上から 1 項目ずつ取り上げ、リポジトリ(vendor/ を除く)を grep して該当箇所を探し、docs/php-upgrade/checklist.md に「項目 id・区分・内容・該当箇所・対応」の表で書いてください。コードは変更しないでください。" \
--allowedTools "Read,Grep,Glob,Write"
# 差分のレビュー(レビュー用プロンプトを docs/php-upgrade/review-prompt.md に保存しておく)
git diff main...HEAD | claude -p "$(cat docs/php-upgrade/review-prompt.md)" > docs/php-upgrade/review.mdレビュー役は、サブエージェントとして定義しておくと何度でも同じ観点で呼び出せます。.claude/agents/ に置いたファイルはチームで共有できます。
--- name: php-upgrade-reviewer description: PHP 8.0.30 から 8.4.27 への移行差分をレビューする。移行作業のコミット後や PR 作成前に使う。 tools: Read, Grep, Glob, Bash --- あなたは PHP の版移行のレビュー担当です。git diff main...HEAD を読み、docs/php-upgrade/reference.html の変更一覧(8.0 系から 8.4 まで)と照らして、次を確認してください。 1. 変更の前後で結果が変わる箇所(== と ===、文字列と数値の比較、round()・range()、PDO の取得値の型) 2. null を内部関数に渡す箇所をキャストで黙らせていないか 3. 動的プロパティを #[\AllowDynamicProperties] で逃がしていないか 4. @ 演算子・error_reporting の変更・空の catch で警告を隠していないか 5. 文字コードの既定値の変化が結果に出ていないか 6. 期待値を書き換えただけのテストがないか 結果は「ファイル:行・問題・根拠の項目 id・修正案」の表で返し、コードは変更しないこと。
hooks を使うと、Claude Code がファイルを編集するたびに PHPStan を走らせ、指摘をその場で Claude に返せます。PostToolUse の hook が終了コード 2 で終わると、標準エラーの内容が Claude に渡されます(編集そのものは取り消されません)。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/phpstan-changed.sh"
}
]
}
]
}
}#!/bin/bash
# 編集されたファイルのパスを標準入力の JSON から取り出す(jq が必要)
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.php) ;;
*) exit 0 ;;
esac
project="$(cd "$(dirname "$0")/../.." && pwd)"
rel="${file#"$project"/}"
out=$(docker run --rm -v "$project":/app php-upgrade:8.4.27 \
vendor/bin/phpstan analyse --no-progress --error-format=raw "$rel" 2>&1)
if [ $? -ne 0 ]; then
echo "$out" >&2
exit 2
fi
exit 0Codex は AGENTS.md を読みます。リポジトリのルートから作業ディレクトリまでの各階層の AGENTS.md が順に結合され、作業場所に近いものが優先されます。内容は CLAUDE.md の移行ルールと同じで構いません。
# AGENTS.md ## このリポジトリで進めている作業 PHP を 8.0.30(8.0 系)から 8.4.27 へ移行している。 参照資料: https://shinichi.noguchi.jp.net/feature/phpupgrade/8.0.html(保存版: docs/php-upgrade/reference.html)。8.0 系から 8.4 までに入った削除・非推奨・挙動変更の一覧で、各項目に id(例: v80-05)がある。 ## 実行環境 - 解析とテストは Docker の php-upgrade:8.4.27 イメージで行う(docker/php-upgrade/Dockerfile)。ホストの php は使わない。 - 静的解析: vendor/bin/phpcs --standard=PHPCompatibility --runtime-set testVersion 8.4 --ignore=*/vendor/* . - 静的解析: vendor/bin/phpstan analyse - 自動変換: vendor/bin/rector process --dry-run で差分を確認してから適用する ## 守ること - 変更の根拠は参照資料の項目 id で示す。 - 1 コミットに 1 種類の変更。Rector の自動変換と手作業の修正を混ぜない。 - @ 演算子、error_reporting の変更、空の catch で警告を隠さない。 - テストの期待値を変えたときは理由を報告する。 - vendor/ 配下は編集しない。 - composer.json の require、DB のスキーマや文字コード、php.ini などの本番設定に関わる変更は行わず、提案として報告に書く。
codex exec は非対話で動き、既定では読み取り専用のサンドボックスで実行されます。ファイルを書き換えさせるときは --sandbox workspace-write を付けます。上の一括依頼プロンプトを docs/php-upgrade/prompt.md に保存し、段階を指定して流します。
# ①インベントリだけ(読み取り専用のまま。最後の報告を -o でファイルに残す)
codex exec -o docs/php-upgrade/01-inventory-report.md \
"docs/php-upgrade/prompt.md を読み、①インベントリだけを実行して結果を報告してください。①以外の段階には進まないでください。"
# ③突き合わせ(checklist.md を書かせるので workspace-write)
codex exec --sandbox workspace-write -o docs/php-upgrade/03-checklist-report.md \
"docs/php-upgrade/prompt.md を読み、③参照資料の一覧との突き合わせだけを実行してください。コードは変更せず、docs/php-upgrade/checklist.md だけを書いてください。"
# プロンプト全体を標準入力から渡すこともできる
cat docs/php-upgrade/prompt.md | codex exec --sandbox workspace-write -必須ではありませんが、次のものは移行作業で役に立ちます。どれも公式のリポジトリかドキュメントで存在と導入方法を確認できたものだけを載せています。Claude Code 向けには、Anthropic の公式プラグインマーケットプレイス(claude-plugins-official)に PHP 向けのプラグインもあります。
| 名前 | 移行作業での使いどころ | 導入 |
|---|---|---|
| php-lsp Claude Code プラグイン | PHP の言語サーバー Intelephense を Claude Code につなぎ、定義元や参照の検索、診断を使えるようにする。関数の呼び出し元を漏れなく追うときに効く。 | npm install -g intelephense のあと /plugin install php-lsp@claude-plugins-official |
| code-modernization Claude Code プラグイン(Anthropic 製) | 古いコードベースの調査、業務ルールの抽出、承認制の計画、移行後の動作が旧コードと同じであることの検証までを段階的に進める。同じ技術の新しい版への移行も対象。大きなシステムでは多数のエージェントが動くので、まず 1 モジュールで試すよう README に書かれている。 | /plugin install code-modernization@claude-plugins-official のあと /code-modernization:modernize |
| Context7 MCP サーバー | ライブラリの版ごとの最新ドキュメントをエージェントに渡す。Rector や PHPStan、フレームワークの新しい書き方を、学習時点の古い知識ではなく現行の資料で確かめさせる。 | リモート https://mcp.context7.com/mcp。Claude Code・Codex それぞれの手順は公式の一覧 |
| GitHub MCP Server MCP サーバー | Issue・PR・ワークフローを操作する。「要判断」の項目を Issue に起こす、移行 PR の CI 結果を読ませる、といった使い方。 | リモート https://api.githubcopilot.com/mcp/。Claude Code の手順は公式ガイド |
| Playwright MCP MCP サーバー | ブラウザを操作する。移行前と移行後の画面を同じ手順で開き、表示や入力の結果を比べる回帰確認に使う。 | claude mcp add playwright npx @playwright/mcp@latestCodex: codex mcp add playwright npx "@playwright/mcp@latest" |
| Serena MCP サーバー | 言語サーバーを使い、シンボル単位でコードを検索・編集する道具をエージェントに渡す。対応言語に PHP が含まれる。 | 公式 README の手順(uv が必要) |
| Laravel Boost Laravel アプリの場合 | Laravel 公式の MCP サーバーとガイドライン。PHP と Laravel の版、DB スキーマ、直近のエラーログをエージェントに読ませ、インストール済みパッケージの版に合ったドキュメントを検索させる。ガイドラインの対象は Laravel 10.x〜13.x。 | composer require laravel/boost --dev のあと php artisan boost:install |
Context7・GitHub・Playwright・Serena・Laravel Boost は、同じ公式マーケットプレイスの外部プラグイン(external_plugins)としても登録されています。外部プラグインの中身は Anthropic が検証したものではない、と README に書かれているので、入れる前に各プロジェクトのページを確認してください。
エージェントの報告が「テストはすべて通過」でも、次の項目は人が差分と実際の出力を見て確かめます。どれもエラーを出さずに結果だけが変わる種類のものです。
== による比較。PHP 8.0 で数値と数値でない文字列の比較規則が変わり、0 == "a" が true から false になった。switch や in_array() の判定も同じ影響を受ける。htmlspecialchars() などの既定の文字コードやフラグの変化、mb_* の内部エンコーディング、DB 接続の charset。round() と range() の結果。金額や件数の端数処理、ページ送りの範囲を、移行前の出力と突き合わせる。=== の比較や JSON の出力が変わる。null を渡している箇所。キャストで警告を消しただけになっていないか。#[\AllowDynamicProperties] で逃がした箇所が、本当にそのままでよいか。E_DEPRECATED がゼロであること。エージェントの報告ではなく、移行先のコンテナで error_reporting=E_ALL にして取ったログの実物で確認する。8.0 系から 8.5 までに経由する各版の、php.net の移行ガイドと php-src の UPGRADING です。このページの一覧は、これらを項目ごとに整理したものです。
※2026年10月時点の公開情報に基づきます。バックポートのブランチの中身(版番号、コミット)は 2026-10-23 に php-src のリポジトリで確認しました。
今回の脆弱性(PHP2Shell、CVE-2026-109618 / CVE-2026-109619)の仕組みと、サーバー側でまず済ませる更新・緩和策は、特集トップにまとめています。8.0 系のサーバーをバックポートやベンダーの修正でしのいでいる場合は、先にそちらで抜けが無いかを確かめてください。他の起点のページへのリンクもすべて特集トップにあるので、ブックマークは特集トップを 1 つ保存しておけば足ります。
本特集に掲載したコード・コマンド・設定例・エージェント向けプロンプトは、あくまで説明のための例です。お使いのソースコードや環境でそのまま動くこと、改修が漏れなく完了することを保証するものではありません。各変更点の最終確認は、php.net の公式 ChangeLog・移行ガイド、php-src の UPGRADING など一次情報で必ず行ってください。
本特集の内容は現状有姿で提供するものであり、正確性・完全性・有用性について、瑕疵担保責任・契約不適合責任を含め、当方(野口真一)はいかなる保証も行いません。本特集の内容やコードを利用したこと、または利用できなかったことにより生じた損害について、当方は一切の責任を負いません。
「8.4 と 8.5 のどちらにするか」「Deprecated が多すぎてどこから手を付けるか」といった段階から、
移行の計画づくりまで、30 分の無料相談で承ります。