psapは名前空間をコンポーネントとして扱います。
--depth 2の場合、App\Domain\Model\UserはApp\Domainに属します。解析対象外のクラスへの依存はコンポーネント間の指標に含めません。
--depthはディレクトリ探索の上限ではなく、解析したすべての型をどの名前空間階層でコンポーネントに束ねるかを指定します。
たとえば次の型がある場合、depthによって境界が変わります。
App\Domain\Model\Order
App\Domain\Service\Checkout
| 設定 | コンポーネント |
|---|---|
--depth 2 |
どちらもApp\Domain |
--depth 3 |
App\Domain\ModelとApp\Domain\Service |
同じコンポーネント内の依存はCa、Ce、循環依存の対象になりません。そのためdepthを増やすと、それまで内部依存だった関係がコンポーネント間依存となり、指標や循環が新たに見えることがあります。反対に、実装詳細の名前空間まで独立コンポーネントとして扱うと、設計上はひとつのモジュールであるコードを過剰に分割する場合があります。
次の順で粒度を選びます。
- 既定の
--depth autoで全体像を見る。autoは全型に共通する名前空間の次の階層を選ぶ - 1コンポーネントに複数の独立した責務が含まれる、または内部の依存を確認したい場合だけdepthを1段増やす
- 隣り合うdepthのレポートを比較し、実際のアーキテクチャ境界に合う方を採用する
- CI、閾値、循環ベースラインでは採用したdepthを明示して固定する
depthが違えばコンポーネント境界も変わるため、Ca、Ce、I、A、Dや循環数を同じ条件の時系列データとして比較することはできません。
depthを変更しても、対象ファイルの探索、PHPの構文解析、クラス間依存の抽出は同じ対象に対して1回行われます。この部分の処理量はdepthではなく、ファイル数、型数、依存参照数に左右されます。
一方、depthを深くしてコンポーネント数が増えると、次の処理と出力は大きくなる可能性があります。
- コンポーネントごとのCa、Ce、I、A、D計算
- コンポーネント間依存グラフのノードとエッジ
- 循環依存の探索
- HTML、Mermaid、PlantUMLなどの点、行、ノード
特にCaの計算は各コンポーネントに対して他コンポーネントのクラス依存を確認するため、コンポーネント数が多いプロジェクトでは影響が大きくなります。循環検出自体はコンポーネント数とコンポーネント間エッジ数に対して線形ですが、その前段で依存グラフを構築する必要があります。
最大depthを機械的に指定せず、意味のある名前空間境界で止めてください。レポートが大きすぎる場合はdepthを戻すほか、解析対象パスをパッケージ単位に分ける、生成物やテストを--excludeする方法があります。
解析カバレッジは、選択したPHPファイルのうちpsapの解析パイプラインを完了できた割合です。レポートを設計判断に使う前に、解析対象の欠落がないか確認できます。
| 項目 | 定義 |
|---|---|
| Discovered | ソースパス配下で発見した、重複を除く.phpファイル。--exclude適用前 |
| Selected | DiscoveredからExcludedを除き、解析対象として選択したファイル |
| Analyzed | 読み込み、UTF-8検証、PHPパース、名前解決を完了したファイル |
| Excluded | --excludeに一致し、意図的に解析対象から外したファイル |
| Skipped | 選択したが、読み込み、文字コード、パース、名前解決の問題で完了できなかったファイル |
Analysis coverage = Analyzed / Selected
Excludedは意図的に対象から外したファイルなので、カバレッジの分母に含めません。Selectedが0件の場合は、何も解析していない状態を成功と誤認させないためN/Aと表示します。型宣言がないPHPファイルも、解析処理が正常に完了すればAnalyzedに含みます。
カバレッジ100%が示すのは、選択したPHPファイルをすべて処理できたことです。文字列で指定されたクラス、Reflection、DI設定など、解析しない参照まで検出したことを意味しません。
スキップしたファイルや評価できない解析結果は、理由、発生箇所、推奨する対処を持つ診断として出力します。JSONのdiagnosticsには次の値が含まれます。
| 項目 | 内容 |
|---|---|
code |
外部ツールが判定に使える安定した識別子 |
severity |
info、warning、error |
file / line |
問題を特定できたソース位置。該当しない場合はnull |
message |
人が直接読むための英語メッセージ |
context |
パーサーの詳細やFQCNなどの追加情報 |
actions |
推奨する対処の識別子。exclude_fileには--excludeも付与 |
連携ツールは、変更される可能性のあるmessageではなくcodeで分岐してください。従来のJSON warningsは後方互換のため当面の間残し、同じ診断から生成した日本語文字列を出力します。textとMarkdownは英語、CLIの標準エラーは日本語で診断を表示します。
| 指標 | 計算 | 見方 |
|---|---|---|
| Ca | 外部から内部へ依存するクラス数 | 大きいほど多くの外部クラスから使われている |
| Ce | 内部から外部へ依存するクラス数 | 大きいほど多くの外部クラスを使っている |
| I | Ce / (Ca + Ce) |
0に近いほど安定、1に近いほど不安定 |
| A | 抽象型数 / 総型数 |
0は具象のみ、1は抽象のみ |
| D | |A + I - 1| |
0に近いほど安定度と抽象度のバランスがよい |
interfaceとabstract classを抽象型として数えます。class、enum、traitは具象型です。
CaとCeがどちらも0のコンポーネントには、苦痛ゾーンや無駄ゾーンの判定を付けません。
解析結果が1コンポーネントだけの場合、Ca、Ce、I、Dは評価できません。テキストとMarkdownではN/A、JSONではnullになります。Aは単独のコンポーネントでも計算します。
多くのコンポーネントから依存されている場所は、変更の影響が大きいため気軽に変えられません。そこがインターフェース中心(抽象)になっていれば、依存する側は抽象にぶら下がったままでいられるので、実装を差し替えても影響が波及しません。逆に、依存されているのに具象のままだと、変更のたびに影響が広がります。これが苦痛ゾーンです。反対に、誰からも依存されていないのに抽象的な場所は、客の来ない受付窓口のようなものです。これが無駄ゾーンです。
- 不安定度 I
- どれだけ「依存する側」か
- I が低い = みんなから依存されている = おいそれと変更できない(安定)
- I が高い = 誰にも依存されていない = 自由に変更できる(不安定)
- 抽象度 A
- インターフェースや抽象クラスの割合
つまり、依存され具合(安定度)と抽象度は釣り合っているのが望ましい、というのがSAPの考え方です。psapはこのバランスの崩れをDとして数値化します。一言でいうと「依存される者は抽象であれ、依存されない者は具象でよし」です。
循環しているコンポーネント全体をひとつのグループとして検出します。各グループには次の情報が含まれます。
- 循環に含まれる全コンポーネント
- 実際に一周する代表最短経路
- 代表経路に含まれないコンポーネント
- 原因となるクラス間依存
- 依存を作った構文、ファイル、行番号
大きな循環でも、まず短い経路から原因を追えます。
- 継承、インターフェイス、trait
- プロパティ、引数、戻り値の型
- union型、intersection型、nullable型
new、静的呼び出し、静的プロパティ、クラス定数instanceof、catch- PHP Attribute
@var、@param、@return、@throws
docblockでは配列、generic、union、intersection、nullable型を分解し、useと名前空間を使ってクラス名を解決します。壊れたdocblockは無視します。
次の参照は解析しません。
class_exists('X')などの文字列参照new $classNameなどの動的参照- 無名クラスの宣言と内部依存
- 解析対象パス外のクラス
設定ファイル、ディレクトリベースのコンポーネント分類には対応していません。
ターミナルで結果を確認するための形式です。解析カバレッジ、指標、問題領域、循環経路を表示します。-vを付けると所属クラスも表示します。
他のツールから処理するための形式です。fileCoverage、diagnostics、全コンポーネント、クラス間依存、循環、依存根拠を含みます。fileCoverage.analysisCoverageは0から1の数値で、Selectedが0件の場合はnullです。
生成AIやコードレビューへ渡すための形式です。次の順で情報をまとめます。
- 解析条件と概要
- 解析カバレッジ
- 優先して確認する箇所
- 循環依存とコード位置
- 依存の多いコンポーネント
- 全コンポーネントの指標
- 構造化診断と指標の読み方
大きな解析結果では依存根拠の例を制限し、省略件数を表示します。助言そのものは生成せず、生成AIが判断するための事実を出力します。
IとAの散布図を生成します。
MermaidのquadrantChartでは円弧を描けないため、苦痛ゾーンと無駄ゾーンを象限ラベルとして近似表示します。点のI、A、Dとコンポーネントの分類は、HTMLを含む他の形式と同じ解析結果です。
循環依存に含まれるコンポーネントは、点のラベルに[cycle]を付け、通常より大きい赤い太枠の点として強調します。これは循環経路そのものではなく、I/A上で循環に関与するコンポーネントを見つけるための表示です。クラス依存やコード位置を含む循環の詳細は、text、markdown、JSON、PlantUMLで確認できます。
IとAの散布図を、ブラウザで探索できる自己完結HTMLとして生成します。外部通信やCDNは使いません。
- 点へマウスを重ねるかキーボードフォーカスを移すと、コンポーネント名とI、A、D、Ca、Ceを表示
- 点または一覧を選ぶと、コンポーネントに含まれるクラスを固定表示
- 詳細欄の指標カードへマウスを重ねるかフォーカスすると、正式名称、計算方法、値の見方を表示
- コンポーネント名やクラス名の検索、ゾーン、最小Dによる絞り込み
- 同じ座標のコンポーネントをひとつの積層点として表示し、詳細欄から個別に選択
- 循環依存がある場合、循環グループ数、代表となる最短経路、コンポーネント間・クラス間の依存、依存を検出したファイルと行番号を表示
- 循環に含まれるコンポーネントを選ぶと詳細欄に通知し、該当する循環グループへ移動
- 発見、選択、解析、除外、スキップしたPHPファイル数と解析カバレッジを表示
- 既定の英語表示と、HTML内の言語セレクターによる日本語表示の切り替え
HTMLの点はクラスではなく、--depthで束ねた名前空間コンポーネントです。所属クラスは点を選択した後の詳細欄で確認します。
HTMLでは、実際のゾーン判定と同じく、(0, 0)と(1, 1)を中心とする半径0.5の境界を円弧で表示します。そのため、象限で近似表示するMermaidとはゾーンの形が異なりますが、点の指標と座標は共通です。
コンポーネントの依存グラフ、指標、循環依存を図示します。
.pumlファイルの生成と、PlantUML/Graphvizによる画像化は別工程です。依存関係が多い大規模な解析結果では、PlantUMLは多数のノードとエッジの配置を計算するため、I/A上へ点を配置するMermaidよりも画像化のCPU時間やメモリ消費が大きくなる場合があります。そのため、解析と.pumlの生成に成功していても、画像化に長時間を要したり、完了しなかったりすることがあります。
大規模な対象では、次の方法で描画対象を小さくしてください。
--depthを小さくして、コンポーネントを粗く束ねる- ディレクトリ単位で解析対象を分割する
--excludeでテスト、fixture、生成データなどを除外する
全体を俯瞰しながらコンポーネントを絞り込みたい場合は、自己完結HTML形式が適しています。MermaidとPlantUMLは、対象を絞った静的な図として使うと確認しやすくなります。