OTOBANK Engineering Blog

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

図でよくわかる Doctrine ORM の基本のキ

こんにちは @kalibora です。

図でよくわかる Doctrine ORM の基本のキ というタイトルで社内LTをしたので、せっかくなので資料を公開しておきます。

EntityManager でよく使うメソッドである persist, flush, clear の挙動を簡単に図解しています。

内容はめっちゃ基本的なものですが、Doctrine 初心者の方には役立つかもしれないのでよろしければどうぞ。

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

前回の記事 レイヤー間の依存関係の静的解析 - PHP deptrac ~ 導入編 からの続きです。

「ユニットテストとは事情が違うし、そうそう違反は起きないよ」と思った方がいらっしゃるかも知れません。いえ、レイヤーの依存関係違反は割と発生します。

起こりうる違反例

前回のコードを少し変更して、Userエンティテイにおいて getType() メソッドで属性を取得できるようにしてみましょう。

<?php
namespace Foo\Entity;

class User
{
    public function getType() : string
    {
        // 何かしらのロジック

        return \Foo\Repository\UserRepository::TYPE_ADMIN;
    }
}
<?php
namespace Foo\Repository;
use Foo\Entity\User;

class UserRepository
{
    public const TYPE_ADMIN = 'admin';

    public function findOneById(int $id) : User
    {}
}

Entityまでのレイヤールールを行うと・・・・

paths:
  - ./src
exclude_files:
layers:
  - 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:
    - Repository
    - Entity
  Repository:
    - Entity
$ deptrac analyse action-repository-entity.yaml
 8/8 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ----------- -------------------------------------------------------------------------------
  Reason      Entity
 ----------- -------------------------------------------------------------------------------
  Violation   Foo\Entity\User must not depend on Foo\Repository\UserRepository (Repository)
              /mnt/d/dev/sandbox-deptrac/src/Entity/User.php:10
 ----------- -------------------------------------------------------------------------------


 -------------------- -----
  Report
 -------------------- -----
  Violations           1
  Skipped violations   0
  Uncovered            6
  Allowed              5
  Warnings             0
  Errors               0
 -------------------- -----

と、Entityは他に参照するレイヤーはあるべきでないのに、UserRepositoryの定数を利用していたことが判明します。 実際のプロジェクトでも数は少なくはありながらも、「サービスクラスの定数を Entityクラス内で用いてしまっていた。」などがありました。

ほかの違反例

さきほどの例では、レイヤー外の定数を挙げましたが、ほかには以下のような違反を発見しました。

  • Entityでのメソッドで、サービスレイヤーとして定義した層の例外クラスの使用
    • これは、定数の例と同様にサービスクラス側に設けた、サービスの例外をエンティテイの例外に利用してしまったというミスです。

現行のエラーをひとまずレポートしないようにする。~ baselineの利用

実際に、既存のプロジェクトに対してレイヤールールをある程度行い、analyseを実行してみると上記で述べたような違反が検出されてくることになります。その場合、deptrac を導入したくても既存の違反分の修正を行わなくては、と思われるかも知れません。ご安心ください。deptracには、phpstanやpsalmのものと同様に既存の違反分を許容するベースラインサポートがあります。

baselineの作成

--formatter=baseline でbaselineを作成します。

$ deptrac analyse --formatter=baseline --baseline-dump=baseline.yaml action-repository-entity.yaml
 8/8 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

Baseline dumped to /mnt/d/dev/sandbox-deptrac/baseline.yaml

以下のような既存の違反に対して skip_violationsが作成されます。

$ cat baseline.yaml
skip_violations:
  Foo\Entity\User:
    - Foo\Repository\UserRepository

baselineのインクルード

baseline: 行を追加します。

paths:
  - ./src
exclude_files:

baseline: baseline.yaml

baselineを含んだ上での再実行

baselineを含ませて改めて実行してみます。

$ deptrac analyse action-repository-entity.yaml
 8/8 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


 -------------------- -----
  Report
 -------------------- -----
  Violations           0
  Skipped violations   1
  Uncovered            6
  Allowed              5
  Warnings             0
  Errors               0
 -------------------- -----

Skipped violations の対象となり、exitコードも0です。

Github Actions でのアノテート

CIでの運用の場合、deptracはさらにフォーマッターとしてgithub-actions が用意されています。 弊社では、deptrac analyse action-repository-entity.yaml --no-progress --formatter=github-actions といったワークフローを設定しています。 なお、補足として、0.11.1以降ではGithub Actionsのフォーマッターでのスキップ対応 を含めスキップ分のレポートはデフォルトでは除外されるようになり、Github Actionsでのアノテートなどでの導入が行いやすくなりました。

継続的なアーキテクチャの維持・向上に向けて

上述のbaselineのサポートもあり、もし既存のコードに対してアーキテクチャを失敗していた!ということが見つかっても、恐れることなく即レイヤー設定の追加・アップデートが行えるようになりました。自身が担当した改修部分に限らず、コードレビューにおいて見つかった点をフィードバックする形で deptrac.yaml に追加し継続的にアーキテクチャテストを行っていきたいと思います。

レイヤー間の依存関係の静的解析 - 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導入まで紹介しました。次回は実際にプロジェクトに導入したうえでの発見点などについて「実践編」としてお送りしたいと思います。