#あすみかんの上にあすみかん

たのしいことしかかかないことをここに決意します

福岡Rubyist会議05 に参加してパネルディスカッションをしてきました #fukuokark05

福岡Rubyist会議05

regional.rubykaigi.org

2026/02/28 に行われた福岡Rubyist会議05に参加しました! また、「コミュニティの垣根を越えよう」というパネルディスカッションに呼んでいただいたのでペラペラ喋ってきました。

続きを読む

PHP 8.5 バージョンアップの勘所 〜おもにPHPUnit〜

はじめに

みなさんのPHPバージョンはいくつですか? どうも、asumikamです。

2026年が始まった気合いを用いて、自プロダクトのPHPのバージョンを8.4→8.5に上げました。エイヤ! ある程度スッと上がったものの、対応方針を悩んだりした部分があるので記事としてまとめあげておこうと思います。

PHP 8.5 バージョンアップの勘所

自プロダクトでのバージョンアップで実際に影響があった部分を紹介します。 これに限らないと思うので 下位互換性のない変更点, PHP 8.5.x で推奨されなくなる機能 をよく読むと良いです。

主に以下の4点について対応していきました。

  1. 配列のオフセットに null を指定するのは非推奨になった
  2. Reflectionの setAccessible は非推奨になった
  3. Opcache拡張モジュールが常にPHPバイナリに組み込まれるようになった
  4. PHPUnit Notices が大量に出るようになった(メイントピック)

1. 配列のオフセットに null を指定するのは非推奨になった

PHP: PHP 8.5.x で推奨されなくなる機能 - Manual

たとえば array_key_first() は配列が空のとき null を返します。それをそのままオフセットに使う行為は非推奨ですので、PHPStanなどで転けるようになります。

解決策は、ズバリ、PHP 8.5 で追加されたarray_first() / array_last() を使うことです。 型エラーが気になる場合は assert と組み合わせて対応しました。

<?php

// Before: これが非推奨になった
$firstMessage = $messages[array_key_first($messages)];

// After: このように書き直した
$firstMessage = array_first($messages);

2. Reflectionの setAccessible は非推奨になった

PHP: PHP 8.5.x で推奨されなくなる機能 - Manual

PHP 8.1 以降、リフレクションで private/protected プロパティにアクセスする際に setAccessible() を呼ぶ必要がなくなっていたのですが、8.5 でついに非推奨になりました。PHPUnitでDeprecationsとして出たことで気づきました。

解決策は、単純に setAccessible() の行を削除するだけでOKでした。

<?php

// Before
$property = new ReflectionProperty($object, 'privateProperty');
$property->setAccessible(true); // ← これが不要に
$value = $property->getValue($object);

// After
$property = new ReflectionProperty($object, 'privateProperty');
$value = $property->getValue($object);

3. Opcache拡張モジュールが常にPHPバイナリに組み込まれるようになった

PHP: その他の変更 - Manual

Opcache 拡張モジュール は、常に PHP バイナリに組み込まれ、ロードされるようになりました。 Dockerfile で opcache を明示的にインストールしてていたので、そのステップを消しました。

4.PHPUnit Notices が大量に出るようになった(メイントピック)

さて、実はここがこのブログのメイントピックとなる場所です。

PHP 8.5 へのアップデートで私たちのPHPUnit のバージョンは「12.3.5」から「12.5.4」になったのですが、バージョンアップ後、テストを実行すると…

OK, but there were issues!
Tests: xxx, Assertions: xxx, PHPUnit Notices: 615.

PHPUnit Notices: 615

😇😇😇😇😇😇😇😇😇😇

多すぎる〜〜〜〜〜!!!!!!

テスト実行結果の見栄えもヤバい

だいぶエゲチ〜感じで気が遠くなりましたが、グッと現世に戻ってきて中を見ると、以下のようなPHPUnit Notices(PHPのNoticeではない)が大量に出ていました。

No expectations were configured for the mock object for XXX. Consider refactoring your test code to use a test stub instead. The #[AllowMockObjectsWithoutExpectations] attribute can be used to opt out of this check.

モックオブジェクトを作っているのに、アサーションが設定されていない、というPHPUnitのNoticeでした。 「N」を消すには以下のどちらかの対応が必要そうです。

  • テストコードをリファクタしてスタブにする
  • Attribute #[AllowMockObjectsWithoutExpectations] をクラスに付与する

ここで、私たちの書いていたテストコードの例を載せます。

<?php

class ServiceTest extends TestCase
{
    private ClassProcess&MockObject $processMock;
    private ClassNyan&MockObject $nyanMock;

    protected function setUp(): void
    {
        $this->processMock = $this->createMock(ClassProcess::class);
        $this->nyanMock = $this->createMock(ClassNyan::class);
    }

    // PHPUnit Notice は出ない
    public function test_にゃんと鳴く(): void
    {
        $this->processMock->expects($this->once())->method('process')->willReturn(true);
        $this->nyanMock->expects($this->once())->method('hello')->willReturn('nyan');

        $this->getSUT()->handle();
    }

    // PHPUnit Notice が出る!!
    public function test_変だった時はエラーとなる(): void
    {
        $this->expectException(\RuntimeException::class);
        $this->processMock->expects($this->once())->method('process')->willReturn(false);

        $this->getSUT()->handle();
    }

    private function getSUT(): Service
    {
        return new Service($this->processMock, $this->nyanMock);
    }
}

「test_変だった時はエラーとなる」のテストケースは $nyanMock$this->never() などで検証すべきですが、テストとしてはPassしていたので書いていませんでした。 もちろん、バージョンアップ後に出たPHPUnit NoticesもテストとしてはPassしているのですが、「.」だった部分が「N」となるのでよくないテストであることは間違いないです。

ただ、私の本来の目的は「PHP8.5へのアップデート」です。 PHPUnit Noticesが出ていてもPHPのバージョンアップはやることはできるので、PHPUnit Noticesの対応は別課題として扱うことができます。 そのため、まずはPHP8.5にアップデートした上で、このケースについてしっかり対応する、というような戦略を立てて対応を進めていきました。

PHPUnit バージョンアップの勘所

以下のような手順で対応を進めました。

  1. rectorphp/rector-phpunitでアップデートできるところはアップデートする
  2. Rectorのカスタムルールを作り #[AllowMockObjectsWithoutExpectations] を付与する
  3. #[AllowMockObjectsWithoutExpectations]を消していく
  4. そして PHPUnit 13 へ...

1. rectorphp/rector-phpunitでアップデートできるところはアップデートする

rectorphp/rector-phpunitは、PHPUnitのテストコードを自動でリファクタリング・アップグレードするRector用拡張パッケージです。これを使えば、以下のようなケースの移行は勝手にシュッとやってくれました。

  • CreateStubOverCreateMockArgRector: 引数に直接渡している createMock() を createStub() に置換
  • CreateStubInCoalesceArgRector: ?? 演算子内の createMock() を createStub() に置換
  • ExpressionCreateMockToCreateStubRector: expects() なしで使われている createMock() を createStub() に置換
  • PropertyCreateMockToCreateStubRector: Stub としてしか使われていないプロパティの Mock を Stub に置換

これを適用後、「PHPUnit Notices: 615」→「PHPUnit Notices: 509」と、約17%ほどはPHPUnit Noticesが改善しました。 ただ、該当しないテストもまだ大量にあるのでもう一工夫必要です。

2. Rectorのカスタムルールを作り #[AllowMockObjectsWithoutExpectations] を付与する

テストコードの大部分が「N」になってしまうと他の開発者たちが戸惑ってしまう&視認性が悪いので、#[AllowMockObjectsWithoutExpectations]を付与する方針に舵を切りました。 具体的には以下のようなRectorのカスタムルールを作ってガッと付与を進めました。

<?php

class AddMockAttributeRector extends AbstractRector
{
    // ここに変更を加えたいクラス名(完全修飾名)を列挙
    private const TARGET_CLASSES = [
        // ...
    ];

    private const ATTRIBUTE_FULL_NAME = 'PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations';

    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition('Add AllowMockObjectsWithoutExpectations attribute to specific classes', []);
    }

    public function getNodeTypes(): array
    {
        return [Class_::class];
    }

    public function refactor(Node $node): ?Node
    {
        $className = $this->getName($node);
        if ($className === null || !in_array($className, self::TARGET_CLASSES, true)) {
            return null;
        }

        // すでに同じAttributeが存在しないか確認
        foreach ($node->attrGroups as $group) {
            foreach ($group->attrs as $attr) {
                if ($this->isName($attr->name, self::ATTRIBUTE_FULL_NAME)) {
                    return null;
                }
            }
        }

        // Attributeを「フルネーム(先頭バックスラッシュ付き)」で追加
        $node->attrGroups[] = new AttributeGroup([
            new Attribute(new FullyQualified(self::ATTRIBUTE_FULL_NAME))
        ]);

        return $node;
    }
}

この対応をした時点で、PHPのバージョンは8.5になり、テストもクリーンに「.」が出るようになりました。 そのため、「PHP 8.5 のバージョンアップ」というゴールは達成です。

3. #[AllowMockObjectsWithoutExpectations]を消していく

#[AllowMockObjectsWithoutExpectations] を消すモチベーションは、ほとんどエンジニアとしての意地だと思います。

そもそも、無くした方が良いのか?(放置でも良いのではないか?)というところにも触れておきます。 この話題に関連するissueなどを読み*1、私と同じような状況に遭っているユーザーがわりといること、今回のケースのために作られたAttributeであること、多くの人の "救い" の手順としてこれを用意していることを理解し、かつ、直近で追加されたAttributeであるためすぐには消されることはなさそうだな〜とも思いました。

ここからどうしていくか、というのは個人の考えですが、このAttributeに依存したままにするのは「綺麗なテストダブルではない」のでよくないものという認識があります。 なんとなく恒久的に存在するようなAttributeでもなさそうと思い、気合を入れて除去していったのでした。

実際に外していく作業はもう、本当にやるだけのやつです。チマチマ泥臭くやっていきました。 既存コードを改修したときにこのAttributeを見つけたら外していったり、規則性のあるところはAIに任せたり、毎日ちょっとずつ直していって、最終的に2月某日、すべての #[AllowMockObjectsWithoutExpectations] を無くしました🎉

4. そして PHPUnit 13 へ...

phpunit.de

そうこうしているうちに PHPUnit 13 がリリースされました。めでたいですね。

  • any()がhard-deprecatedなのでonce()などに置き換え
  • モックでexpects()をつけ忘れているところにつける

上記2点についてテストが落ちたりしたので、いくつかテストを修正したらスッと上がりました。こちらもめでたいです。 新しいアサーションも増えたので隙あらば使っていきたい所存です。

余談: OSS Contributeをいくつかすることができた

自プロダクトのアップデートに伴い、いくつかOSS Contributeを果たすことができたので挙げておきます。

PHPUnit の --display-phpunit-notices, --fail-on-phpunit-notice のマニュアル拡充

github.com

github.com

./vendor/bin/phpunit --display-phpunit-notices

PHPUnit Notices はデフォルトではどんな内容かが出ないのですが、上記オプションをつけることでどんな内容なのかを詳細にみることができます。 しかし、このオプションたちの記述がマニュアルになかったので追加しました。 余談ですが、XML Configuration の displayDetailsOnPhpunitNotices もあるので、trueに設定しておくと便利です。

Rector の特定ルールで IntersectionTypeをサポートする

PHPUnitSetList::PHPUNIT_120 にセットされているルールの PropertyCreateMockToCreateStubRector は IntersectionTypeをサポートしていませんでした。 置換で使った際に結局手動で戻して面倒だったので、コントリビュートチャンスでは!?と思いドキドキしながらPRを送ってみました(2026/02/26時点でPRはオープンな状態)。

// Before: \stdClass が消えてしまう
-private \PHPUnit\Framework\MockObject\MockObject&\stdClass $someMock;
+private \PHPUnit\Framework\MockObject\Stub $someMock;

// After: \stdClass も残ってハッピー
-private \PHPUnit\Framework\MockObject\MockObject&\stdClass $someMock;
+private \PHPUnit\Framework\MockObject\Stub&\stdClass $someMock;

おわりに

speakerdeck.com

実は、PHPバージョンアップのやる気を出してくれたのは PHPカンファレンス小田原2025 でのスポンサーセッションを年末に見返したからなのでした。 気合を入れた時点では、まさかコントリビュートまでいくとは思ってませんでしたが、やってみると意外とそうなるもんですね!めでたい!!

github.com

同じチームだった時に77webさんが PHP 8.4 バージョンアップに際してOSSにPR投げていたのとかをみてかっこいいなぁ〜〜、と思ったりしていたので、それと同じことができたと思うと誇らしいです。

最近は長らく無視していたRenovateのPRもガツガツマージしていってるので、自プロダクトを令和最新キラピカRepotisoryに仕立てていきたいです。

かしこ。

*1:久しぶりにみたら、直近で Claude subagent を作っている人もいるようだ