Laravel製の趣味プロダクトをDDD + ヘキサゴナルアーキテクチャで書き直してみた

この記事は[WHITEPLUS Advent Calendar 2017](https://qiita.com/advent-calendar/2017/whiteplus)の18日目です。

----

最近、趣味で開発しているLaravel製のプロダクトをDDD + ヘキサゴナルアーキテクチャで書き直してみたので、この記事ではその構成について超ざっくりと紹介します。
Laravelアプリケーションを開発する上での参考になれば嬉しいです。

# プロダクトの紹介
![](https://avatars1.githubusercontent.com/u/864041?s=120&amp;v=4) [ngmy/webloyer: Webloyer is a Web UI for managing Deployer deployments](https://github.com/ngmy/webloyer) [![](http://b.hatena.ne.jp/entry/image/https://github.com/ngmy/webloyer)](http://b.hatena.ne.jp/entry/s/github.com/ngmy/webloyer)

- PHP製のデプロイツール[Deployer](https://deployer.org/)のWeb UIです
- Deployerの詳しい紹介については、他の方が書いた記事があるのでここでは省略します
    - [Deployerを使ってデプロイしてみたら想像以上に楽だった](https://qiita.com/ritukiii/items/ce1876b85c5bed68c93b)
    - [PHP製のdeployツール"deployer"でLaravelアプリケーションをgit clone/composer installでデプロイ](https://qiita.com/takky/items/53ef5c6ac9d3755dce88)
- こんな感じのWeb UIです
    - https://github.com/ngmy/webloyer/blob/master/SCREENSHOTS.md
- ざっくりと下記の機能を持っています
    - プロジェクト管理
        - プロジェクト単位のデプロイ設定
    - プロジェクト単位のデプロイ管理
        - 1クリックでのデプロイ・ロールバック
        - デプロイ履歴の保存
        - デプロイ完了のメール通知
    - レシピファイル管理
    - サーバファイル管理
    - ユーザ管理
        - メールアドレス・パスワードによる認証
        - ロールベースのアクセス制御
    - Web APIによるデプロイ
    - Webhookによるデプロイ
        - 記事執筆時点でサポートしているのはGitHub Webhookのみ
- 利用しているWebアプリケーションフレームワークはLaravel 5.2です
    - 開発開始時点ではLaravel 4.2でした
- 書き直す前の最新バージョンは0.43.0です
    - https://github.com/ngmy/webloyer/tree/0.43.0
- 書き直す前（バージョン0.43.0）のアーキテクチャは下記のような感じでした
    - MVC + サービス層 + リポジトリパターン
    - ビジネスロジックを`app/`配下に配置（`app/Services/`、`app/Repositories/`など）
    - リポジトリではEloquentのインスタンスをそのまま返却
    - [Implementing Laravel](https://leanpub.com/implementinglaravel-jpn)を参考にしていました

# 書き直してどうなったか？
https://github.com/ngmy/webloyer/tree/feature/ddd-hexagonal-architecture

## ビジネスロジックのディレクトリ構成
- ビジネスロジックは`app/`配下ではなく、`packages/`というディレクトリを別に作成してその配下に配置しています

```
packages/
├── common
│   ├── src
│   │   ├── Config
│   │   ├── Domain
│   │   │   ├── Model
│   │   │   └── Service
│   │   ├── Enum
│   │   ├── Filesystem
│   │   ├── Notification
│   │   ├── Port
│   │   │   └── Adapter
│   │   │       ├── Notification
│   │   │       ├── Persistence
│   │   │       └── Validation
│   │   ├── QueryObject
│   │   └── Validation
│   └── tests
├── identity_access
│   ├── src
│   │   ├── Application
│   │   │   ├── Role
│   │   │   └── User
│   │   ├── Domain
│   │   │   ├── Model
│   │   │   │   ├── Role
│   │   │   │   └── User
│   │   │   └── Service
│   │   └── Port
│   │       └── Adapter
│   │           └── Persistence
│   │               └── Eloquent
│   └── tests
└── webloyer
    ├── src
    │   ├── Application
    │   │   ├── Deployer
    │   │   ├── Deployment
    │   │   ├── Project
    │   │   ├── Recipe
    │   │   ├── Server
    │   │   └── Setting
    │   ├── Domain
    │   │   ├── Model
    │   │   │   ├── Deployer
    │   │   │   ├── Deployment
    │   │   │   ├── Project
    │   │   │   ├── Recipe
    │   │   │   ├── Role
    │   │   │   ├── Server
    │   │   │   ├── Setting
    │   │   │   └── User
    │   │   └── Service
    │   │       └── Deployer
    │   └── Port
    │       └── Adapter
    │           ├── Form
    │           │   ├── DeploymentForm
    │           │   ├── ProjectForm
    │           │   ├── RecipeForm
    │           │   ├── ServerForm
    │           │   ├── SettingForm
    │           │   └── UserForm
    │           ├── JsonRpc
    │           │   └── Middleware
    │           ├── Messaging
    │           └── Persistence
    │               └── Eloquent
    └── tests
        └── Port
            └── Adapter
                └── Form
                    └── ProjectForm
```

- `packages/`の直下に、ドメインごとのディレクトリを作成しています（`webloyer/`、`identity_access/`、`common/`）
- 各ドメインのディレクトリの直下には`src/`と`tests/`があり、それぞれ実コードとテストコードを配置しています（今のところテストコードはほとんど書けてませんが:sweat_smile:）
- `src/`と`tests/`は同じ名前空間にしていて、`composer.json`にてPSR-4を使ってオートロードで読み込めるようにしています

```{composer.json}
{
    ...
    "autoload": {
        ...
        "psr-4": {
            "App\\": "app/",
            "Ngmy\\Webloyer\\Webloyer\\": "packages/webloyer/src/",
            "Ngmy\\Webloyer\\IdentityAccess\\": "packages/identity_access/src/",
            "Ngmy\\Webloyer\\Common\\": "packages/common/src/"
        }
    },
    "autoload-dev": {
        ...
        "psr-4": {
            "Ngmy\\Webloyer\\Webloyer\\": "packages/webloyer/tests/",
            "Ngmy\\Webloyer\\IdentityAccess\\": "packages/identity_access/tests/",
            "Ngmy\\Webloyer\\Common\\": "packages/common/tests/"
        }
    },
    ...
}
```

- 各ドメインに対して1個ずつサービスプロバイダを作成していて、`src/`直下に配置しています（`WebloyerServiceProvider.php`、`IdentityAccessServiceProvider.php`、`CommonServiceProvider.php`）

## Webloyerドメイン
- Webloyerのコアドメインです。プロジェクト管理、デプロイ管理、レシピファイル管理、サーバファイル管理、ユーザ管理、設定などの機能を持ちます

### `Application`
- サブディレクトリ
    - 集約単位で作成しています
- アプリケーションサービスのメソッドの入力パラメータ
    - ドメインモデルのオブジェクトを使わず基本型のみを使うようにしています
    - 『実践ドメイン駆動設計』の14章に書かれているように、コマンドオブジェクトを使うともっとすっきり書けるのかもしれません   

### `Domain`
#### `Model`
- サブディレクトリ
    - 集約単位で作成しています
- レイヤスーパータイプ
    - `Domain/Model/`直下に配置しています
    - エンティティや値オブジェクトなどの抽象クラス群、楽観的並行制御を行うためのトレイトなどがあります
- エンティティ
    - Eloquentは使わずに、POPOで実装しています

#### `Service`
- サブディレクトリ
    - 集約単位で作成しています

### `Port/Adapter`
#### `Persistence`
- 永続指向のリポジトリの実装と、Eloquentモデルを配置してします
- インピーダンスミスマッチ
    - ライブラリなどは利用せず、リポジトリの中で地道にマッピングしています  
- リポジトリのメソッドの入力パラメータ
    - 値オブジェクト、基本型、仕様オブジェクト、クエリオブジェクトなど様々です
    - 取得件数（LIMIT）・開始位置（OFFSET）、ソート順、ページネーションなどの指定はクエリオブジェクトを使って行うようにしています（下記）

```{packages/webloyer/src/Application/Deployment/DeploymentService.php}
public function getDeploymentsOfProjectAndPage($projectId, $page = 1, $perPage = 10)
{
    $criteria = new DeploymentCriteria($projectId);
    $order = new Order('deployments.created_at', Direction::desc());
    $pagination = new Pagination($page, $perPage);
    $queryObject = new QueryObject();
    $queryObject->setCriteria($criteria)
        ->addOrder($order)
        ->setPagination($pagination);
    return $this->deploymentRepository->deployments($queryObject);
}          
```

#### `Form`
- Webのフォーム処理のためのクラス群を配置しています
- Formクラスの責務は、バリデーションとアプリケーションサービスの呼び出しだけです
- バリデータの実装
    - Commonドメインに`Illuminate\Validation\Factory`をラップした`AbstractLaravelValidator`抽象クラスを作っており、これを継承したバリデーションクラスを各フォームに対して1個ずつ作成しています

#### `Messaging`
- Deployerをディスパッチするためのドメインサービスのキュー実装を配置しています

#### `JsonRpc`
- [JSON-RPC 2.0](http://www.jsonrpc.org/specification)のWeb APIです
- JSON-RPCサーバの実装はサードパーティ製のライブラリ[^1]に任せているので、ここではドメインオブジェクトをJSONに変換して返す処理だけを書いています
- レスポンスクラスを作ってドメインオブジェクトからJSONへ変換する責務を持たせています
- 手抜きして、バリデーションやアプリケーションサービスの呼び出しをFormクラスに任せてしまっていますが、本当は個別に持った方がいいと思います

## IdentityAccessドメイン
- Webloyerの支援サブドメインです。ユーザやロールといった、認証やアクセス制御に関する機能を持ちます

### `Application`
- Webloyerドメインと同じ方針です

### `Domain/Model`
- Webloyerドメインと同じ方針です

### `Port/Adapter`
- Webloyerドメインと同じ方針です

#### `Persistence/EloquentUserProvider.php`
- Laravelの`Auth::user()`で認証済みユーザを取得した際に、Eloquentではなくドメインオブジェクトで返ってきてほしかったので、`Illuminate\Auth\EloquentUserProvider`を継承したクラスを定義して、各種メソッドをオーバーライドしています

## Commonドメイン
- Webloyerの汎用サブドメインです
- `src/`直下に、サードパーティ製のライブラリに置き換え可能な機能や別リポジトリに切り出し可能な機能を、機能ごとに配置しています
    - クエリオブジェクト、列挙型[^2]、バリデーションなどがあります
- `Port/Adapter/`配下には、それらの機能のインタフェースの実装を配置しています

## Laravel
- 基本`app/`配下のコードにはビジネスロジックを書かないようにしています（一部移行しきれずにビジネスロジックが残ってますが:sweat_smile:）
- コントローラやArtisanコマンドやジョブを、ドメインのクライアントとして利用しています
    - ドメインモデルを直接利用しないようにして、アプリケーションサービスやFormクラスのメソッドを経由するようにしています

### `Console`
- コマンド系
    - Webloyerのインストールコマンドや、古いデプロイのお掃除バッチがあります

### `Http`
- `routes.php`
    - コントローラやJsonRpcクラスへのルート登録を行っています
- `breadcrumbs.php`
    - パンくずリストです

#### `Controllers`
- アプリケーションサービスやFormクラスをコンストラクタインジェクションしています
- 下記は典型的なコントローラの実装です

```{app/Http/Controllers/ServersController.php}
class ServersController extends Controller
{
    private $serverForm;

    private $serverService;

    /**
     * Create a new controller instance.
     *
     * @param \Ngmy\Webloyer\Webloyer\Port\Adapter\Form\ServerForm\ServerForm $serverForm
     * @param \Ngmy\Webloyer\Webloyer\Application\Server\ServerService        $serverService
     * @return void
     */
    public function __construct(ServerForm $serverForm, ServerService $serverService)
    {
        $this->middleware('auth');
        $this->middleware('acl');

        $this->serverForm = $serverForm;
        $this->serverService = $serverService;
    }

    /**
     * Display a listing of the resource.
     *
     * @param \Illuminate\Http\Request $request
     * @return Response
     */
    public function index(Request $request)
    {
        $page = $request->input('page', 1);

        $perPage = 10;

        $servers = $this->serverService->getServersOfPage($page, $perPage);

        return view('servers.index')->with('servers', $servers);
    }

    ...

    /**
     * Store a newly created resource in storage.
     *
     * @param \Illuminate\Http\Request $request
     * @return Response
     */
    public function store(Request $request)
    {
        $input = $request->all();

        if ($this->serverForm->save($input)) {
            return redirect()->route('servers.index');
        } else {
            return redirect()->route('servers.create')
                ->withInput()
                ->withErrors($this->serverForm->errors());
        }
    }

    ...
}
```                                      

#### `Middleware`
- Laravelの機能を利用した認証やCSRFトークンの検証、GitHub Webhookのシークレットキーの検証などを行っています

### `Jobs`
- キューに投入するためのジョブ
- Webloyerではデプロイやロールバックのジョブをキューで処理しています
- Webフォームからデプロイやロールバックのリクエストを受けると、Formクラス（`packages/webloyer/src/Port/Adapter/Form/DeploymentForm/DeploymentForm.php`）がアプリケーションサービスのメソッドを呼び出して、ジョブの生成とキューへの投入を行っています
- ジョブクラスも単なるアプリケーションサービスのクライアントになっているので、アプリケーションサービスのメソッドを呼び出すことでデプロイやロールバックを行います

### `Providers`
- ドメインごとにサービスプロバイダを作るようにしたのでほとんど使っていませんが、ルートとドメインオブジェクトを結合するために唯一`RouteServiceProvider.php`だけ使っています
- 使っていますが、アプリケーションサービスのメソッドの入力パラメータを基本型だけにしたので、ここでドメインオブジェクトを結合してもあまり旨味がないような気がしています。これは後で消すかもしれません

# まとめ
超ざっくりとですが、Laravel + DDD + ヘキサゴナルアーキテクチャで構築したアプリケーションの構成について説明してきました。

正直なところヘキサゴナルアーキテクチャに移行したというだけで、肝心なドメインモデルがまだまだ未熟で改善の余地があるのが実情ですが、それについては今後の開発の中で育てていければいいなと思っています。

# 参考文献
1. [エリック・エヴァンスのドメイン駆動設計 ソフトウェアの核心にある複雑さに立ち向かう](http://www.shoeisha.co.jp/book/detail/9784798121963)
2. [実践ドメイン駆動設計](http://www.shoeisha.co.jp/book/detail/9784798131610)
3. [VaughnVernon/IDDD_Samples](https://github.com/VaughnVernon/IDDD_Samples)

[^1]: JSON-RPCサーバは[fguillot/JsonRPC](https://github.com/fguillot/JsonRPC)というライブラリを使用しています。

[^2]: 列挙型は @Hiraku さんの[Enumトレイト](https://qiita.com/Hiraku/items/71e385b56dcaa37629fe#comment-e415b6f030f7f1faa931)をカスタマイズしたものを使用しています。値オブジェクトをすっきり書けて便利です。