Fortissimo

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