レイヤードアーキテクチャからクリーンアーキテクチャへ
依存関係を内側に向けるべき理由を理解します。
「レイヤードアーキテクチャからクリーンアーキテクチャへ」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
クリーンアーキテクチャの理由
従来の3層PHPスタック、つまりController → Service → Repository → Databaseはすでにご存じでしょう。これは機能しますが、ビジネスロジックがEloquent、Doctrine、HTTPリクエスト、フレームワークのライフサイクルに結合してしまいます。クリーンアーキテクチャでは依存方向を逆転させ、ドメインがインフラストラクチャについて何も知らないようにします。その結果、テスト可能なユースケース、交換可能なアダプター、フレームワークのアップグレードに耐えられるコードベースが得られます。
依存関係のルール
クリーンアーキテクチャの唯一のルールは、ソースコードの依存関係を内側にのみ向けることです。内側の円(エンティティ、ユースケース)は、外側の円(コントローラー、ORM、フレームワーク)を決して参照してはいけません。実行時の制御フローはインターフェースを通じて外側に向かいますが、コンパイル/インポート時には、内側から外側のものをインポートすることはありません。
- エンティティ:エンタープライズルール
- ユースケース:アプリケーションルール
- アダプター:コントローラー、プレゼンター、ゲートウェイ
- フレームワークとドライバー:DB、HTTP、Web
結合したレイヤード構成の例
ここに示すのは、多くのPHPアプリケーションで使われているサービスの例です。ドメインロジックがEloquentやHTTPレスポンスと絡み合っていることに注目してください。データベースとフレームワークなしに、割引ルールを単体テストすることはできません。
<?php
class OrderService
{
public function place(Request $request)
{
$user = User::find($request->user_id); // Eloquent
$total = 0;
foreach ($request->items as $i) {
$total += Product::find($i['id'])->price * $i['qty'];
}
if ($user->is_vip) {
$total *= 0.9; // business rule trapped in infra code
}
Order::create(['user_id' => $user->id, 'total' => $total]);
return response()->json(['total' => $total]);
}
}エンティティ:フレームワークに依存しないドメイン
エンティティはエンタープライズ全体に及ぶルールを表現し、何にも依存しません。プレーンなPHPで記述され、アノテーションもORMの基底クラスもありません。テストで完全に構築できます。
<?php
final class Money
{
public function __construct(public readonly int $cents) {
if ($cents < 0) throw new InvalidArgumentException('negative money');
}
public function multiply(float $factor): self {
return new self((int) round($this->cents * $factor));
}
}
final class Order
{
/** @param array<int,int> $lineCents */
public function __construct(private array $lineCents, private bool $vip) {}
public function total(): Money {
$sum = array_sum($this->lineCents);
$money = new Money($sum);
return $this->vip ? $money->multiply(0.9) : $money;
}
}
echo (new Order([1000, 2000], true))->total()->cents, PHP_EOL; // 2700ユースケースがワークフローを所有する
ユースケース(インタラクター)はエンティティをオーケストレーションし、外部とのやり取りはインターフェース(ポート)を通じてのみ行います。リクエストDTOを受け取り、レスポンスDTOを返します。HTTPオブジェクトを返すことはありません。
<?php
interface OrderRepository {
public function save(Order $order): void;
}
final class PlaceOrder
{
public function __construct(private OrderRepository $orders) {}
public function execute(array $lineCents, bool $vip): int {
$order = new Order($lineCents, $vip);
$this->orders->save($order);
return $order->total()->cents;
}
}境界はインターフェース
ユースケースは、必要とするOrderRepositoryインターフェースを宣言します。インターフェースは内側の円に属し、具象的なEloquent/Doctrine実装は外側に存在して内側に依存します。これは、アーキテクチャの境界に適用した依存性逆転の原則です。
ソースコードの依存方向:EloquentOrderRepository → OrderRepository(インターフェース)であり、逆方向ではありません。
<?php
// Lives in infrastructure layer, points INWARD to the domain interface
final class EloquentOrderRepository implements OrderRepository
{
public function save(Order $order): void {
OrderModel::create(['total' => $order->total()->cents]);
}
}インフラなしでのテスト
ユースケースはインターフェースに依存するため、テストではフェイクを注入できます。データベースもフレームワークの起動も必要ありません。マイクロ秒単位で実行できる単体テストで、純粋なビジネスの振る舞いを検証できます。
<?php
final class InMemoryOrders implements OrderRepository {
public array $saved = [];
public function save(Order $o): void { $this->saved[] = $o; }
}
$repo = new InMemoryOrders();
$useCase = new PlaceOrder($repo);
$total = $useCase->execute([1000, 2000], true);
assert($total === 2700);
assert(count($repo->saved) === 1);
echo "PASS total=$total saved=" . count($repo->saved) . PHP_EOL;コントローラーを薄いアダプターにする
コントローラーはアダプターになりました。HTTPをユースケースの呼び出しに変換し、結果をHTTPに変換します。ビジネスルールは一切持ちません。RESTをCLIやキューワーカーに置き換えても、ユースケースには影響がありません。
<?php
final class OrderController
{
public function __construct(private PlaceOrder $placeOrder) {}
public function store(Request $request): JsonResponse {
$total = $this->placeOrder->execute(
lineCents: $request->input('lineCents'),
vip: (bool) $request->input('vip'),
);
return new JsonResponse(['total' => $total], 201);
}
}スクリーミングアーキテクチャ
フォルダー構成はフレームワークではなく、ドメインを明確に示すものであるべきです。トップレベルにControllers/やModels/を置くのは避けます。ケイパビリティごとに整理し、新しく参加した人にもアプリケーションの目的がわかるようにします。
src/Ordering/Domain/— エンティティ、値オブジェクトsrc/Ordering/Application/— ユースケース、ポートインターフェースsrc/Ordering/Infrastructure/— Eloquentリポジトリ、HTTPコントローラー
各境界づけられたコンテキストをトップレベルのフォルダーにし、フレームワークは境界に置きます。
依存関係のルールを強制する
ツールがなければ規律は失われます。CIでdeptracまたはphparkitectを使い、DomainがInfrastructureをインポートしたときにビルドを失敗させます。これにより、ルールはコードレビューでの期待ではなく、コンパイル時に保証されるものになります。
# deptrac.yaml
deptrac:
layers:
- name: Domain
collectors: [{ type: directory, value: src/.*/Domain/.* }]
- name: Application
collectors: [{ type: directory, value: src/.*/Application/.* }]
- name: Infrastructure
collectors: [{ type: directory, value: src/.*/Infrastructure/.* }]
ruleset:
Domain: [] # Domain may depend on nothing
Application: [Domain]
Infrastructure: [Application, Domain]DTOで境界を越える
エンティティが外側に漏れ出すのを防ぐため、境界を越えるデータはエンティティやORMモデルではなく、単純なDTOとして渡します。ユースケースはアダプターがシリアライズできるフラットな構造を返すため、ドメインオブジェクトがコアの外に出ることはなく、外側のレイヤーが内部状態への参照を得ることもありません。
<?php
final class OrderSummary // boundary DTO, no behavior, no domain types
{
public function __construct(
public readonly string $orderId,
public readonly int $totalCents,
) {}
}
final class PlaceOrderV2 {
public function __construct(private OrderRepository $orders) {}
public function execute(array $lineCents, bool $vip): OrderSummary {
$order = new Order($lineCents, $vip);
$this->orders->save($order);
return new OrderSummary('ord_1', $order->total()->cents);
}
}クイックチェック
依存関係のルールで許可される依存方向はどれでしょうか。
まとめ
結合したレイヤードスタックからクリーンアーキテクチャへ移行しました。
- 依存関係のルール:ソースコードの依存関係は内側にのみ向けます。
- エンティティ:フレームワークに依存しないPHPでエンタープライズルールを保持します。
- ユースケース:ポートインターフェースを通じてオーケストレーションし、HTTPではなくDTOを返します。
- コントローラーとORMリポジトリは、内側に依存する外側のアダプターです(DIP)。
- 構造はドメインを明確に示すべきであり、deptracなどのツールがCIでルールを強制します。
よくある質問
「レイヤードアーキテクチャからクリーンアーキテクチャへ」レッスンは無料ですか?
はい。「レイヤードアーキテクチャからクリーンアーキテクチャへ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「レイヤードアーキテクチャからクリーンアーキテクチャへ」で何を学びますか?
依存関係を内側に向けるべき理由を理解します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「レイヤードアーキテクチャからクリーンアーキテクチャへ」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- レイヤードアーキテクチャからクリーンアーキテクチャへ
- ポートとアダプターの概要
- ユースケースとアプリケーションサービス
- 依存性逆転の実践