2025年11月20日、PHP 8.5 がリリースされました。今回のアップデートは「読みやすさ」「安全性」「パフォーマンス」の3軸に的を絞った実践的な進化です。目玉である パイプ演算子(|>) は関数型スタイルのコードを PHP に自然に持ち込み、組み込み URI 拡張 はこれまで外部ライブラリに頼っていた URL 操作を標準化します。また Clone With 構文はイミュータブルな設計パターンを言語レベルでサポートします。既存コードの改善から新規設計の方針まで、実務に直結する変更が揃っています。
技術的背景:PHP 8.5 が解決する課題
PHP 8.x シリーズはマイナーバージョンごとに一貫したテーマを持っています。8.0 での JIT・Union Types、8.1 での Enums・readonly プロパティ、8.2 での readonly クラス、8.3 での型付き定数、8.4 でのプロパティフック——そして 8.5 は「処理の流れを直感的にする記法の整備」と「外部依存の標準化」に注力しています。
特に URL 操作は長らく parse_url() の返り値が不完全であること、外部ライブラリ(league/uri など)への依存が避けられないことが課題でした。また、深くネストした関数呼び出しは PHPDoc や変数の中継が増え、可読性とテスト容易性を下げる要因になっていました。8.5 はこれらを言語仕様レベルで解消します。
主要新機能 詳細解説
1. パイプ演算子(|>):処理の流れを左から右へ
課題: strtolower(trim(str_replace(' ', '-', $input))) のようなネストした関数呼び出しは、処理順序が「内側から外側」に読む必要があり、追跡が困難でした。変数を中継すれば読みやすくなりますが、コード量が増えます。
解決策: パイプ演算子 |> は左辺の値を右辺の関数の第一引数として渡します。処理が「上から下・左から右」の自然な流れになり、各ステップの意図が明確になります。第一引数以外の位置に値を渡す場合は、アロー関数(fn($s) => ...)でラップします。
// PHP 8.4 以前:内側から外側に読む必要があり、追跡が難しい
$slug = strtolower(str_replace('.', '', str_replace(' ', '-', trim(' PHP 8.5 '))));
// PHP 8.5:処理の順序が上から下に一目でわかる
$slug = ' PHP 8.5 '
|> trim(...) // 1. 前後の空白を除去
|> (fn($s) => str_replace(' ', '-', $s)) // 2. スペースをハイフンに
|> (fn($s) => str_replace('.', '', $s)) // 3. ドットを除去
|> strtolower(...); // 4. 小文字に変換
// 実用例:バリデーションパイプライン
$result = $request->input('email')
|> trim(...)
|> strtolower(...)
|> (fn($email) => filter_var($email, FILTER_VALIDATE_EMAIL));
Laravel のコレクションや Eloquent のメソッドチェーンに慣れている方には、配列・文字列操作においても同様のスタイルが書けるようになる変化です。ただし、パイプ演算子は第一引数への渡しのみであるため、引数の位置が異なる関数には毎回アロー関数が必要になる点は把握しておきましょう。
2. 組み込み URI 拡張:URL 操作を標準 API で
課題: 組み込みの parse_url() は返り値が不定(スキームが省略されると false)で型安全でなく、URL の構築や正規化には別途ライブラリが必要でした。セキュリティ面でも、手動でのエスケープ漏れがリスクになっていました。
解決策: URI 拡張は RFC 3986(Uri\Rfc3986\Uri)と WHATWG URL(Uri\WhatWg\Url)の2つのクラスを提供します。RFC 3986 は一般的な URI の厳格なパース、WHATWG はブラウザ準拠の URL 解釈(相対 URL の解決など)に向いています。どちらも型付きの getter を持ち、不正な URI はコンストラクタで例外を投げるためフェイルファスト設計になっています。
use Uri\Rfc3986\Uri;
use Uri\WhatWg\Url;
// RFC 3986:厳格なパース(API の URI など)
$uri = new Uri('https://php.net/releases/8.5/en.php');
var_dump($uri->getScheme()); // string(5) "https"
var_dump($uri->getHost()); // string(7) "php.net"
var_dump($uri->getPath()); // string(21) "/releases/8.5/en.php"
// WHATWG:ブラウザ準拠の URL 解釈(相対 URL の解決など)
$base = new Url('https://example.com/path/');
$resolved = $base->resolve('../other');
var_dump((string) $resolved); // "https://example.com/other"
// 不正な URI はコンストラクタで例外を投げる(フェイルファスト)
try {
$invalid = new Uri('not a valid uri');
} catch (\ValueError $e) {
echo $e->getMessage(); // 不正な URI として即座に検出
}
3. Clone With 構文:Wither パターンを言語仕様に
課題: readonly クラスでは一度セットしたプロパティを変更できません。「一部のプロパティだけ変えた新しいインスタンスを作りたい」という Wither パターンでは、__clone() をオーバーライドするか、各プロパティを手動でコピーする定型コードが必要でした。
解決策: clone($obj, ['prop' => 'value']) の形式で、変更したいプロパティだけを指定してクローンを作成できます。readonly プロパティでも機能し、指定しなかったプロパティは元の値がそのまま引き継がれます。
readonly class Money {
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
// Wither パターン:変更したいプロパティだけ指定してクローン
public function withAmount(int $amount): self {
return clone($this, ['amount' => $amount]);
}
public function withCurrency(string $currency): self {
return clone($this, ['currency' => $currency]);
}
}
$price = new Money(1000, 'JPY');
$sale = $price->withAmount(800); // currency は 'JPY' を維持
$usd = $price->withCurrency('USD'); // amount は 1000 を維持
// 複数プロパティを同時に変更することも可能
$converted = clone($price, ['amount' => 10, 'currency' => 'USD']);
4. #[\NoDiscard] 属性:戻り値の見落としを警告で防ぐ
課題: validate_token() や save() のような「戻り値を使って初めて意味がある」関数の戻り値を、うっかり捨てるバグは見つけにくいものです。静的解析ツールで検出できることもありますが、言語仕様としての保証はありませんでした。
解決策: #[\NoDiscard] 属性を付けた関数の戻り値が使われなかった場合、実行時に E_WARNING レベルの警告が発生します。意図的に無視する場合は (void) キャストを使うことで、「これは意図的な破棄である」という意図を明示できます。
#[\NoDiscard('検証結果を確認してください')]
function validate_token(string $token): bool {
return hash_equals($expected, $token);
}
// 戻り値を無視すると Warning が発生
validate_token($token);
// Warning: The return value of validate_token() should not be discarded: 検証結果を確認してください
// 正しい使い方:戻り値を確認する
if (!validate_token($token)) {
throw new \RuntimeException('Invalid token');
}
// 意図的に捨てる場合:(void) キャストで明示
(void) validate_token($token); // 警告なし(意図的な破棄として記録される)
5. array_first / array_last:配列の先頭・末尾を安全に取得
課題: 配列の先頭や末尾の要素を取得するには reset($arr)(副作用あり)や $arr[array_key_first($arr)](冗長)などを使う必要がありました。空配列での $arr[0] は Undefined offset エラーを引き起こす可能性もありました。
$events = ['signup', 'login', 'purchase'];
// シンプルに先頭・末尾を取得(空配列なら null を返す)
$first = array_first($events); // 'signup'
$last = array_last($events); // 'purchase'
// ?? 演算子と組み合わせてデフォルト値を設定
$latest = array_last($events) ?? 'no events';
// コールバックで条件を指定することも可能
$firstPaid = array_first($orders, fn($o) => $o->isPaid());
6. 定数式でのクロージャと永続的 cURL シェアハンドル
定数式でのクロージャとして、静的クロージャや第一級コールバックが属性の引数・プロパティのデフォルト値・定数などで使えるようになりました。属性にコールバックを渡して処理を定義するパターンが、より宣言的に書けます。
#[Validate(fn($v) => $v > 0)]
public int $amount;
永続的 cURL シェアハンドルとして、curl_share_init_persistent() が追加されました。従来の curl_share_init() はリクエストごとにハンドルが破棄されていましたが、永続ハンドルは DNS キャッシュや接続プールをリクエストを跨いで保持します。外部 API を頻繁に叩くアプリケーションで接続初期化コストを削減できます。
エンジニアパイプ演算子、Laravel のコレクションのメソッドチェーンに慣れた感覚で書けるのが嬉しいですね。ただ第一引数への渡しのみという制約は頭に入れておく必要があります。アロー関数でラップするひと手間は残りますが、可読性の向上は大きいです。
アーキテクトURI 拡張が RFC 3986 と WHATWG の両方に対応しているのは実用的ですね。API のエンドポイント検証には RFC 3986、ユーザー入力の URL 正規化には WHATWG と使い分けられるので、league/uri への依存を外せそうです。
その他の改善点
致命的エラーのバックトレースとして、最大実行時間超過などの致命的エラー発生時にスタックトレースが含まれるようになりました。これまでは「どこで無限ループが起きているか」の特定が難しかったケースで、デバッグが格段にしやすくなります。
属性の対象拡大として、定数への属性付与が可能になり、#[\Override] がプロパティに、#[\Deprecated] がトレイトと定数に使用できるようになりました。静的プロパティの非対称可視性(public private(set) static)や、Closure::getCurrent() による匿名関数内での再帰の簡略化、Cookie の Partitioned 属性(CHIPS)のサポートも追加されています。
移行時の注意点:非推奨・互換性の変更
既存プロジェクトを PHP 8.5 へ移行する際に確認すべき変更をまとめます。
-
バックティック演算子の非推奨:
`ls -la`のような形式が非推奨になりました。shell_exec('ls -la')に移行してください。grep で`を検索すると影響箇所を特定できます。 -
非正規のキャスト名の非推奨:
(boolean)・(integer)・(double)・(binary)が非推奨になりました。それぞれ(bool)・(int)・(float)・(string)を使用してください。 -
INI 設定の変更:
disable_classes設定が削除されました。この設定に依存したセキュリティ制御を行っている場合は代替手段への移行が必要です。 -
配列オフセットへの null:
array_key_exists(null, $arr)などが非推奨になりました。nullの代わりに空文字列''を使用してください。
まとめ
PHP 8.5 は「外部ライブラリへの依存を減らし、言語仕様で安全・明快に書ける範囲を広げる」アップデートです。改めてポイントを整理します。
- パイプ演算子(|>):ネストした関数呼び出しを「上から下」の自然な流れに書き直せる。第一引数への渡しのみという制約は把握した上で活用する。
- 組み込み URI 拡張:RFC 3986 と WHATWG の2クラスを使い分け、league/uri などの外部依存を標準 API に置き換えられる。
- Clone With 構文:readonly クラスの Wither パターンが定型コードなしに書けるようになり、イミュータブルな設計がより現実的に。
- #[\NoDiscard] 属性:重要な戻り値の見落としを実行時警告で検出でき、
(void)キャストで意図的な破棄を明示できる。 - 移行時の注意点:バックティック演算子・非正規キャスト名・
disable_classes設定の削除は既存プロジェクトで影響が出やすいため早めに確認を。
まず既存コードの中で「深くネストした関数呼び出し」や「parse_url() を使った URL 操作」を探してみることが、8.5 への移行と恩恵を実感する最短ルートになるはずです。