以下の内容はhttps://kojirooooocks.hatenablog.com/entry/2025/12/28/103807より取得しました。


PHPat を使ってみた

はじめに

こんばんは。

普段PHPStanを使ってるんですが、PHPStanの拡張で「アーキテクチャテスト」ができる PHPat というツールを見つけたので試してみました。

「このレイヤーはこのレイヤーに依存しちゃダメ」とか「Controllerは絶対AbstractControllerを継承してね」みたいなルールをコードでチェックできるやつです。

レビューで毎回指摘するの面倒だなーと思ってたので、なるべく自動でやってほしい。

PHPatとは

PHPStan の拡張として動作するアーキテクチャテストツールです。

https://github.com/carlosas/phpat

自然言語っぽい書き方でルールを定義できるのが特徴で、例えば「DomainレイヤーはInfrastructureに依存してはいけない」みたいなことを直感的に書けます。

インストール

PHPStanは入っている前提で進めます。

composer require --dev phpat/phpat

PHPStanの設定ファイルに拡張を追加します。

# phpstan.neon
includes:
    - vendor/phpat/phpat/extension.neon

テストクラスの作成

テストクラスを作成して、PHPStanの設定に登録します。

# phpstan.neon
includes:
    - vendor/phpat/phpat/extension.neon

parameters:
    paths:
        - app
        - tests/Architecture  # ここを解析対象に含める

services:
    - class: Tests\Architecture\ArchitectureTest
      tags:
          - phpat.test

テストクラスはこんな感じで作ります。

<?php

declare(strict_types=1);

namespace Tests\Architecture;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class ArchitectureTest
{
    public function test_example(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('App\Domain'))
            ->shouldNotDependOn()
            ->classes(Selector::inNamespace('App\Infrastructure'));
    }
}

ルールは test_ で始まるメソッド名か、 #[TestRule] 属性をつけたメソッドとして定義します。

レイヤー間の依存関係チェック

こんな感じのアーキテクチャで、Domainレイヤーが他のレイヤーに依存していないかチェックしてみます。

app/
├── Domain/           # ドメイン層(ビジネスロジック)
│   ├── User/
│   │   ├── User.php
│   │   └── UserRepositoryInterface.php
│   └── Order/
│       └── Order.php
├── Application/      # アプリケーション層(ユースケース)
│   └── User/
│       └── RegisterUserUseCase.php
├── Infrastructure/   # インフラ層(DB、外部API)
│   └── User/
│       └── EloquentUserRepository.php
└── Http/             # プレゼンテーション層
    └── Controllers/
        └── UserController.php

Domainが他のレイヤーに依存していないかをチェックしてみます。

<?php

namespace Tests\Architecture;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class LayerDependencyTest
{
    /**
     * Domainレイヤーは他のレイヤーに依存してはいけない
     */
    public function test_domain_should_not_depend_on_other_layers(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('App\Domain'))
            ->shouldNotDependOn()
            ->classes(
                Selector::inNamespace('App\Application'),
                Selector::inNamespace('App\Infrastructure'),
                Selector::inNamespace('App\Http')
            )
            ->because('Domainレイヤーは最も内側のレイヤーなので、外側に依存してはいけません');
    }

    /**
     * Applicationレイヤーは Infrastructure と Http に依存してはいけない
     */
    public function test_application_should_not_depend_on_infrastructure(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('App\Application'))
            ->shouldNotDependOn()
            ->classes(
                Selector::inNamespace('App\Infrastructure'),
                Selector::inNamespace('App\Http')
            )
            ->because('ApplicationレイヤーはInfrastructureの実装詳細を知るべきではありません');
    }
}

because() でルールの理由を書いておくと、違反したときにメッセージとして表示されるので便利です。

これで vendor/bin/phpstan analyse を実行すると、ルール違反があればエラーとして報告されます。

命名規則・継承ルールのチェック

「Controllerは必ずAbstractControllerを継承する」とか「Requestクラスはfinalにする」みたいなルールもチェックできます。

<?php

declare(strict_types=1);

namespace Tests\Architecture;

use App\Http\Controllers\Controller;
use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class NamingConventionTest
{
    /**
     * Controllerは基底クラスを継承していること
     */
    public function test_controllers_should_extend_base_controller(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('App\Http\Controllers'))
            ->excluding(Selector::classname(Controller::class))
            ->shouldExtend()
            ->classes(Selector::classname(Controller::class));
    }

    /**
     * FormRequestはfinalであること
     */
    public function test_form_requests_should_be_final(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::inNamespace('App\Http\Requests'))
            ->shouldBeFinal();
    }

    /**
     * UseCaseクラスは __invoke メソッドを持つこと(単一責任)
     */
    public function test_use_cases_should_be_invokable(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::classname('/.+UseCase$/', true))
            ->shouldBeInvokable();
    }

    /**
     * Repositoryインターフェースはinterfaceであること
     */
    public function test_repository_interfaces_should_be_interface(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::classname('/.+RepositoryInterface$/', true))
            ->shouldBeInterface();
    }
}

excluding() を使うと特定のクラスを除外できます。基底クラス自身は継承チェックから外したいときに便利。

正規表現も使えるので、/.+UseCase$/ みたいに末尾が UseCase で終わるクラスを対象にする、みたいなこともできます。第2引数を true にすると正規表現として解釈されます。

終わりに

PHPStan使ってるならサクッと導入できるし、レビューで「ここ依存関係おかしいよ」って毎回言わなくてよくなるのは嬉しいですね。

CIに組み込んでおけば、アーキテクチャ違反をマージ前に検知できるので、今のチームでも使えそうです。

copilotとかに任せたらもっと楽になるんですかね?

詳しい人教えて下さい。




以上の内容はhttps://kojirooooocks.hatenablog.com/entry/2025/12/28/103807より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14