phpat 0.10.0 の 正規表現拡張の利用(依存関係/directoryの定義)

初めに

エキサイト株式会社でエンジニアをしている @nukisashineko です。
この記事は エキサイトホールディングスのカレンダー | Advent Calendar 2022 - Qiita の 12/11 に参加する記事となります。
今回は、phpat の使い方についてガイドを書いていこうと思います。

目次

  • phpat の説明/使い方
  • phpat 0.10.0 (正規表現オプションの追加) でできるようになったこと
    • *(ワイルドカード) が2個以上必要な深い階層での定義例
  • phpat 0.10.0 で ディレクトリ構造のチェックをやってみよう

phpat って何?

Googleability が低い プロダクトですが、"phpat/phpat" です。
これは、 phpstan の拡張機能で 依存関係を定義し、
そこから外れるクラスファイルを CI でチェックできるようになるものです。

github.com

具体例

laravel で DDD 的にディレクトリを切ろうと思った時に、

  • app/ (アプリケーション層: コントローラーやルーティング、フォーム等) は


みたいなルールを作るとしましょう。

しかし、このようなルールを作ったとしても
人間に頼るレビューでは見逃しが発生してしまいます。

下記の例を見てください。

見逃してしまいそうな例:

<?php

// app が直接 Infrastructure のクラスと依存関係を作っている例:

namespace ProductName\App\Http\Controllers\User\Index;

use ProductName\App\Http\Controllers\ParentClass\Controller;
// ↓Infrastructure のクラス
use ProductName\Packages\Infrastructure\Repository\User\UserRepository;

class UserIndexGetController extends Controller
{
    private readonly UserRepository $userRepository;
    public function __construct() {
        // ↓ルール外の依存発生
        $this->userRepository = new UserRepository();
    }
}

本当に書きたかった例:

<?php

// app が Domain を経由させることで、 Infrastructure のクラスと依存関係を断っている例:

namespace ProductName\App\Http\Controllers\User\Index;

use ProductName\App\Http\Controllers\ParentClass\Controller;
// ↓ Domain に置いてある Interface ( Laravel によって DI される )
use ProductName\Packages\Domain\User\Repository\UserRepositoryInterface;

class UserIndexGetController extends Controller
{
    public function __construct(
        // ↓😁
       private readonly UserRepositoryInterface $userRepository,
    ) {
    }
}

どうすれば見逃さなかったの?

こういうルールを作ったら自動化しないと、レビュー負荷が上がっていきます。
自動化するためにはコマンドラインで実行して静的解析を行う必要があります。
この依存関係について php 言語で利用できて、
独自のルールを決められて CI 化しやすいライブラリとして phpat があります。

phpat の利用例:

例えば下記のルールの 依存してはいけない の部分を phpat では表現ができます。

  • app/ (アプリケーション層: コントローラーやルーティング、フォーム等) は

<?php
namespace ProjectName\Tests\Small\Architecture\App;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

/**
 * app/ 配下に関する依存関係をチェックするアーキテクチャテスト
 */
class AppArchitectureTest
{
    /**
     * UI層は インフラストラクチャー層 に依存してはいけない
     */
    public function testUILayerMustNotDependOnInfrastructureLayer(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('ProjectName\App'))
            ->excluding(Selector::namespace('ProjectName\App\Provider'))
            ->shouldNotDependOn()
            ->classes(
                Selector::namespace('ProjectName\Packages\Infrastructure'),
                Selector::namespace('ProjectName\Tests'),
            );
    }
}

エラー例:

見逃してしまいそうな例 で実行すると、
phpstan の github action と phpat が設定してあると下記のように検知してくれます。
"Xxxxx should not depend on Yyyyy" と。

 ------ ---------------------------------------------------------------------------------------------------------------------------------------------
  Line   app/Http/Controllers/User/Index/UserIndexGetController.php 
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------
 12       ProductName\App\Http\Controllers\User\Index should not depend on ProductName\Packages\Infrastructure\Repository\User\UserRepository 

便利ですね。 ぜひ導入してください。

github action

phpat を導入するなら github action も設定するのが良いです。
phpstan の拡張なので github action で実行するとエラーの該当行に
コメントの形でエラーを書いてくれます。
ぜひ実感してもらいたいので yaml を置いときますね。

付録: phpstan の github action の設定例
name: PHPSTAN

on:
  push:
    branches:
      - main
  pull_request:
    types: [synchronize, opened, reopened]
    paths: '**.php'

jobs:
  check-by-phpstan-fix-dry-run:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v2
      - name: Install Dependencies
        uses: docker://composer:2.4.1
        env:
          COMPOSER: composer.json
          COMPOSER_VENDOR_DIR: vendor
        with:
          entrypoint: /usr/bin/composer
          args: install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
          path: ${{ github.workspace }}
      - name: setup local files
        uses: docker://composer:2.4.1
        env:
          COMPOSER: composer.json
          COMPOSER_VENDOR_DIR: vendor
        with:
          entrypoint: /usr/bin/composer
          args: setup-local
      - name: phpstan
        uses: docker://php:8.1-cli
        env:
          COMPOSER: composer.json
          COMPOSER_VENDOR_DIR: vendor
        with:
          entrypoint: /usr/local/bin/php
          # memory_limit で落ちないように、実行時オプションで memory_limit を引き上げる
          args: -d "memory_limit=512M" vendor/bin/phpstan analyse -c phpstan.neon --no-progress --ansi
          path: ${{ github.workspace }}

ここまでが序章です

phpat について理解を深めてくれたところで、より深い活用法について話していきます。

phpat v0.10.0 とその違い

phpat は v0.9.1 までは *ワイルドカード一つしか利用できず、
深い階層の表現ができませんでした。
phpat の v0.10.0 から正規表現での表現が可能になりました。
そのため、深い階層での依存関係を関係を記述できるようになりました。

php で 依存関係を きちんと検知したかったら
phpat 0.9.1 では 右側の技術駆動パッケージング を行うしかなかったものが、
phpat 0.10.0 では 左側の形 でも検知できるようになったということです。

  • *ワイルドカード一つしか利用できない場合の表現幅
    • packages/Entity/*packages/Repository/* に依存不可
  • 正規表現が利用できる場合の表現幅
    • packages/Domain/*/Entitypackages/Domain/*/Repository に依存不可

正規表現で定義する DDD の依存関係 例:

諸注意

実際に最近のプロダクトで利用するために用意した rule を
多少 omit し匿名化し 公開させていただきます。

DDD での依存関係のレビュー負荷を減らすために
大変便利だと思いますが万能では有りません。

チームメンバーと話し合ってチームに合うディレクトリ構造に変えてください。
また、README.md も適切に整備しましょう

※ この素敵なディレクトリ構造も自分が定義したものではなく、
※ 偉大なる先人が とあるプロジェクトで ルール定義/整備 したものを参考にしています。

正規表現の読み方について

正規表現の否定と完全一致を利用することで
特定の namespace で 指定のディレクトリ名 以外 を全てマッチさせる ことが可能です。
これを利用すると

  • Domain/* (ただし Domain/Entity は除く) へ依存不可

のような定義を作成することができます。

このため、否定や完全一致等、複雑な正規表現になっています。
必要な箇所について読み解き方を書いておきます。

  • 意味: 'ValueObject' 以外のディレクトリ全てにマッチ
  • 例: ^(?!ValueObject)(?:[^\\]+)$
    • ^$
      • 囲まれているなかで文字列との完全一致のみマッチ
    • (?!)
      • この中に入っているものが現れたら、マッチ判定がマッチしないになる
    • (?:[^\\]+)
      • '\'以外の文字列が連続するとき
    • ^(?!ValueObject)(?:[^\\]+)$
      • 下記を満たす時にマッチ
        • 文字列が 'ValueObject' 以外であり
        • [^\\]+に完全一致

具体例

<?php

namespace ProjectName\Tests\Small\Architecture\Packages;

use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;
use ProjectName\Packages\Domain\Shared\Exceptions\TypeCheckInvalidArgumentException;
use ProjectName\Packages\Domain\Shared\Exceptions\TypeCheckInvalidEnumMatchDefaultException;

/**
 * packages/ 配下に関する依存関係をチェックするアーキテクチャテスト
 *
 * 正規表現では、 '\' を表現するのに '\\\\' である必要があることに注意
 *
 * このような directory に対する packages の依存関係を定義する
 *
 * `tree . --charset=x`
 *
 * ```
 * .
 * |-- app
 * |-- packages
 * |   |-- Application
 * |   |   |-- Exceptions
 * |   |   |-- QueryService
 * |   |   `-- UseCase
 * |   |-- Domain
 * |   |   `-- *
 * |   |       |-- Entity
 * |   |       |-- Exceptions
 * |   |       |-- Repository
 * |   |       |-- Service
 * |   |       `-- ValueObject
 * |   |-- Infrastructure
 * |   `-- Library
 * `-- tests
 * ```
 */
class PackagesArchitectureTest
{
    /**
     * アプリケーション層はインフラストラクチャー層、UI層、フレームワーク、テストに依存してはいけない
     * ※ Laravel のコレクションだけは使いたいため許可
     */
    public function testApplicationLayerMustNotDependOnInfrastructureLayerAndUILayerAndFramework(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('ProjectName\Packages\Application'))
            ->shouldNotDependOn()
            ->classes(
                // Illuminate\Support\Collection 以外
                Selector::classname('/^\\\\?Illuminate\\\\(?!Support\\\\Collection)(?:[^\\\\]+)(?:\\\\[^\\\\]+)*\\\\?$/', true),
                Selector::namespace('Symfony'),
                Selector::namespace('ProjectName\App'),
                Selector::namespace('ProjectName\Packages\Infrastructure'),
                Selector::namespace('ProjectName\Tests'),
            );
    }

    /**
     * ドメイン層はアプリケーション層、インフラストラクチャー層、UI層、フレームワークに依存してはいけない
     */
    public function testDomainLayerMustNotDependOnApplicationLayerAndInfrastructureLayerAndUILayerAndFramework(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('ProjectName\Packages\Domain'))
            ->shouldNotDependOn()
            ->classes(
                // Illuminate\Support\Collection 以外
                Selector::classname('/^\\\\?Illuminate\\\\(?!Support\\\\Collection)(?:[^\\\\]+)(?:\\\\[^\\\\]+)*\\\\?$/', true),
                // Symfony\Component\HttpFoundation\Response 以外
                Selector::classname('/^\\\\?Symfony\\\\(?!Component\\\\HttpFoundation\\\\Response)(?:[^\\\\]+)(?:\\\\[^\\\\]+)*\\\\?$/', true),
                Selector::namespace('ProjectName\App'),
                Selector::namespace('ProjectName\Packages\Application'),
                Selector::namespace('ProjectName\Packages\Infrastructure'),
            );
    }

    /**
     * インフラストラクチャー層はUI層とフレームワーク(Symfony)に依存してはいけない
     * ファサードやコレクションなどの機能は使いたいためLaravelには依存しても良い
     */
    public function testInfrastructureLayerMustNotDependOnUILayerAndFramework(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('ProjectName\Packages\Infrastructure'))
            ->shouldNotDependOn()
            ->classes(
                // Symfony\Component\HttpFoundation\Response 以外
                Selector::classname('/^\\\\?Symfony\\\\(?!Component\\\\HttpFoundation\\\\Response)(?:[^\\\\]+)(?:\\\\[^\\\\]+)*\\\\?$/', true),
                Selector::namespace('ProjectName\App'),
            );
    }

    /**
     * ライブラリは本プロジェクトやフレームワークに依存しないピュアなPHPでないといけない
     */
    public function testLibraryMustNotDependOnAnyLayerAndFramework(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('ProjectName\Packages\Library'))
            ->shouldNotDependOn()
            ->classes(
                Selector::namespace('Illuminate'),
                Selector::namespace('Symfony'),
                Selector::namespace('ProjectName\App'),
                Selector::namespace('ProjectName\Packages\Application'),
                Selector::namespace('ProjectName\Packages\Domain'),
                Selector::namespace('ProjectName\Packages\Infrastructure'),
            );
    }

    /**
     * ValueObject は ValueObject にしか依存してはいけない
     *
     * ※ TypeCheck の呼び出しのため TypeCheckInvalidEnumMatchDefaultException に依存することを許可する
     */
    public function testDomainXxxxValueObjectMustNotDependOnExcludeValueObject(): Rule
    {
        # 正規表現のせいか、 '\' を表現するのに '\\\\' である必要があることに注意
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\ValueObject\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // Domain/Xxxxx/ValueObject 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\(?!ValueObject)(?:[^\\\\]+)\\\\?$/', true),
            )->excluding(
                // phpstan の都合上 type check が必須になっているため、 type check にのみ依存を許可する
                Selector::classname(TypeCheckInvalidEnumMatchDefaultException::class),
                Selector::classname(TypeCheckInvalidArgumentException::class),
            );
    }

    /**
     * Entity は ValueObject にしか依存してはいけない
     */
    public function testDomainXxxxEntityMustNotDependOnExcludeEntityAndValueObject(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\Entity\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // Domain/Xxxxx/{Entity,ValueObject} 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\(?!Entity|ValueObject)(?:[^\\\\]+)\\\\?$/', true),
            )
            ->excluding(
                // phpstan の都合上 type check が必須になっているため、 type check にのみ依存を許可する
                Selector::classname(TypeCheckInvalidArgumentException::class),
            );
    }


    /**
     * Repository は Entity か ValueObject にしか依存してはいけない
     */
    public function testDomainXxxxRepositoryMustNotDependOnExcludeEntityAndValueObject(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\Repository\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // Domain/Xxxxx/{Entity,Exceptions,ValueObject} 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\(?!Entity|Exceptions|ValueObject)(?:[^\\\\]+)\\\\?$/', true),
            );
    }

    /**
     * Service は Entity か ValueObject にしか依存してはいけない
     *
     * ※ Domain/Xxxxx/Service が、Domain/Shared/Service にある Class に依存するのは許す
     * ※ Domain/Xxxxx/Service が、Domain/Xxxxx/Service にある Interface に依存するのは許す
     */
    public function testDomainXxxxServiceMustNotDependOnExcludeEntityAndValueObject(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\Service\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // Domain/Xxxxx/{Entity,Exceptions,Service,ValueObject} 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\(?!Entity|Exceptions|Service|ValueObject)(?:[^\\\\]+)\\\\?$/', true),
                // Domain/Shared/Service/* 以外
                // Domain/Xxxxx/Service/*Interface 以外
                Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\(?!Shared)(?:[^\\\\]+)\\\\Service\\\\(?:[^\\\\]+)(?<!Interface)\\\\?$/', true),
            );
    }

    /**
     * Exceptions は Entity か ValueObject にしか依存してはいけない
     *
     * ※ Domain/Xxxxx/Exceptions が、Domain/Shared/Exceptions にある Class に依存するのは許す
     */
    public function testDomainXxxxExceptionsMustNotDependOnExcludeEntityAndValueObject(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\Exceptions\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // Domain/Xxxxx/{Entity,Exceptions,ValueObject} 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\(?!Entity|Exceptions|ValueObject)(?:[^\\\\]+)\\\\?$/', true),
                // Domain/Shared/Exceptions/* 以外
                Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\(?!Shared)(?:[^\\\\]+)\\\\Exceptions\\\\(?:[^\\\\]+)\\\\?$/', true),
            );
    }

    /**
     * Application/UseCase は Application/QueryService, Domain にしか依存してはいけない
     * ※ Application/UseCase が Application/UseCase にある suffix "UseCaseOutput" Class に依存するのは許す
     */
    public function testApplicationUseCaseMustNotDependOnExcludeDomain(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\UseCase\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // packages/Domain 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\(?!Domain)(?:[^\\\\]+)\\\\?$/', true),
                // packages/Application/{Exceptions,QueryService,UseCase} 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\(?!Exceptions|QueryService|UseCase)(?:[^\\\\]+)\\\\?$/', true),
                // packages/Application/UseCase/**/*UseCaseOutput 以外
                Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\UseCase\\\\(?:[^\\\\]+\\\\)*(?:[^\\\\]+)\\\\(?:[^\\\\]+)(?<!UseCaseOutput)\\\\?$/', true),
            );
    }

    /**
     * Application/QueryService は Domain にしか依存してはいけない
     */
    public function testApplicationQueryServiceMustNotDependOnExcludeDomain(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\QueryService\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // packages/Domain 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\(?!Domain)(?:[^\\\\]+)\\\\?$/', true),
            );
    }

    /**
     * Application/Exceptions は Domain にしか依存してはいけない
     */
    public function testApplicationExceptionsMustNotDependOnExcludeDomain(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\QueryService\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // packages/Domain 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\(?!Domain)(?:[^\\\\]+)\\\\?$/', true),
            );
    }

    /**
     * Infrastructure は Domain にしか依存してはいけない
     */
    public function testInfrastructureMustNotDependOnExcludeDomain(): Rule
    {
        return PHPat::rule()
            ->classes(Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Infrastructure\\\\?/', true))
            ->shouldNotDependOn()
            ->classes(
                // packages/Domain 以外
                Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\(?!Domain)(?:[^\\\\]+)\\\\?$/', true),
            );
    }
}

以上が、 phpat を利用し、 laravel で適切に依存関係を自動検知する例となります。

phpat を拡張せずに、できることとできないこと

期待を持たせすぎても悪いので、できないことについても触れておきます。

  • できること
    • 深い階層を含めた依存関係の定義
      • 例: packages/Domain/*/Entitypackages/Domain/*/Repository 依存不可
  • できないこと
    • どの定義関数が起因でエラーが発生したかの確認
      • should not depends on でてきたけど、どの定義で落ちたかは表示されません
        • 複数で定義されてるとどっちで落ちたの?ってなりがち
    • 「同じディレクトリあるなら許可」等の対象のディレクトリの情報が必要なケースの定義 (※)
      • できない例: packages/Domain/User/Exceptionspackages/Domain/User でのみ依存可能
      • できる例: packages/Domain/*/Exceptionspackages/Domain/* で依存可能

※ Selector を拡張したらできるかも

蛇足 (もうちょっとだけ続くんじゃ)

phpat の禁断の使い方

phpat の禁断の利用方法をお教えしましょう。
正規表現が使えるというのはめちゃくちゃ便利で、割と何でも表現できてします。

これにより、今まで夢想してたディレクトリ構造についての規定が可能となるのです。

ディレクトリ構造の規定って何?

特定のディレクトリではクラス名や namespace に制限を設けたい場合があります。

  • シングルアクションコントローラーを必須にする
    • クラスの suffix を *Index{Post,Get,Put,Delete}Controller.php に強制したい
    • クラスの namespace が特定 **/Index/*Controller.php の形になるように強制したい

tree で表すとこんな形↓

<?php
    /**
     * ```
     * .
     * `-- app
     *     `-- Http
     *         `-- Controllers
     *             `-- **
     *                 `-- Index
     *                     |-- *IndexDeleteController.php
     *                     |-- *IndexGetController.php
     *                     |-- *IndexPostController.php
     *                     `-- *IndexPutController.php
     * ```
     */

こういう地味なルールを規定できるライブラリって実は少なくて、CI化しにくいんです。

phpat 0.10.0 はそれができます。
そう phpat ならね!!!

どう実現するのか?

psr4 を前提にすると下記のように言い換えられます。

  • ディレクトリ構造 を規定する」
  • ≒「特定の namespace を持つクラスへの依存関係を持つことを禁止する」
  • ≒「指定外の namespace を持つクラスへの依存関係を持つことを禁止する」

これは「正規表現」と「phpat の依存関係の検知」で実現できます。

具体的な例

<?php

namespace ProjectName\Tests\Small\Architecture\Directory;

use PHPat\Selector\Selector;
use PHPat\Selector\SelectorInterface;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

/**
 * ./ 配下に関するディレクトリ構造をチェックするアーキテクチャテスト
 * 未定義のディレクトリからの依存関係を許さないことで、間接的にディレクトリ構造をチェックする
 */
class DirectoryArchitectureTest
{
    /**
     * 未定義の層からはどの層への依存もしてはいけない
     */
    public function testUnknownLayerMustNotDependOnAllLayer(): Rule
    {
        $directoryDefinition = $this->getDirectoryDefinition();

        return PHPat::rule()
            ->classes(...$directoryDefinition)
            ->shouldNotDependOn()
            ->classes(Selector::all());
    }

    /**
     * 全ての層は未定義の層に依存してはいけない
     */
    public function testAllLayerMustNotDependOnUnknownLayer(): Rule
    {
        $directoryDefinition = $this->getDirectoryDefinition();

        return PHPat::rule()
            ->classes(Selector::all())
            ->shouldNotDependOn()
            ->classes(...$directoryDefinition);
    }

    /**
     * UnknownLayer(未定義の層 = 想定外の場所に作成されている)のクラスを全て取得する array<Selector> を返却する
     *
     * このクラスで規定され許可されている php クラスのディレクトリ構造は下記
     *
     * `tree . --charset=x`
     *
     * ```
     * .
     * |-- app
     * |   |-- Auth
     * |   |-- Console
     * |   |-- Exceptions
     * |   |-- Http
     * |   |   |-- Controllers
     * |   |   |   |-- ParentClass
     * |   |   |   `-- **
     * |   |   |       `-- Index
     * |   |   |           |-- *IndexDeleteController.php
     * |   |   |           |-- *IndexGetController.php
     * |   |   |           |-- *IndexPostController.php
     * |   |   |           `-- *IndexPutController.php
     * |   |   |-- Kernal.php
     * |   |   |-- Requests
     * |   |   |   `-- **
     * |   |   |       `-- Index
     * |   |   |           |-- *IndexDeleteForm.php
     * |   |   |           |-- *IndexGetForm.php
     * |   |   |           |-- *IndexPostForm.php
     * |   |   |           `-- *IndexPutForm.php
     * |   |   |-- Middleware
     * |   |   `-- ViewComposers
     * |   |-- Providers
     * |   `-- ViewModel
     * |-- packages
     * |   |-- Application
     * |   |   |-- Exceptions
     * |   |   |-- QueryService
     * |   |   `-- UseCase
     * |   |       `-- **
     * |   |           |-- *UseCase.php
     * |   |           |-- *UseCaseInterface.php
     * |   |           `-- *UseCaseOutput.php
     * |   |-- Domain
     * |   |   `-- *
     * |   |       |-- Entity
     * |   |       |-- Exceptions
     * |   |       |-- Repository
     * |   |       |-- Service
     * |   |       `-- ValueObject
     * |   |-- Infrastructure
     * |   |   |-- Aws
     * |   |   |-- ContentRootFileIO
     * |   |   |-- DomainService
     * |   |   |-- InternalApi
     * |   |   `-- Repository
     * |   `-- Library
     * `-- tests
     *     |-- Large
     *     |-- Medium
     *     |   `-- SpecFeature
     *     `-- Small
     *         |-- Architecture
     *         `-- SpecUnitTest
     * ```
     *
     * @return array<SelectorInterface>
     */
    private function getDirectoryDefinition(): array
    {
        # 正規表現のせいか、 '\' を表現するのに '\\\\' である必要があることに注意
        return [
            // {app,packages,tests}以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\(?!App|Packages|Tests)(?:[^\\\\]+)\\\\?$/', true),
            // app/{Auth,Console,Exceptions,Http,Providers,ViewModel} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\App\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\App\\\\(?!Auth|Console|Exceptions|Http|Providers|ViewModel)(?:[^\\\\]+)\\\\?$/', true),
            // app/Http/Controllers/**/Index/*Index{Post,Get,Put,Delete}Controller, app/Http/Controllers/ParentClass/ 以外のディレクトリ構造  (シングルコントローラーを遵守しやすくさせるため route と一致する階層を意識させるため)
            Selector::classname('/^\\\\?ProjectName\\\\App\\\\Http\\\\(?!Kernel)(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\App\\\\Http\\\\Controllers\\\\(?!ParentClass)(?:[^\\\\]+)\\\\?$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\App\\\\Http\\\\(?!Controllers|Middleware|Requests|ViewComposers)(?:[^\\\\]+)\\\\?$/', true),
            // app/Http/Controllers/**/Index/*Index{Post,Get,Put,Delete}Controller 以外のディレクトリ構造  (シングルコントローラーを遵守しやすくさせるため route と一致する階層を意識させるため)
            Selector::classname('/^\\\\?ProjectName\\\\App\\\\Http\\\\Controllers\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\App\\\\Http\\\\Controllers\\\\(?:[^\\\\]+\\\\)*(?:[^\\\\]+)\\\\(?!Index)(?:[^\\\\]+)\\\\?$/', true),
            Selector::classname('/^\\\\?ProjectName\\\\App\\\\Http\\\\Controllers\\\\(?:[^\\\\]+\\\\)*(?:[^\\\\]+)\\\\Index\\\\(?!(?:[^\\\\]+)Index(?:Get|Post|Put|Delete)Controller)[^\\\\]+$/', true),
            // app/Http/Requests/**/Index/*Index{Post,Get,Put,Delete}Form 以外のディレクトリ構造  (シングルコントローラーを遵守しやすくさせるため route と一致する階層を意識させるため)
            Selector::classname('/^\\\\?ProjectName\\\\App\\\\Http\\\\Requests\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\App\\\\Http\\\\Requests\\\\(?:[^\\\\]+\\\\)*(?:[^\\\\]+)\\\\(?!Index)(?:[^\\\\]+)\\\\?$/', true),
            Selector::classname('/^\\\\?ProjectName\\\\App\\\\Http\\\\Requests\\\\(?:[^\\\\]+\\\\)*(?:[^\\\\]+)\\\\Index\\\\(?!(?:[^\\\\]+)Index(?:Get|Post|Put|Delete)Form)[^\\\\]+$/', true),
            // packages/{Application,Domain,Infrastructure,Library} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\(?!Application|Domain|Infrastructure|Library)(?:[^\\\\]+)\\\\?$/', true),
            // packages/Application/{Exceptions,QueryService,UseCase} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\(?!Exceptions|QueryService|UseCase)(?:[^\\\\]+)\\\\?$/', true),
            // packages/Application/UseCase/**/*UseCase{,Output,Interface} 以外のクラス
            Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\Application\\\\UseCase\\\\(?:[^\\\\]+\\\\)*(?:[^\\\\]+)\\\\(?!(?:[^\\\\]+)UseCase(?:|Output|Interface))[^\\\\]+\\\\?$/', true),
            // packages/Domain/Xxxxx/{Entity,Repository,Service,Exceptions,ValueObject} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Domain\\\\[^\\\\]+\\\\(?!Entity|Exceptions|Repository|Service|ValueObject)(?:[^\\\\]+)\\\\?$/', true),
            // packages/Infrastructure/{Aws,ContentRootFileIO,DomainService,InternalApi,Repository} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\Packages\\\\Infrastructure\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\Packages\\\\Infrastructure\\\\(?!Aws|ContentRootFileIO|DomainService|InternalApi|Repository)(?:[^\\\\]+)\\\\?$/', true),
            // tests/{Small,Medium,Large} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\Tests\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\Tests\\\\(?!Small|Medium|Large)(?:[^\\\\]+)\\\\?$/', true),
            // tests/Medium/{SpecFeature} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\Tests\\\\Medium\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\Tests\\\\Medium\\\\(?!SpecFeature)(?:[^\\\\]+)\\\\?$/', true),
            // tests/Small/{Architecture,SpecUnitTest} 以外のディレクトリ構造
            Selector::classname('/^\\\\?ProjectName\\\\Tests\\\\Small\\\\(?:[^\\\\]+)$/', true),
            Selector::namespace('/^\\\\?ProjectName\\\\Tests\\\\Small\\\\(?!Architecture|SpecUnitTest)(?:[^\\\\]+)\\\\?$/', true),
        ];
    }
}

なんでこれが禁断の使い方なの?

phpat が どの test function からのエラーなのかを
test function 名を表示してくれないんです。

本来はこのようなテストで fail したら
from testAllLayerMustNotDependOnUnknownLayer
( = ディレクトリ構造やクラス名を見直してね )
と修正方法やテストがエラーした原因も表示すべきなのに
下記のように 一律 should not depend on と表示されてしまいます。

修正方法がわかりにくいテスト になってしまうので若干テストとしてはアンチパターンで、
導入前後で何度もメンバーへ周知が必要です。
依存関係のチェックのライブラリを想定外の使い方をしているため、しょうがないです

間接的にでもディレクトリ構造やクラス名定義に失敗していることを
CIで教えてもらえるのは便利なんですけどね。

 ------ ---------------------------------------------
  Line   Xxxxx.php                                                      
 ------ ---------------------------------------------
 n     Xxxxx should not depend on  Yyyyy1
 n     Xxxxx should not depend on  Yyyyy1
 n     Xxxxx should not depend on  Yyyyy2
 n     Xxxxx should not depend on  Yyyyy3
 n     Xxxxx should not depend on  Yyyyy4
 n     Xxxxx should not depend on  Yyyyy5

 ------ ---------------------------------------------
  Line   Zzzzz.php                                                      
 ------ ---------------------------------------------
 n     Zzzzz should not depend on  Xxxxx

終わりに

レビュー指摘の負荷を減らすのはスケールするためには大切です。
なるべく自動化していきましょう。

phpat での依存関係の不許可定義は
メンバーと話し合いつつ拡張/緩和されていくものです。
現在も拡張されている最中ですので、
これで完成版というわけでも有りません。

みなさんもぜひ、チームで話し合ったディレクトリ構造や依存関係のチェックを
CI に任せられるように自動化を行ってみてください。