OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

レイヤー間の依存関係の静的解析 - PHP deptrac ~ 導入編

はじめに

今日の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 UserRepository
     */
    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導入まで紹介しました。次回は実際にプロジェクトに導入したうえでの発見点などについて「実践編」としてお送りしたいと思います。