こんにちは @kalibora です。
図でよくわかる Doctrine ORM の基本のキ
というタイトルで社内LTをしたので、せっかくなので資料を公開しておきます。
EntityManager でよく使うメソッドである persist
, flush
, clear
の挙動を簡単に図解しています。
内容はめっちゃ基本的なものですが、Doctrine 初心者の方には役立つかもしれないのでよろしければどうぞ。
こんにちは @kalibora です。
図でよくわかる Doctrine ORM の基本のキ
というタイトルで社内LTをしたので、せっかくなので資料を公開しておきます。
EntityManager でよく使うメソッドである persist
, flush
, clear
の挙動を簡単に図解しています。
内容はめっちゃ基本的なものですが、Doctrine 初心者の方には役立つかもしれないのでよろしければどうぞ。
前回の記事 レイヤー間の依存関係の静的解析 - 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クラス内で用いてしまっていた。」などがありました。
さきほどの例では、レイヤー外の定数を挙げましたが、ほかには以下のような違反を発見しました。
実際に、既存のプロジェクトに対してレイヤールールをある程度行い、analyseを実行してみると上記で述べたような違反が検出されてくることになります。その場合、deptrac を導入したくても既存の違反分の修正を行わなくては、と思われるかも知れません。ご安心ください。deptracには、phpstanやpsalmのものと同様に既存の違反分を許容するベースラインサポートがあります。
--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:
行を追加します。
paths: - ./src exclude_files: baseline: baseline.yaml
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です。
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でのウェブアプリケーションでは、MVCなどのレイヤー分割を気を付けて構造の開発を行われているかと思います。はたして気を付けてるだけでよいのでしょうか? ・・・ということで、"アーキテクチャテスト" とも称されるレイヤーへの静的解析による検証について記事をお送りします。
PHPプロジェクトにおけるソフトウェブレイヤー間の依存関係の静的解析ツールである deptrac のセットアップまでを行う本エントリー「導入編」と、実際に弊社でのバックエンドプロジェクトにCIに導入している現状を踏まえた上での「実践編」とに分けてお送りしたいと思います。
上述した通り、気を付けるだけでは気づかぬうちに違反が起きていく可能性があります。また、レビューで担保しようとするとそのチェックのためのコストがかかり、チェック項目として用意しなければいけません。また、チェック項目として用意するとしても文章では曖昧で判定がしにくい箇所がでてきます。 それならば、ユニットテストのテイスティングツールと同じように反復してテストするためのツールを用意できればということになります。
パッケージやクラスの依存関係をテストするツールは、アーキテクチャテストともよばれているようで、『ドメイン駆動設計を支えるアーキテクチャテスト』という講演資料では、Javaの ArchUnitを取り上げているほか、PHPのツールについても紹介がありました。
https://speakerdeck.com/kawanamiyuu/object-oriented-conference-2020
ここまで前提なしに"レイヤー"という用語を出していましたが、改めてPHPにおけるレイヤーをざっくり考えてみます。
ということを踏まえてPHPにおける現在のレイヤーとは、「自前かライブラリのクラスに対して定義する」というように捉えられると思います。
2016年ごろ以降は、 ast/PHP-Parserをベースとした静的解析ツールの隆盛により、依存関係のチェックに特化してチェックするツールも出回るようになりました。 後述する deptracのほかには以下のツールがあります。
deptracの開発リポジトリは以下で、READMEに詳細な利用法も記述されています。
deptracを開発/メンテナンスしているのは、Symfony で知られる Sensio Labsのドイツ支社のメンバーです。
2016年に開発が始まり現在まで継続的に開発され、2020年の10月には 0.10.0
がリリースされました。
現時点での特徴としては、
collector
と呼ぶ正規表現などでレイヤー群を定義できます。
READMEでのインストール方法には、まずpharのダウンロードでの利用が案内されていますが、私はバージョンアップの手間などを考えてphive でのグローバルインストールを利用しています。
sudo phive --global install deptrac
また、READMEに案内されているようにレイヤー間の図示出力 (--formatter=graphviz
) にはgraphvizのインストールも必要です。
バージョンは、--version
で確認できます。
$ deptrac --version deptrac 0.10.2
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と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導入まで紹介しました。次回は実際にプロジェクトに導入したうえでの発見点などについて「実践編」としてお送りしたいと思います。