はじめに
今日のPHPでのウェブアプリケーションでは、MVCなどのレイヤー分割を気を付けて構造の開発を行われているかと思います。はたして気を付けてるだけでよいのでしょうか?
・・・ということで、"アーキテクチャテスト" とも称されるレイヤーへの静的解析による検証について記事をお送りします。
PHPプロジェクトにおけるソフトウェブレイヤー間の依存関係の静的解析ツールである deptrac のセットアップまでを行う本エントリー「導入編」と、実際に弊社でのバックエンドプロジェクトにCIに導入している現状を踏まえた上での「実践編」とに分けてお送りしたいと思います。
なぜ、レイヤー間の依存関係のチェックをツールで行えるようにするのか
上述した通り、気を付けるだけでは気づかぬうちに違反が起きていく可能性があります。また、レビューで担保しようとするとそのチェックのためのコストがかかり、チェック項目として用意しなければいけません。また、チェック項目として用意するとしても文章では曖昧で判定がしにくい箇所がでてきます。
それならば、ユニットテストのテイスティングツールと同じように反復してテストするためのツールを用意できればということになります。
パッケージやクラスの依存関係をテストするツールは、アーキテクチャテストともよばれているようで、『ドメイン駆動設計を支えるアーキテクチャテスト』という講演資料では、Javaの ArchUnitを取り上げているほか、PHPのツールについても紹介がありました。
https://speakerdeck.com/kawanamiyuu/object-oriented-conference-2020
現在のPHPにおけるレイヤーとは
ここまで前提なしに"レイヤー"という用語を出していましたが、改めてPHPにおけるレイヤーをざっくり考えてみます。
- PHPはクラスベースの言語ではない。
- 組み込みで用意されている I/O(ストリーム, DBアクセス, サーバーパラメータなど) はグローバルな関数・変数でのアクセスである。
- 00年代後半からのOOP啓蒙~2010年代前半頃でのオニオンアーキテクチャ・DDDムーブメント、psr-0/4の普及によりクラスベースでの開発が一般的に
- クラス定義・利用の手間を考えて配列が多用される場面も多かったが、昨今では配列の代わりでのクラス作成は抵抗がなくなってきた。
- またDIの考え方の普及とともに疎結合なコンポーネント/オブジェクトの作成も受け入れやすくなった。
ということを踏まえてPHPにおける現在のレイヤーとは、「自前かライブラリのクラスに対して定義する」というように捉えられると思います。
PHP での レイヤー間の依存関係チェックツール
2016年ごろ以降は、 ast/PHP-Parserをベースとした静的解析ツールの隆盛により、依存関係のチェックに特化してチェックするツールも出回るようになりました。
後述する deptracのほかには以下のツールがあります。
deptrac について
deptracの開発リポジトリは以下で、READMEに詳細な利用法も記述されています。
deptracを開発/メンテナンスしているのは、Symfony で知られる Sensio Labsのドイツ支社のメンバーです。
2016年に開発が始まり現在まで継続的に開発され、2020年の10月には 0.10.0
がリリースされました。
現時点での特徴としては、
- yamlでのルールセット定義
- 他のプログラム記述型のものと違い、プロジェクト全体でのレイヤーとその従属関係のルールが捉えやすくなります。
collector
と呼ぶ正規表現などでレイヤー群を定義できます。
- スカラー値やresourceと言ったシンボルを定義はできなさそうです。
- ※ 配列は・・・ psalm-type のような概念を導入しないといけなさそうです。
- Graphviz経由によるクラス間の依存関係の可視化
- Allowed数ならびに Uncovered 数の算出
- (0.10 から) baselineサポートによるviolationなクラスをスキップすることが可能に
- ※ このbaselineとは psalmやPHPStan のと同様の考えのものです。
deptracのインストール・セットアップ
READMEでのインストール方法には、まずpharのダウンロードでの利用が案内されていますが、私はバージョンアップの手間などを考えてphive でのグローバルインストールを利用しています。
sudo phive --global install deptrac
また、READMEに案内されているようにレイヤー間の図示出力 (--formatter=graphviz
) にはgraphvizのインストールも必要です。
バージョンは、--version
で確認できます。
$ deptrac --version
deptrac 0.10.2
deptrac の実行例
deptracのREADMEでは、「Ruleset (Allowing Dependencies)」の項に Controller-Services-Respositories
を例にとり説明がありますが、ここからはフレームワーク・ライブラリとの関係を考えてみたいので psr/http-server-handler(psr-15) をベースとしたプロジェクトを例にとってみます。
サンプルのプロジェクト構造
Psr\Http\Server\RequestHandlerInterface
を実装したものを Actionとして、Action-Repository-Entity
をレイヤー構造に位置づけたとします。
$ tree src/
src/
├── Action
│ └── UserShowAction.php
├── Entity
│ └── User.php
└── Repository
└── UserRepository.php
3 directories, 3 files
各クラスの中身は以下の通りです。
<?php
namespace Foo\Action;
use Foo\Repository\UserRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class UserShowAction implements RequestHandlerInterface
{
@var
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{}
}
<?php
namespace Foo\Entity;
class User
{}
<?php
namespace Foo\Repository;
use Foo\Entity\User;
class UserRepository
{
public function findOneById(int $id) : User
{}
}
ActionがRepository に依存するをルールセットに定義する
ActionがRepositoryを呼ぶ。言い換えると、ActionとRespotiroyというレイヤーがあり、ActionがRepositoryのクラスを呼び出すのを許容する というのをルールセットとして定義したい場合は以下の通りとなります。
paths:
- ./src
exclude_files:
layers:
- name: Action
collectors:
- type: implements
implements: Psr\Http\Server\RequestHandlerInterface
- name: Repository
collectors:
- type: className
regex: Foo\\Repository\\.*Repository
ruleset:
Action:
- Repository
Repository: ~
これを action-repository.yaml
として保存します。
実行とエラー検出
解析は analyse
コマンドとともに引数としてルールセット定義yamlを渡して実行します。
$ deptrac analyse action-repository.yaml
3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Report:
Violations: 0
Skipped violations: 0
Uncovered: 8
Allowed: 3
RepositoryがActionから呼び出されるのを許容するのをコメントアウトした場合は、
ruleset:
Action:
# - Repository
Repository: ~
$ deptrac analyse action-repository.yaml
3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Foo\Action\UserShowAction must not depend on Foo\Repository\UserRepository (Action on Repository)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::4
Foo\Action\UserShowAction must not depend on Foo\Repository\UserRepository (Action on Repository)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::11
Foo\Action\UserShowAction must not depend on Foo\Repository\UserRepository (Action on Repository)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::16
Report:
Violations: 3
Skipped violations: 0
Uncovered: 8
Allowed: 0
と Violations が検出されます。この3か所は use句での利用とプロパティでのアノテーションでの利用とコンストラクタでの型宣言での利用のものがそれぞれ検出されています。
「定義されたレイヤーは、ruleset
で従属関係を定義しないと呼び出し違反となる」がdeptracでの基本の考え方です。
カバーされなかった箇所の検出 ~ --report-uncovered
さきほどの出力では、「Uncovered: 8」となっていました。カバーされてなかった箇所の検出には --report-uncovered
オプションで確認できます。
$ deptrac analyse --report-uncovered action-repository.yaml
3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Uncovered dependencies:
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Server\RequestHandlerInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::9
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ResponseInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::5
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ServerRequestInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::6
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Server\RequestHandlerInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::7
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ServerRequestInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::21
Foo\Action\UserShowAction has uncovered dependency on Psr\Http\Message\ResponseInterface (Action)
/mnt/d/dev/sandbox-deptrac/src/Action/UserShowAction.php::21
Foo\Repository\UserRepository has uncovered dependency on Foo\Entity\User (Repository)
/mnt/d/dev/sandbox-deptrac/src/Repository/UserRepository.php::3
Foo\Repository\UserRepository has uncovered dependency on Foo\Entity\User (Repository)
/mnt/d/dev/sandbox-deptrac/src/Repository/UserRepository.php::7
Report:
Violations: 0
Skipped violations: 0
Uncovered: 8
Allowed: 3
もし全てのクラスをカバーしようとするならば
仮に Uncoverdなしにしようとした場合、以下の通りルールセットが必要になります。
paths:
- ./src
exclude_files:
layers:
- name: Psr\Http\Message
collectors:
- type: className
regex: Psr\\Http\\Message\\.*
- name: Psr\Http\Server\RequestHandlerInterface
collectors:
- type: className
regex: Psr\\Http\\Server\\RequestHandlerInterface
- name: Action
collectors:
- type: implements
implements: Psr\Http\Server\RequestHandlerInterface
- name: Repository
collectors:
- type: className
regex: Foo\\Repository\\.*Repository
- name: Entity
collectors:
- type: className
regex: Foo\\Entity\\.*
ruleset:
Action:
- Psr\Http\Message
- Psr\Http\Server\RequestHandlerInterface
- Entity
- Repository
Repository:
- Entity
$ deptrac analyse all.yaml
3/3 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Report:
Violations: 0
Skipped violations: 0
Uncovered: 0
Allowed: 11
以上の通り、すべてのクラスに対するチェックが行える訳ですが、はじめのうちは Uncoveredの数に捕らわれず、チェックしたいレイヤー関係が充足できているか?を重点に導入を始めたほうがよいと思います。
ここまでで、PHPにおけるレイヤーの俯瞰とdeptrac導入まで紹介しました。次回は実際にプロジェクトに導入したうえでの発見点などについて「実践編」としてお送りしたいと思います。