はじめに
こんばんは。
普段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とかに任せたらもっと楽になるんですかね?
詳しい人教えて下さい。