PHP 8.5は2025年11月20日にリリースされたマイナーバージョンで、2026年10月21日に出た8.5.12が現在のパッチ版です。パイプ演算子|>、clone()での一部プロパティ差し替え(clone with)、戻り値の使い忘れを警告する#[\NoDiscard]、規格準拠のURI拡張が主な追加で、致命的エラーにもスタックトレースが出るようになりました。この記事では、同じコードをPHP 8.4.24と8.5.8で実行して何が変わったかを確かめ、非推奨の警告文、性能の実測値、移行の順番まで1本にまとめます。
まとめ:PHP 8.5の変更点の要点
- 8.5.0は2025-11-20リリース。8.5.12(2026-10-21)が現行パッチで、バグ修正は2027-12-31、セキュリティ修正は2029-12-31まで続きます。
- 8.4との一番の違いは新構文です。
|>・clone with・(void)キャスト・定数へのアトリビュートを1か所でも書いたファイルは、8.4では読み込んだ時点でParse errorになります。 - 致命的エラー(メモリ不足・実行時間超過)にスタックトレースが付きます。INIの
fatal_error_backtracesで制御し、既定は有効です。 - 非推奨はバッククォート演算子、
(integer)等の別名キャスト、caseのセミコロン、nullの配列キー、curl_close()などです。標準のエラー処理ではDeprecatedが出るだけで処理は続きます。 - 性能は一律には上がりません。手元の計測では例外の生成が約26%、
array_map()が約22%短縮した一方、urlencode()は約26%遅くなりました。自分の環境で測ってから判断します。 - OPcacheは本体に組み込まれ常に読み込まれます。php.iniに
zend_extension=opcache.soが残っていると警告になるので削除します。
PHP 8.5のリリース日・パッチ版・サポート期限
8.5.0から8.5.12までのリリース日
8.5系のパッチ版はおおむね4週間おきに出ています。日付はphp.netのリリース情報(JSON)から取得した値です。パッチ版は不具合とセキュリティの修正だけで、新機能は入りません。8.5.13は2026年10月26日時点で未公開です。
| 版 | リリース日 | 版 | リリース日 |
|---|---|---|---|
| 8.5.0 | 2025-11-20 | 8.5.7 | 2026-06-04 |
| 8.5.1 | 2025-12-18 | 8.5.8 | 2026-07-02 |
| 8.5.2 | 2026-01-15 | 8.5.9 | 2026-07-30 |
| 8.5.3 | 2026-02-12 | 8.5.10 | 2026-08-27 |
| 8.5.4 | 2026-03-12 | 8.5.11 | 2026-09-24 |
| 8.5.5 | 2026-04-09 | 8.5.12 | 2026-10-21 |
| 8.5.6 | 2026-05-07 | – | – |
8.5.12は、PHP本体の脆弱性CVE-2026-109618・CVE-2026-109619を修正したセキュリティリリースです。影響する版と更新できない場合の設定はPHP2Shellとは:CVE-2026-109618・109619の影響範囲と修正版・更新できない場合の設定【2026年10月】で扱っています。8.5系を使うなら8.5.12未満のまま運用しないでください。
サポート期限と次のPHP 8.6
php.netのサポート版一覧では、各版に2年のバグ修正(アクティブサポート)と、その後2年のセキュリティ修正が付きます。
| 版 | 初回リリース | バグ修正の終了 | セキュリティ修正の終了 |
|---|---|---|---|
| PHP 8.5 | 2025-11-20 | 2027-12-31 | 2029-12-31 |
| PHP 8.4 | 2024-11-21 | 2026-12-31 | 2028-12-31 |
| PHP 8.3 | 2023-11-23 | 2025-12-31(終了) | 2027-12-31 |
| PHP 8.2 | 2022-12-08 | 2024-12-31(終了) | 2026-12-31 |
8.2は2026年12月31日でセキュリティ修正も終わります。8.2から上げるなら、8.4を経由せず8.5へ直接移る方が次の移行まで1年長く使えます。全系列の期限と移行の手順はPHPのサポート期限一覧:8.2は2026年末で終了、版ごとのEOLと移行手順【2026年10月】にまとめています。
次の版は「PHP 9」ではなくPHP 8.6です。PHP 8.6のリリース計画では2026年10月21日にRC4が出ており、正式版(GA)は2026年11月19日の予定です。RC版は本番利用向けではないため、2026年10月時点で本番に入れられる最も新しい系列は8.5です。8.6へ上げるのは、正式版が出て使っているフレームワークや拡張の対応が揃ってからにします。
PHP 8.4と8.5の違い:同じコードで見るエラーの発生時点
この記事のコードは、Intel Core i9-9880H・macOS 26.6.2のHomebrew版PHP 8.4.24と8.5.8を、php.iniを読まないphp -nで実行しました。下の章で使う8本のファイルを8.4.24で動かすと、次の結果になります。
| ファイル(8.5の機能) | PHP 8.4.24での結果 | 落ちる時点 |
|---|---|---|
pipe.php(|>) |
Parse error: unexpected token “>” | 読み込み時 |
clone_with.php(clone($obj, [...])) |
Parse error: unexpected token “,” | 読み込み時 |
nodiscard.php((void)キャスト) |
Parse error: unexpected identifier “save” | 読み込み時 |
| const_attr.php(定数のアトリビュート) | Parse error: unexpected token “const” | 読み込み時 |
static_aviz.php(静的プロパティのprivate(set)) |
Fatal error: Static property may not have asymmetric visibility | コンパイル時 |
| uri.php(URI拡張) | Error: Class “Uri\Rfc3986\Uri” not found | その行の実行時 |
filter.php(FILTER_THROW_ON_FAILURE) |
Error: Undefined constant | その行の実行時 |
| curl_share.php(永続cURL共有) | Error: Call to undefined function | その行の実行時 |
差が出るのは「落ちる時点」です。新しい構文は、その行を通らない分岐の中に書いてもファイル全体が読み込めません。8.4と8.5の両方で動かすライブラリやプラグインでは、|>・clone with・(void)は8.4のサポートを切るまで使えません。一方、新しい関数・クラス・定数は呼んだときに初めて落ちるので、function_exists('array_first')やclass_exists(Uri\Rfc3986\Uri::class)で分岐すれば共存できます。#[\NoDiscard]属性そのものは8.4では無視されるだけで、エラーにはなりません。
8.4側の新機能(プロパティフック・array_find()・非対称可視性)はPHP 8.4の新機能と変更点|プロパティフック・配列検索関数・非推奨対応を実行結果で確認で、8.4.24の実行結果つきで解説しています。
コードの書き方を変える新構文
パイプ演算子(|>)による左から右への連結
|>は左の値を右の呼び出し可能な値に渡し、その結果をさらに右へ流します。str_replace(' ', '-', strtolower(trim($s)))のように内側から読んでいた処理を、実行順のまま書けます。仕様はRFC: Pipe operator v3です。
<?php
$slug = " Hello World PHP "
|> trim(...)
|> strtolower(...)
|> (fn($s) => str_replace(" ", "-", $s));
var_dump($slug);
function add(int $a, int $b): int { return $a + $b; }
echo 1 |> (fn($x) => add($x, 10)), "\n"; // 引数が2つ要る関数はアロー関数で包む
echo 1 |> add(...), "\n"; // 包まずに渡すと ArgumentCountError
$ php -n pipe.php
string(15) "hello-world-php"
11
Fatal error: Uncaught ArgumentCountError: Too few arguments to function add(), 1 passed in pipe.php on line 10 and exactly 2 expected in pipe.php:8
Stack trace:
#0 pipe.php(10): add(1)
#1 {main}
thrown in pipe.php on line 8
右側に置けるのは引数を1つ受け取るcallableだけです。add(...)のように引数が2つ必要な関数をそのまま渡すとArgumentCountErrorになるので、アロー関数で包んで残りの引数を補います。アロー関数を|>の右に置くときは括弧で囲む必要があります。
clone withによるreadonlyプロパティの差し替え
cloneがclone($object, ["プロパティ名" => 値])の形で第2引数を取れるようになり、複製と同時に一部のプロパティを差し替えられます。readonlyクラスで「金額だけ変えた新しいインスタンス」を返すwith系メソッドが1行で書けます。
<?php
final class Money {
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
public function withAmount(int $amount): static {
return clone($this, ["amount" => $amount]);
}
}
$a = new Money(100, "JPY");
$b = $a->withAmount(250);
var_dump($a->amount, $b->amount, $b->currency);
clone($a, ["amount" => 999]); // クラスの外から readonly を書き換えようとする
$ php -n clone_with.php
int(100)
int(250)
string(3) "JPY"
Fatal error: Uncaught Error: Cannot modify protected(set) readonly property Money::$amount from global scope in clone_with.php:17
Stack trace:
#0 clone_with.php(17): clone(Object(Money), Array)
#1 {main}
thrown in clone_with.php on line 17
差し替えは可視性を守ります。readonlyプロパティは書き込みがprotected(set)扱いなので、クラスの外からclone($a, ["amount" => 999])と書くとErrorになります。privateプロパティを外から差し替えようとした場合も「Cannot access private property」で止まりました。一方、readonlyでないpublicプロパティはクラスの外からもclone($p, ["x" => 5])で差し替えられます。readonlyやprivateのプロパティを差し替えるときは、クラスの中のwith系メソッドに書きます。
#[\NoDiscard]と(void)キャストによる戻り値の使い忘れ検出
#[\NoDiscard]を付けた関数・メソッドの戻り値を捨てると、実行時にWarningが出ます。保存の成否や検証結果のように、受け取らないとバグになる戻り値に付けます。わざと捨てるときは(void)キャストで警告を消します。
<?php
#[\NoDiscard("保存できたかを確認してください")]
function save(array $row): bool { return true; }
save(["id" => 1]); // 戻り値を捨てると警告
$ok = save(["id" => 1]); // 受け取れば警告なし
(void) save(["id" => 1]); // 意図的に捨てるときは (void)
echo "done\n";
$ php -n nodiscard.php
Warning: The return value of function save() should either be used or intentionally ignored by casting it as (void), 保存できたかを確認してください in nodiscard.php on line 5
done
警告は5行目だけで、変数で受けた6行目と(void)の7行目は無警告でした。属性の第1引数の文は警告文の末尾に付きます。
定数へのアトリビュートと定数式のクロージャ
constで宣言する定数にアトリビュートを付けられるようになり、#[\Deprecated]を定数にも使えます(RFC: Attributes on Constants)。define()で作る定数は対象外です。あわせて、アトリビュートの引数やプロパティの初期値などの定数式の中で、staticなクロージャと第一級callableを使えるようになりました。
<?php
#[\Deprecated("LEGACY_MODE は次のメジャー版で削除します", since: "2.0")]
const LEGACY_MODE = 1;
echo LEGACY_MODE, "\n";
#[Attribute]
final class Validate {
public function __construct(public \Closure $check) {}
}
#[Validate(static function (int $v): bool { return $v > 0; })]
final class Config {}
$rule = (new ReflectionClass(Config::class))->getAttributes()[0]->newInstance();
var_dump(($rule->check)(5), ($rule->check)(-1));
$ php -n const_attr.php
Deprecated: Constant LEGACY_MODE is deprecated since 2.0, LEGACY_MODE は次のメジャー版で削除します in const_attr.php on line 4
1
bool(true)
bool(false)
定数式のクロージャはstaticが必須で、useで外の変数を取り込むことはできません。検証ルールをアトリビュートの引数に直接書けるので、ルールのためだけのクラスを用意しなくて済みます。
静的プロパティの非対称可視性とクラス設計の小さな追加
8.4でインスタンスプロパティに入った非対称可視性(読み取りと書き込みで可視性を分ける記法)が、静的プロパティにも使えるようになりました。
<?php
class Counter {
public private(set) static int $count = 0;
public static function inc(): void { self::$count++; }
}
Counter::inc();
var_dump(Counter::$count); // 読み取りはクラスの外からもできる
Counter::$count = 10; // 書き込みはクラスの中だけ
$ php -n static_aviz.php
int(1)
Fatal error: Uncaught Error: Cannot modify private(set) property Counter::$count from global scope in static_aviz.php:8
Stack trace:
#0 {main}
thrown in static_aviz.php on line 8
このほか、#[\Override]をプロパティに付けられるようになり、コンストラクタのプロパティ昇格でfinalを指定できるようになりました。#[\Attribute]を抽象クラス・enum・interface・traitに付けるとコンパイル時にエラーになる変更も入っており、従来どおり実行時まで遅らせたい場合は新しい#[\DelayedTargetValidation]を使います。
標準ライブラリ・拡張の追加
規格準拠のURI拡張(Uri\Rfc3986\Uri / Uri\WhatWg\Url)
PHP 8.5には、RFC 3986準拠のUri\Rfc3986\Uriと、WHATWG URL標準準拠のUri\WhatWg\Urlを持つURI拡張が常に有効な状態で入っています(RFC: Add RFC 3986 and WHATWG compliant URI parsing support)。コンストラクタは不正な入力で例外を投げ、静的メソッドparse()はnullを返します。
<?php
use Uri\Rfc3986\Uri;
use Uri\WhatWg\Url;
$u = new Uri("https://example.com:8080/a/../b?q=1#top");
var_dump($u->getHost(), $u->getPort(), $u->getPath());
$w = new Url("HTTPS://EXAMPLE.com/a/../b");
var_dump($w->toAsciiString());
// 「@」を2つ含む紛らわしい入力
$in = "https://[email protected]:[email protected]/";
var_dump(parse_url($in)["host"]); // 従来の関数
var_dump(Uri::parse($in)); // RFC 3986 では不正として null
var_dump(Url::parse($in)?->getAsciiHost()); // WHATWG(ブラウザと同じ規則)
$ php -n uri.php
string(11) "example.com"
int(8080)
string(2) "/b"
string(21) "https://example.com/b"
string(12) "evil.example"
NULL
string(12) "evil.example"
「@」を2つ含む入力では、RFC 3986のクラスは不正としてnullを返し、WHATWGのクラスとparse_url()はどちらもホストをevil.exampleと解釈しました。WHATWG側はブラウザと同じ規則で解釈するので、ブラウザが実際に接続する先と一致します。RFC 3986に適合しない入力を拒否する場面ではRFC 3986のクラス、ブラウザに渡すURLを組み立てる場面ではWHATWGのクラスを使い分けます。許可リスト判定では、検証側と実際に接続するブラウザ・HTTPクライアントでURLの解釈が一致することも確認してください。
array_first() / array_last()による先頭値・末尾値の取得
配列の先頭と末尾の値を返すarray_first()・array_last()が追加されました。$array[array_key_first($array)]と書いていた処理が1関数で済み、空配列ならnullを返します。reset()やend()と違って内部ポインタを動かしません。8.4と共存させるならfunction_exists()で分岐するか、symfony/polyfill-php85のような互換レイヤーを使います。
FILTER_THROW_ON_FAILUREとハンドラの取得
filter_var()などのフィルタ関数にFILTER_THROW_ON_FAILUREを渡すと、検証に失敗したときにfalseではなくFilter\FilterFailedExceptionを投げます。FILTER_NULL_ON_FAILUREと同時には指定できず、組み合わせるとValueErrorです。登録中のハンドラを取り出すget_error_handler()・get_exception_handler()も追加されました。
<?php
try {
$age = filter_var("abc", FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE);
} catch (Filter\FilterFailedException $e) {
echo get_class($e), ": ", $e->getMessage(), "\n";
}
set_exception_handler(fn(Throwable $e) => error_log($e->getMessage()));
$prev = get_exception_handler(); // 今のハンドラを取り出せる
var_dump($prev instanceof Closure);
$ php -n filter.php
Filter\FilterFailedException: filter validation failed: filter int not satisfied by 'abc'
bool(true)
永続的なcURL共有ハンドル
curl_share_init_persistent()は、リクエストをまたいで残るcURL共有ハンドル(CurlSharePersistentHandle)を作ります。PHP-FPMのワーカー内でDNSの解決結果・SSLセッション・接続を使い回せるので、同じ外部APIへリクエストごとに接続し直す処理で効きます。
<?php
$share = curl_share_init_persistent([
CURL_LOCK_DATA_DNS,
CURL_LOCK_DATA_SSL_SESSION,
CURL_LOCK_DATA_CONNECT,
]);
var_dump(get_class($share));
$ch = curl_init("https://api.example.com/");
curl_setopt($ch, CURLOPT_SHARE, $share);
try {
curl_share_init_persistent([CURL_LOCK_DATA_COOKIE]);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
$ php -n curl_share.php
string(25) "CurlSharePersistentHandle"
curl_share_init_persistent(): Argument #1 ($share_options) must not contain CURL_LOCK_DATA_COOKIE because sharing cookies across PHP requests is unsafe
Cookieの共有は指定できず、CURL_LOCK_DATA_COOKIEを渡すと「sharing cookies across PHP requests is unsafe」というValueErrorになります。別ユーザーのリクエスト間でCookieが混ざるのを防ぐための制限です。
ほかに、ロケールに合わせて「A、B、C」のような並びを作るIntlListFormatter、書記素単位で編集距離を測るgrapheme_levenshtein()、Dom\Element::$outerHTML、Cookieのpartitioned属性(CHIPS)、getimagesize()のHEIF/HEIC対応と、libxml拡張が読み込まれている場合のSVG対応、定数PHP_BUILD_DATEが追加されています。全体はphp-srcのUPGRADING(php-8.5.0タグ)で確認できます。
致命的エラーのスタックトレース(fatal_error_backtraces)
PHP 8.5では、メモリ上限超過や最大実行時間の超過といった致命的エラーにもスタックトレースが付きます(RFC: Error Backtraces v2)。3段の関数呼び出しの中でメモリを使い切るスクリプトを、8.4.24と8.5.8で実行しました。
<?php
function load(array $rows): array { return array_map(fn($r) => build($r), $rows); }
function build(int $r): string { return str_repeat("x", 1024 * 1024 * $r); }
load([1, 2, 64]);
$ php -n -d memory_limit=32M fatal.php # PHP 8.4.24
Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 67108896 bytes) in fatal.php on line 3
$ php -n -d memory_limit=32M fatal.php # PHP 8.5.8
Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 67108896 bytes) in fatal.php on line 3
Stack trace:
#0 fatal.php(3): str_repeat('x', 67108864)
#1 fatal.php(2): build(64)
#2 [internal function]: {closure:load():2}(64)
#3 fatal.php(2): array_map(Object(Closure), Array)
#4 fatal.php(4): load(Array)
#5 {main}
8.4までは「3行目でメモリが尽きた」ことしか分からず、build()を誰がどの引数で呼んだかはログから追えませんでした。8.5ではload()→array_map()→クロージャ→build(64)の経路と引数の値まで出ます。本番でまれにしか起きないメモリ不足やタイムアウトの原因特定が、再現環境なしで進められます。
この挙動はINIのfatal_error_backtracesで切り替え、既定は1(有効)です。0にすると8.4と同じ1行だけの出力に戻りました。引数の値がログに残ると困る場合は、無効にするよりzend.exception_ignore_args=1を使う方が経路の情報を残せます。
$ php -n -d memory_limit=32M -d zend.exception_ignore_args=1 fatal.php
Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 67108896 bytes) in fatal.php on line 3
Stack trace:
#0 fatal.php(3): str_repeat()
#1 fatal.php(2): build()
#2 [internal function]: {closure:load():2}()
#3 fatal.php(2): array_map()
#4 fatal.php(4): load()
#5 {main}
PHP同梱のphp.ini-productionはzend.exception_ignore_args = Onなので、これを元にした本番環境では引数なしのトレースになります。開発用の設定を本番に流用している場合は、引数に個人情報や認証情報が出ないかを確認してから8.5へ上げてください。
PHP 8.5の性能:速くなる処理と測ると逆になる処理
UPGRADINGに書かれた高速化
UPGRADINGの「Performance Improvements」には、例外オブジェクトの生成の高速化、array_map()・array_filter()・usort()などコールバックを取る配列関数の高速化、urlencode()の高速化、match(true)の真偽値比較のOPcode削減、=== []の特殊化が挙がっています。Clang 19以上でx86_64またはaarch64向けにビルドした場合はTAILCALL VMが既定になり、GCCでビルドしたバイナリと同等の速さになるとも書かれています。
PHP 8.4.24と8.5.8の実測
上の項目のうち5つを、同じマシンで8.4.24と8.5.8を交互に15回ずつ実行して比べました。php -nのためOPcacheとJITは無効の状態です。
<?php
// 実行例: php -n bench.php exception_new
// 引数は exception_new、array_map_closure、usort、urlencode_ascii、match_true のいずれか
$tests = [
'exception_new' => function () {
for ($i = 0; $i < 1_000_000; $i++) { $e = new RuntimeException("x"); }
},
'array_map_closure' => function () {
$a = range(1, 1000);
for ($i = 0; $i < 2_000; $i++) { $b = array_map(fn($v) => $v * 2, $a); }
},
'usort' => function () {
mt_srand(1); $base = [];
for ($i = 0; $i < 100_000; $i++) $base[] = mt_rand();
for ($r = 0; $r < 5; $r++) { $a = $base; usort($a, fn($x, $y) => $x <=> $y); }
},
'urlencode_ascii' => function () {
$s = str_repeat("abc-def_ghi.jkl~mno ", 20);
for ($i = 0; $i < 300_000; $i++) { $x = urlencode($s); }
},
'match_true' => function () {
$c = 0;
for ($i = 0; $i < 5_000_000; $i++) {
$c += match (true) { $i < 10 => 1, $i % 2 === 0 => 2, default => 3 };
}
},
];
$name = $argv[1];
$t = hrtime(true);
$tests[$name]();
printf("%s\t%.1f\n", $name, (hrtime(true) - $t) / 1e6);
| 処理 | 8.4.24 中央値 | 8.5.8 中央値 | 8.5/8.4 | 8.5が速かった回数 |
|---|---|---|---|---|
| 例外の生成 100万回 | 1,003.8ms | 747.1ms | 0.74 | 15/15 |
| array_map(1,000要素×2,000回) | 213.2ms | 165.1ms | 0.78 | 13/15 |
| match(true) 500万回 | 570.4ms | 541.3ms | 0.93 | 11/15 |
| usort(10万要素×5回) | 1,582.9ms | 1,543.2ms | 0.99 | 9/15 |
| urlencode(400字×30万回) | 415.5ms | 540.3ms | 1.26 | 1/15 |
「8.5/8.4」は同じ回の8.4と8.5の比の中央値です。例外オブジェクトの生成は15回すべてで8.5が速くなりました。ただし、このコードはthrow・catchやフレームワーク全体の処理を測っていないため、アプリ全体の短縮率は別に測る必要があります。usort()は差がノイズの範囲に収まりました。urlencode()はUPGRADINGの記述とは逆に、15回中14回で8.5の方が遅くなりました。この計測はHomebrew版を使ったIntel Macの1台だけの結果で、コンパイラやCPU、OPcache・JITの設定で変わります。「8.5にすれば速くなる」と見込んで移行計画を立てず、自分のアプリの重い処理を両方の版で測ってください。
OPcacheの本体組み込みとmax_memory_limit
8.5ではOPcacheが常にPHP本体に組み込まれ、opcache.soは作られなくなりました。opcache.enableとopcache.enable_cliの設定は従来どおり効きますが、php.iniにzend_extension=opcache.soが残っていると警告が出ます。ディストリビューションのphp8.5-opcacheパッケージの扱いを含め、設定の見直しはOPcacheとは?読み方とPHP 8.5の変更点・設定・確認方法で解説しています。読み取り専用のコンテナ向けにはopcache.file_cache_read_onlyが追加され、opcache.jit_hot_loopの既定値は61(素数)に変わりました。
起動時にだけ設定できるmax_memory_limitも追加されました。memory_limitの上限を決めるもので、アプリがini_set()で上限を超える値を設定しようとすると、警告を出して上限値に丸めます。
$ php -n -d max_memory_limit=256M -d memory_limit=128M -r 'ini_set("memory_limit", "1G"); var_dump(ini_get("memory_limit"));'
Warning: Failed to set memory_limit to 1073741824 bytes. Setting to max_memory_limit instead (currently: 268435456 bytes) in Command line code on line 1
string(4) "256M"
ライブラリやスクリプトがmemory_limit=-1に書き換えてサーバーのメモリを使い切る事故を、サーバー側で止められます。起動時にmemory_limit=-1を指定した場合も256Mに丸められました。
非推奨・互換性のない変更と8.5で出る警告
8.5で実行すると出るDeprecatedとWarning
8.4.24では何も出ずに動くコードを、8.5.8で実行しました。この検証環境では、非推奨の診断を出しても処理は続き、従来どおりの結果を返しました。ただし、独自のエラーハンドラで非推奨の診断を例外に変換するアプリでは、処理が中断する場合があります。
<?php
error_reporting(E_ALL);
$n = (integer) "42";
$d = (double) "1.5";
$out = `echo hi`;
switch ($n) {
case 42;
echo "matched\n";
break;
}
$arr = [null => "a"];
var_dump(array_key_exists(null, $arr));
$s = "a9";
$s++;
var_dump($s);
$ch = curl_init();
curl_close($ch);
var_dump(chr(300));
var_dump(ord("ab"));
var_dump((int) 1e20);
var_dump((int) NAN);
[$x] = 123;
class Old { public $v = 1; public function __sleep() { return ["v"]; } }
echo serialize(new Old), "\n";
$ php -n legacy.php # PHP 8.5.8
Deprecated: Non-canonical cast (integer) is deprecated, use the (int) cast instead in legacy.php on line 3
Deprecated: Non-canonical cast (double) is deprecated, use the (float) cast instead in legacy.php on line 4
Deprecated: The backtick (`) operator is deprecated, use shell_exec() instead in legacy.php on line 5
Deprecated: Case statements followed by a semicolon (;) are deprecated, use a colon (:) instead in legacy.php on line 7
matched
Deprecated: Using null as an array offset is deprecated, use an empty string instead in legacy.php on line 11
Deprecated: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead in legacy.php on line 12
bool(true)
Deprecated: Increment on non-numeric string is deprecated, use str_increment() instead in legacy.php on line 14
string(2) "b0"
Deprecated: Function curl_close() is deprecated since 8.5, as it has no effect since PHP 8.0 in legacy.php on line 17
Deprecated: chr(): Providing a value not in-between 0 and 255 is deprecated, this is because a byte value must be in the [0, 255] interval. The value used will be constrained using % 256 in legacy.php on line 18
string(1) ","
Deprecated: ord(): Providing a string that is not one byte long is deprecated. Use ord($str[0]) instead in legacy.php on line 19
int(97)
Warning: The float 1.0E+20 is not representable as an int, cast occurred in legacy.php on line 20
int(7766279631452241920)
Warning: The float NAN is not representable as an int, cast occurred in legacy.php on line 21
int(0)
Warning: Cannot use int as array in legacy.php on line 22
O:3:"Old":1:{s:1:"v";i:1;}
8.4.24では同じファイルが警告なしでmatchedから最後の行まで出力しました。(int) 1e20は8.4でも8.5でも同じ値(7766279631452241920)を返しますが、8.5では「int で表せない」という警告が付きます。金額や件数の計算で巨大な浮動小数点数を整数にしている箇所は、この警告で見つかります。最終行の__sleep()は「ソフト非推奨」でメッセージは出ませんが、__serialize()・__unserialize()への移行が推奨されています(実行時の警告や削除は決まっていません)。
非推奨になった主な機能と置き換え先
一覧はRFC: Deprecations for PHP 8.5とUPGRADINGの4章からの抜粋です。
| 非推奨になったもの | 置き換え先 |
|---|---|
| バッククォート演算子 | shell_exec() |
(boolean) (integer) (double) (binary) |
(bool) (int) (float) (string) |
case 1;(セミコロン終端) |
case 1: |
nullの配列キー・array_key_exists(null, ...) |
空文字列"" |
非数値文字列の++ |
str_increment() |
curl_close() curl_share_close() |
削除(自動で解放) |
finfo_close() imagedestroy() xml_parser_free() |
削除(自動で解放) |
mysqli_execute() |
mysqli_stmt_execute() |
socket_set_timeout() |
stream_set_timeout() |
$http_response_header |
http_get_last_response_headers() |
PDO::MYSQL_ATTR_*等のドライバ固有定数 |
Pdo\Mysql::ATTR_*等 |
Reflection*::setAccessible() |
削除(8.1から効果なし) |
__debugInfo()でnullを返す |
空配列を返す |
DATE_RFC7231 MHASH_*定数 |
使用をやめる |
curl_close()は、CurlHandleがオブジェクトになった8.0から呼んでも何も起きない関数です。8.5の警告文も「it has no effect since PHP 8.0」と書いています。8.0以上だけをサポートするコードなら、呼び出しを消しても動作は変わりません。PDOのドライバ固有定数は設定ファイルに残りやすい箇所です。Laravel 11のプロジェクト雛形のconfig/database.phpはPDO::MYSQL_ATTR_SSL_CAを使っており、8.5では読み込むたびに「Constant PDO::MYSQL_ATTR_SSL_CA is deprecated since 8.5, use Pdo\Mysql::ATTR_SSL_CA instead」が出ます。Laravel 12の雛形はPHP_VERSION_ID >= 80500で分岐し、13の雛形はPdo\Mysql::ATTR_SSL_CAだけを使っています(laravel/laravelの12.xブランチ)。古い雛形から作ったプロジェクトは自分で書き換えます。
設定の削除・PDO定数値・オブジェクト制約などの互換性のない変更
次の変更は警告ではなく挙動が変わるもので、テストで拾う必要があります。
- INIの
disable_classesが削除されました。この設定でクラスを無効にしていたサーバーでは、制限が効かなくなります。 - 配列でない値(
null以外)を[$a] = ...やlist()で分解するとWarningになります(上の実行結果の「Cannot use int as array」)。 - PDOの
PDO::FETCH_GROUP・FETCH_UNIQUE・FETCH_CLASSTYPE・FETCH_PROPS_LATE・FETCH_SERIALIZEの定数値が変わりました。数値を直接書いているコードは結果が変わります。 - 構築済みの
mysqliオブジェクトに再度コンストラクタを呼ぶとErrorになります。 ArrayObjectにenumを渡せなくなりました。
PHP 8.5へ移行する手順と実行環境の対応状況
実行環境ごとの8.5の提供状況
| 環境 | 2026年10月26日時点の状況 |
|---|---|
| Docker公式イメージ | php:8.5-fpm php:8.5-apache=8.5.12 |
| Ubuntu 26.04 LTS | 標準のphpは8.5系(php8.5-cliは8.5.4-0ubuntu1.5) |
| Amazon Linux 2023 | php8.5-8.5.12-1.amzn2023.0.1 |
| Homebrew(macOS) | php=8.5.12、8.4は[email protected] |
| WordPress | 6.9以降でfully supported |
| Laravel | 13はPHP 8.3〜8.5、12は8.2〜8.5 |
出典はDocker公式イメージのタグ定義、Ubuntuのパッケージ一覧(resolute)、Amazon Linux 2023のリリースノート(2023.12.20261021)、Homebrewのphp formula、WordPressのホスティングハンドブック、Laravel 13のリリースノートです。Ubuntuのパッケージは版番号がphp.netの8.5.12と一致しないため、脆弱性への対応はphp -vの数字ではなく、パッケージの更新履歴とUbuntuのセキュリティ通知で確認します。Laravelの版ごとの対応PHPはLaravelのバージョン確認方法|最新バージョンは13・artisanとcomposerの違いとサポート期限で一覧にしています。
PHP 8.5への移行手順:テスト・静的解析・設定・依存関係の確認
- テスト環境のPHPを8.5.12にし、
error_reporting=E_ALLでテストを通してDeprecatedをログに集めます。上の非推奨表の項目はほぼすべてここで見つかります。 - 静的解析のPHPバージョン設定を8.5にします。PHPStanは
phpVersion、PsalmはphpVersion属性で指定できます。設定とレベルの選び方はPHPStanとは?レベル0〜10の違いと導入・ベースライン運用【2.3対応】とPsalmとは?PHP静的解析の使い方とPHPStanとの違い【6.20・7 RC対応】で扱っています。 - php.iniから
zend_extension=opcache.soとdisable_classesを消し、zend.exception_ignore_argsとfatal_error_backtracesの値を決めます。 composer.jsonのrequire.phpと依存パッケージの対応範囲を確認します。8.4のサポートを残す間は、新構文(|>・clone with・(void))をアプリのコードに入れません。- 性能が重要な処理は8.4と8.5で測り比べ、遅くなった処理がないかを本番反映前に確かめます。
独自フレームワークや古いCMSで非推奨の箇所が多く、自社だけでは移行の手間を見積もれない場合は、システムマイグレーション・リプレイスとして、現行コードの調査からPHPの版上げ・インフラの更新までまとめてご相談いただけます。
よくある質問
PHP 8.5のリリース日はいつですか?
PHP 8.5.0は2025年11月20日にリリースされました。その後は約4週間おきにパッチ版が出ており、2026年10月21日の8.5.12が10月26日時点の最新パッチです。
PHP 8.5.13はいつ出ますか?8.5.11との違いは?
8.5.13は2026年10月26日時点で公開されていません。8.5.11(2026-09-24)と8.5.12(2026-10-21)のようなパッチ版の違いは不具合とセキュリティの修正だけで、新機能はありません。8.5.12はCVE-2026-109618・109619を修正しているので、8.5.11以前を使っているなら更新してください。
PHP 8.5にすると性能は上がりますか?
処理によって上がるものと下がるものがあります。今回の計測では例外の生成とarray_map()が速くなり、urlencode()は遅くなりました。重い処理を8.4と8.5で測ってから判断してください。
curl_close()は削除した方がよいですか?
PHP 8.0以上だけで動かすコードなら削除して問題ありません。8.0からcurl_close()は何もしない関数で、ハンドルは、そのオブジェクトへの参照がすべてなくなると自動で解放されます。8.5では呼ぶたびにDeprecatedが出るので、ログを汚さないためにも消しておく方が扱いやすくなります。
PHP 8.5の次はPHP 9ですか?
次はPHP 8.6で、正式版は2026年11月19日の予定です。2026年10月21日にRC4が出ています。PHP 9のリリース時期は決まっていません。