Journal / Tech
最新の PHP 8.5 を、AI はどこまで書けるか
コードを書くのに AI を使うのが当たり前になりました。ただ、AI の知識には鮮度の限界があります。学習データはどこかで締め切られていて、それより後に決まった仕様は、当然ながら手薄です。そしてそのズレが最もはっきり出るのが、言語の最新版です。
PHP 8.5 は 2025 年 11 月にリリースされました。パイプ演算子や clone with といった、これまでになかった構文が入っています。では、これらを AI 支援で書くと、実際どこでつまづくのか。手元で PHP 8.5.8 を動かし、小さなコードを書いて走らせながら確かめました。
先に結論を述べると、AI は一部の新構文を言語仕様どおりに書けません。ただしその誤り方には規則性があり、どの機能で誤りやすいかはある程度予測できます。順に見ていきます。
題材:小さな値オブジェクト
価格を表す小さな値オブジェクト Money を、8.4 と 8.5 の新機能をひととおり使って書くことにしました。使ったのは次の六つです。
- 非対称可視性(8.4)—
public private(set)。外からは読めるが書けない - プロパティフック(8.4)— 実体を持たない算出プロパティ
- clone with(8.5)— オブジェクトを複製しつつ一部を差し替える
- パイプ演算子
|>(8.5)— 中間変数なしで値を関数に流す array_first/array_last(8.5)— 配列の先頭と末尾を取る組み込み関数#[\NoDiscard]属性(8.5)— 戻り値を捨てると警告する
いずれもリリースノートの目玉です。裏を返せば、AI の学習データにはまだ十分に載っていない可能性が高い機能でもあります。実行環境は Docker 上の PHP 8.5.8。使ったクラスの全文は、記事末尾に載せます。
最初に問題になったのは clone with
素直に書いた Money を走らせて、最初に返ってきたのは実行結果ではなく構文エラーでした。原因は clone with です。機能の名前と、各所の紹介記事の語り口から、自然に書くとこうなります。
#[\NoDiscard('戻り値の新しい Money を使ってください')]
public function withAmount(int $amount): static
{
return clone $this with { amount: $amount };
}
「clone に with を続け、波かっこで差し替えるプロパティを並べる」。読みやすく、いかにもありそうな形です。しかし本物の 8.5 はこれを受け付けません。
Parse error: syntax error, unexpected identifier "with" in money.php on line 29
正しい構文には、そもそも with キーワードがありません。clone with の実体は、clone() に渡す第二引数でした。
return clone($this, ['amount' => $amount]);
機能名は「clone with」なのに、書くときに with は登場しない。この名前と構文の乖離こそ、AI がいちばん誤りやすい箇所です。学習データが薄いところでは、モデルは機能名の語感や、他言語の似た構文(多くの言語の with 式)に影響されて、それらしい形を生成してしまう。今回はまさにそれが起きました。
もっとも、これは AI に限った話ではありません。正直に言えば、書いた本人も最初は with { … } の形で書いていました。名前から構文を推測すれば人も同じように間違えるわけで、AI はその人の癖を平均的になぞっている、と見るのが実態に近いでしょう。
念のため候補をいくつか実機で試しましたが、with を使う書き方はいずれも構文エラーで、通ったのは clone($obj, [...]) の一つだけでした。
残りは、素直に書いて通った
clone with を直したあとは順調でした。残る五つは、最初に書いたまま動きます。実行結果です。
1,200 JPY ← プロパティフックで算出した formatted
960 JPY ← clone(...) で複製し amount を差し替え
Sale, New, Limited ← パイプ演算子 |> のチェーン
Sale / Limited ← array_first / array_last
Warning: The return value of method Money::withAmount() should either be
used or intentionally ignored by casting it as (void), 戻り値の… ← #[\NoDiscard] が発火
パイプ演算子は、文字列を整える一連の処理をそのまま繋げました。
$s |> trim(...) |> strtolower(...) |> ucfirst(...)
#[\NoDiscard] も期待どおり働きます。withAmount() の戻り値を受け取らずに呼び捨てると、属性に添えたメッセージ付きで警告が出ました。非対称可視性とプロパティフックも、宣言の形を含めて過不足なく生成できています。
正しく書けないのは「形が直感と違う機能」
ここまでを並べると、境界が見えてきます。AI が誤ったのは clone with だけで、しかもそれは名前から連想される形と、実際の構文がずれている機能でした。逆に、パイプ演算子や属性のように、記法が素直で他言語にも類例があるものは、学習データが薄くても正しく書けています。
つまり問題は「新しさ」そのものではありません。新しくても、形が直感どおりなら通る。注意すべきは、新しく、かつ形が直感と異なる構文です。移行やレビューで確認すべき箇所も、そこに絞れます。
もちろん、今回試したのは小さな値オブジェクト一つで、8.5 の全機能を網羅したわけではありません。URI 拡張や定数式まわりなど、触れていない領域は残っています。それでも「形が直感と異なるところで誤る」という傾向は、題材を変えても大きくは変わらないはずです。
この見立ては、前に TypeScript 7 のネイティブコンパイラを計測したときの結果とも重なります。あのときは「設定が古いと型チェックが一行も走らない」という、順序の問題が最初に来ました。今回も同じで、AI を新しい対象に使うと、機能そのものの難しさより先に、AI の知識と最新仕様とのズレが問題になります。
実務では、どう補正するか
対処は難しくありません。要は、薄い知識を外から補ってやることです。
- 一次情報を渡す — 該当機能の公式マニュアルや RFC の抜粋を文脈に入れる。clone with のような構文物は、これだけで一気に精度が上がります
- 走らせて確かめる — 新構文を含むコードは、生成物を鵜呑みにせず、必ず実機で一度通す。今回のように構文エラーはすぐ露見します
- 型と宣言で誘導する — 期待するシグネチャや戻り値の型を先に書いておくと、AI はそれに合わせて本体を埋める
AI を使うようになっても、公式ドキュメントをよく読んで確認する、という基本は変わりません。
まとめ
最新の PHP 8.5 を AI 支援で書くと、大半の新機能は素直に通り、clone with のように名前と構文が乖離した一点だけ、言語仕様どおりに書けない。難所は「新しさ」ではなく「直感とのズレ」にあり、そこは一次情報を渡して実機で確かめれば埋められる。
「AI が最新版を書けるか」への答えは、単純な「書ける/書けない」ではありません。書ける、ただし形が直感と異なる構文だけは、人が確かめる。 新しい版を追いかけながら仕事で使う以上、この線引きを持っておくのが現実的だと思います。
付録:実際に走らせたコード
記事で使った Money の全文です。clone with は修正後(clone($this, [...]))の形にしてあります。Docker があれば、php:8.5-cli イメージでそのまま動きます。
<?php
declare(strict_types=1);
final class Money
{
// 非対称可視性 (8.4): 外からは読めるが書けない
public function __construct(
public private(set) int $amount,
public private(set) string $currency = 'JPY',
) {
if ($amount < 0) {
throw new InvalidArgumentException('金額は0以上');
}
}
// プロパティフック (8.4): 実体を持たない算出プロパティ
public string $formatted {
get => number_format($this->amount) . ' ' . $this->currency;
}
// #[\NoDiscard] (8.5): 返り値を捨てると警告
#[\NoDiscard('戻り値の新しい Money を使ってください')]
public function withAmount(int $amount): static
{
// clone with (8.5): 実際の構文は clone($obj, [プロパティ => 値])
return clone($this, ['amount' => $amount]);
}
}
$price = new Money(1200);
echo $price->formatted, PHP_EOL; // 1,200 JPY
echo $price->withAmount(960)->formatted, PHP_EOL; // 960 JPY
// パイプ演算子 (8.5)
$labels = [' Sale ', 'new', ' LIMITED '];
$clean = array_map(
fn($s) => $s |> trim(...) |> strtolower(...) |> ucfirst(...),
$labels,
);
echo implode(', ', $clean), PHP_EOL; // Sale, New, Limited
// array_first / array_last (8.5)
echo array_first($clean), ' / ', array_last($clean), PHP_EOL; // Sale / Limited
// #[\NoDiscard]: 戻り値を使わず呼び捨てると警告が出る
$price->withAmount(500); // Warning: The return value of method Money::withAmount() ...
実行は次のとおりです。
docker run --rm -v "$PWD":/app -w /app php:8.5-cli php money.php