LaravelにDDDを導入する

# はじめに
本記事では**PHP**のフレームワーク**Laravel**に**DDD**を導入する方法を解説します。
前提として...**Laravel**は**MVC**が採用されていたり**Service Provider**などの便利なDIツールが採用されていることから、**DDD**を導入せずとも規則的なコードを書くことが可能になっています。そのため本記事で紹介する内容はあくまで一案を紹介することを目的としています。

# GitHub
- 本記事で紹介するリポジトリ

https://github.com/architecture-template/laravel-ddd

# プロジェクト構成
- ベースは**レイヤードアーキテクチャ**
- **Eloquent Model**(ORM)や**Service Provider**(DI)などを利用して**Laravel**の既存機能を活かした構成にする
- Nginxではなく**Laravel Octane**(Swoole)を使用する

https://readouble.com/laravel/8.x/ja/octane.html
## ディレクトリ構成
- app配下のみ記載
- routerは**Laravel**の既存機能(routes/api.php)を使用する
```
app/
 ├──Console
 ├──Domain(ドメイン層)
 │    ├──Models
 │    └──Repositories
 ├──Exceptions
 ├──Http(プレゼンテーション層)
 │    ├──Controllers
 │    ├──Middleware
 │    ├──Outputs
 │    └──Kernel.php
 ├──Infra(インフラ層)
 │    ├──Apis(外部APIに渡す)
 │    └──Daos(DBに入れる)
 ├──Models
 ├──Providers
 └──Services(アプリケーション層)
```
## 依存関係
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575230/584fd125-0ac2-ce46-bb7b-67cabeee4f42.png)

# 環境構築
※Docker環境が必要です。
- コンテナを起動
```
make docker_up
```
- Octuneサーバーを起動
```
make docker_run
```
# コード解説
例では下記のAPIを実装して解説します。
### POST: http://localhost:8000/api/create_example
- リクエスト
```json
{
    "name": "example1"
}
```
- レスポンス
```json
{
    "id": 1,
    "name": "example1",
    "message": "Example completed"
}
```

### GET: http://localhost:8000/api/get_example/1
- レスポンス
```json
{
    "id": 1,
    "name": "example1",
    "message": "Example completed"
}
```

## Model
レイヤードアーキテクチャで実装を行う場合、依存関係の低いレイヤーから実装していきます。今回の場合では**Model**から実装します。

**Model**ではロジックの中心となる構造体（※PHPには構造体の機能はありませんが便宜上構造体と呼んでいます）や**Model**に強く依存している変換処理を記述していきます。今回の場合は単純な構造体とそれをJson化して呼び出す`jsonSerialize()`メソッドを追加しています。

例えばよくある実装例としてはemailのバリデーションや特定の配列やJsonをエンコードして**Model**に格納する処理などはこちらにメソッドを追加します。

```php
<?php

namespace App\Domain\Models;

class Example implements \JsonSerializable
{
    private int $id;
    private string $name;

    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    public function jsonSerialize()
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
}
```

## Dao
**Dao**ではメソッドに渡されたモデルや値を**Eloquent Model**に置き換えてデータベースとのやり取りを行います。また、この段階で**Eloquent Model**を作成します。

```php
<?php

namespace App\Infra\Daos;

use App\Domain\Models\Example;
use App\Models\Example as EloquentExample;
use App\Domain\Repositories\ExampleRepository;

class ExampleDao implements ExampleRepository
{
    public function findById(int $id): ?Example
    {
        $eloquentExample = EloquentExample::find($id);
        if ($eloquentExample === null) {
            return null;
        }

        return new Example(
            $eloquentExample->id,
            $eloquentExample->name
        );
    }

    public function save(Example $example): Example
    {
        $eloquentExample = EloquentExample::create([
            'name' => $example->jsonSerialize()['name'],
        ]);

        return new Example(
            $eloquentExample->id,
            $eloquentExample->name
        );
    }
}
```
```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Example extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     * @var array
     */
    protected $fillable = [
        'name',
    ];
}
```

## Repository
**Repository**では**Dao**に記述したメソッドのインターフェースを実装します。
```php
<?php

namespace App\Domain\Repositories;

use App\Domain\Models\Example;

interface ExampleRepository
{
    public function findById(int $id): ?Example;
    public function save(Example $example): ?Example;
}
```

## Service Provider 
次に**Repository**を**Dao**のインターフェースとして組み込むために**DI**で依存性関係を注入していきます。**Laravel**には**AppServiceProvider**という便利なDIツールが標準採用されているためこちらの`register()`メソッドに使用するクラスを記述していきます。

```php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\Repositories\ExampleRepository;
use App\Infra\Daos\ExampleDao;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(
            ExampleRepository::class,
            ExampleDao::class,
        );
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        //
    }
}
```

## Service
**Service**ではメソッドに渡された値を**Model**に当てはめて**Repository**に渡す処理を記述します。また、**Infra層**では複雑になる依存関係の強い処理をまとめて記述して、他のレイヤーでの依存度を低くすることを意識します。

※**Service**でどれだけ依存度の高い処理を記述するのかは個人やチームでの判断になるとは思うのであくまで参考程度に考えてください。

```php
<?php

namespace App\Services;

use App\Domain\Models\Example;
use App\Domain\Repositories\ExampleRepository;

class ExampleService
{
    private $exampleRepository;

    public function __construct(ExampleRepository $exampleRepository)
    {
        $this->exampleRepository = $exampleRepository;
    }

    /**
     * Get Example By ID
     * @param int id
     * @return Example|null
     */
    public function getExampleById(int $id): ?Example
    {
        return $this->exampleRepository->findById($id);
    }

    /**
     * Create Example
     * @param string name
     * @return Example|null 
     */
    public function createExample(string $name): ?Example
    {
        $example = new Example(0, $name);

        return $this->exampleRepository->save($example);
    }
}
```

## Controller
**Controller**では通常の**Laravel**と同様に**Router**から送られたリクエストを**Model**や適切な値に変換してレスポンスを返す処理を記述します。今回はレスポンスの値を定義する**Output**クラスで構造体を定義して使用しています。
```php
<?php

namespace App\Http\Controllers;

use App\Domain\Models\Example;
use App\Services\ExampleService;
use App\Http\Outputs\ExampleOutput;
use Illuminate\Http\Request;

class ExampleController extends Controller
{
    /**
     * @var ExampleService
     */
    private $exampleService;

    /**
     * @var ExampleOutput
     */
    private $exampleOutput;

    /**
     * ExampleController constructor
     * @param ExampleService $exampleService
     */
    public function __construct(ExampleService $exampleService,  ExampleOutput $exampleOutput)
    {
        $this->exampleService = $exampleService;
        $this->exampleOutput = $exampleOutput;
    }

    /**
     * Get Example
     * @param int id
     * @return \Illuminate\Http\JsonResponse
     */
    public function getExample(int $id)
    {
        $example = $this->exampleService->getExampleById($id);
        if ($example === null) {
            return response()->json(['error' => 'Example not found'], 404);
        }

        $output = $this->exampleOutput->toExample($example, "Example completed");

        return response()->json($output, 200);
    }

    /**
     * Create Example
     * @param Request request
     * @return \Illuminate\Http\JsonResponse
     */
    public function createExample(Request $request)
    {
        $data = $request->all();
        $example = $this->exampleService->createExample($data['name']);

        $output = $this->exampleOutput->toExample($example, "Example completed");

        return response()->json($output, 200);
    }
}
```

## Output
**Output**ではレスポンスで使用する構造体を定義します。
```php
<?php

namespace App\Http\Outputs;

use App\Domain\Models\Example;

class ExampleOutput
{
    /**
     * ExampleOutput
     * @param Example Example
     * @param string message
     * @return array array
     */
    public function toExample(Example $example, string $message)
    {
        return [
            'id' => $example->jsonSerialize()['id'],
            'name' =>  $example->jsonSerialize()['name'],
            'message' => $message,
        ];
}
```

## Router
最後にAPIのPathから**Controller**にハンドリングを行う処理を記述していきます。この辺りは通常のLaravelの記述方法と全く同じです。  
```php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ExampleController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::get('/get_example/{id}', [ExampleController::class, 'getExample']);
Route::post('/create_example', [ExampleController::class, 'createExample']);
```

# まとめ
**Laravel**を使用するにあたり**MVC**のアーキテクチャをそのまま使用するか**DDD**や**クリーンアーキテクチャ**などの概念を取り入れるかは賛否が分かれるところですが、個人的には開発チームの特性やサービスの特性によって適切に変えていくべきだと考えます。例えば大規模開発やチームの入れ替わりが少なくローカルルールを採用しやすい組織では、**Laravel**のような既存アーキテクチャがあるフレームワークに**DDD**や**クリーンアーキテクチャ**のような外の概念を導入してオリジナルフレームワークを構築することで開発効率や保守性を高めることは可能でしょう。しかし、小規模開発やメンバーの入れ替わりが多い組織では、多くの場合Laravelの既存機能を用いて開発する方が効率的だと思います。