Laravelにオニオンアーキテクチャを導入実験
Laravelは優れたPHPフレームワークですが、デフォルトの構造では大規模なアプリケーションの開発において課題が生じることがあると思います。
そこでLaravelの利点を残しつつ、オニオンアーキテクチャを導入して開発してみたのでその試行結果を書きたいと思います。
開発内容については、以下の記事に書きました。
オニオンアーキテクチャとは
ChatGPTに聞いてみました。
オニオンアーキテクチャは、ソフトウェアのアーキテクチャパターンの一つで、内部のコードや依存関係を層状に整理する設計手法です。
中心にあるコア部分(Core)を中心に、その周囲には具体的な実装や外部サービスに依存した部分を分離した層(Layers)が配置されます。
これにより、各層が特定の責務を持ち、疎結合で保守性や拡張性が高まります。
なぜLaravelにオニオンアーキテクチャを導入するのか
Laravelは「Convention over Configuration」の原則に基づいており、迅速な開発を可能にします。
しかし、大規模なプロジェクトでは以下の課題が生じることがあると思います。
- ビジネスロジックの散在: コントローラやモデルにビジネスロジックが散在しがち
- テスト困難性: フレームワークに強く依存したコードはテストが難しい
- 変更の影響範囲: フレームワークの変更がアプリケーション全体に影響する
オニオンアーキテクチャを導入することで、これらの課題を解決し、より保守性の高いコードベースを実現できると考えます。
※今回開発した程度の規模であれば、オニオンアーキテクチャはオーバーエンジニアリングになると思います。今後本格的なEC管理システムとなる想定での実験と捉えていただければと思います。
導入方針
導入方針は以下の通りです。
- オニオンアーキテクチャをベースにドメイン層とユースケース層はLaravelに依存しないようにする
- ただし、DB(トランザクション)やLOGはLaravelのFacadeを使っています(横断事関心毎としてこれらも抽象化する案も考えましたが、冗長になるため止めました。参考)
- インフラ層でのDBの読み書きはEloquentModelを使用する
アーキテクチャ図
ディレクトリ構造
オニオンアーキテクチャを導入したLaravelプロジェクトのディレクトリ構造は以下のようになります。
app/
├── Console/
│ └─ Commands/ # アプリケーション層
│ └─ ReceiveTestMallOrderBatch.php
├── Http/
│ ├── Controllers/ # アプリケーション層の一部
│ │ └── OrderController.php
│ └── ...
├── Models/ # Eloquentモデル
│ ├── Order.php
│ ├── OrderItem.php
│ └── ...
├── Packages/ # オニオンアーキテクチャの実装
│ ├── Order/
│ │ ├── Domains/ # ドメイン層
│ │ │ ├── ValueObjects/
│ │ │ │ ├── Order.php
│ │ │ │ ├── OrderId.php
│ │ │ │ └── ...
│ │ │ ├── Services/
│ │ │ │ └── InvoiceService.php
│ │ │ └── OrderRepositoryInterface.php
│ │ ├── UseCases/ # ユースケース層
│ │ │ ├── OrderShowUseCase.php
│ │ │ └── ...
│ │ └── Infrastructures/ # インフラ層
│ │ ├── OrderRepository.php
│ │ └── ...
│ └── Shared/ # 共有コンポーネント
│ └── ...
└── ...
各層の詳細
ドメイン層
ドメイン層はビジネスロジックの中心であり、以下のコンポーネントで構成されます。
- Entity: ビジネスオブジェクトを表現するクラス
- ValueObject: 値を表現する不変のオブジェクト
- RepositoryInterface: データアクセスの抽象化
この層はフレームワークに依存せず、純粋なPHPで実装します。
また、リポジトリのインターフェースもこの層で定義しています。
ユースケース層
ユースケース層はアプリケーションのユースケース(機能)を実装します。
- コンストラクタDIでインフラ層のクラスを設定
- ドメイン層のオブジェクトを操作する
アプリケーション層
アプリケーション層はユーザーインターフェースとユースケースを接続します。
- Controller: HTTPリクエストを処理
- Commands: コンソールコマンドを処理
※この層はLaravelに依存します。
インフラ層
インフラ層はDB,外部システムとの連携を担当します。
- Repository: EloquentModelを使用してDBの読み書き
※この層はLaravelに依存します。
依存性の注入
Laravelの依存性注入コンテナを使用して、インターフェースと実装をバインドします。
コード例
// app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;
use App\Packages\Order\Domains\OrderGetterInterface;
use App\Packages\Order\Domains\OrderRepositoryInterface;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(OrderGetterInterface::class, function ($app) {
return new \App\Packages\Order\Infrastructures\TestMallOrderGetter();
});
$this->app->bind(OrderRepositoryInterface::class, function ($app) {
return new \App\Packages\Order\Infrastructures\OrderRepository();
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
試行してみた雑感
オニオンアーキテクチャを導入することによって層によって責務を明確に分離することができ、以下のメリットがあると考えます。
- コードの見通しが良くなる
- 変更容易性(保守性)が高まる
- 単体テストがし易くなる
しかしながら、小規模なプロジェクトでは、オニオンアーキテクチャはオーバーエンジニアリングになる可能性があります。
ただ小規模なプロジェクトでもドメインが複雑であったり変更が多かったりする場合は導入する利点は大きいと考えます。
プロジェクトの規模と要件に応じて、適切なアーキテクチャを選択することが重要です。
さいごに
最後までお読みいただき、ありがとうございました。
この記事がわずかでも役に立てば幸いです。
Discussion