はじめに
本記事ではPHPのフレームワークLaravelにDDDを導入する方法を解説します。
前提として...LaravelはMVCが採用されていたりService Providerなどの便利なDIツールが採用されていることから、DDDを導入せずとも規則的なコードを書くことが可能になっています。そのため本記事で紹介する内容はあくまで一案を紹介することを目的としています。
GitHub
- 本記事で紹介するリポジトリ
プロジェクト構成
- ベースはレイヤードアーキテクチャ
- Eloquent Model(ORM)やService Provider(DI)などを利用してLaravelの既存機能を活かした構成にする
- NginxではなくLaravel Octane(Swoole)を使用する
ディレクトリ構成
- 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(アプリケーション層)
依存関係
環境構築
※Docker環境が必要です。
- コンテナを起動
make docker_up
- Octuneサーバーを起動
make docker_run
コード解説
例では下記のAPIを実装して解説します。
POST: http://localhost:8000/api/create_example
- リクエスト
{
"name": "example1"
}
- レスポンス
{
"id": 1,
"name": "example1",
"message": "Example completed"
}
GET: http://localhost:8000/api/get_example/1
- レスポンス
{
"id": 1,
"name": "example1",
"message": "Example completed"
}
Model
レイヤードアーキテクチャで実装を行う場合、依存関係の低いレイヤーから実装していきます。今回の場合ではModelから実装します。
Modelではロジックの中心となる構造体(※PHPには構造体の機能はありませんが便宜上構造体と呼んでいます)やModelに強く依存している変換処理を記述していきます。今回の場合は単純な構造体とそれをJson化して呼び出すjsonSerialize()
メソッドを追加しています。
例えばよくある実装例としてはemailのバリデーションや特定の配列やJsonをエンコードしてModelに格納する処理などはこちらにメソッドを追加します。
<?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
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
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
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
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
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
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
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
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の既存機能を用いて開発する方が効率的だと思います。