Upgrade to Pro — share decks privately, control downloads, hide ads and more …

なぜ適用するか、移行して理解するClean Architecture 〜構造を超えて設計を継承...

なぜ適用するか、移行して理解するClean Architecture 〜構造を超えて設計を継承する〜 / Why Apply, Migrate and Understand Clean Architecture - Inherit Design Beyond Structure

PHP カンファレンス 2025
https://phpcon.php.gr.jp/2025/

Avatar for shiro seike

shiro seike

June 28, 2025
Tweet

More Decks by shiro seike

Other Decks in Programming

Transcript

  1. { return $this->posts->count() < 5; // ドメイン知識:「ユーザーは最大5件まで投稿可能」 } // この実装がLaravel→Symfony移行時に変更不要となる理由

    } <?php // Domain/Entity/User.php - フレームワーク非依存 namespace App\Domain\Entity; // Clean Architectureの核心:Pure PHPで実装されたドメイン層 class User // Entityはアイデンティティを持つビジネスオブジェクト { private UserId $id; // Value Objectによる型安全性の確保 private UserName $name; // プリミティブ型ではなくドメイン概念として表現 private Email $email; // バリデーションロジックをValue Object内に委譲 private Posts $posts; // コレクションも専用のValue Objectで管理 public function __construct(UserId $id, UserName $name, Email $email) // コンストラクタでの不変条件の確立 { $this->id = $id; // Value Object を使用:IDの一意性と型安全性を保証 $this->name = $name; // バリデーションは Value Object 内で実行済み $this->email = $email; // フレームワーク依存なし:Laravel/Symfony問わず動作 $this->posts = new Posts([]); // 初期状態は空のコレクションで安全に初期化 } public function canCreateNewPost(): bool // ビジネスルールをEntityに集約
  2. public function equals(Email $other): bool // 値による等価性:Value Objectの重要特性 { return

    $this->value === $other->value; // 同じ値 = 同じオブジェクト:ビジネス的な同一性判定 } // この仕組みがLaravel/Symfony関係なく動作する理由 } <?php // Domain/ValueObject/Email.php namespace App\Domain\ValueObject; // Value ObjectもDomain層に配置:ドメイン知識の一部 class Email // 値そのものを表現するオブジェクト:メールアドレスの概念を型で表現 { private string $value; // 不変な内部状態:一度作成されたら変更されない public function __construct(string $email) // 生成時バリデーション:不正な値でのオブジェクト作成を防ぐ { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { // ドメインルール:メールアドレス形式の検証 throw new InvalidArgumentException('Invalid email format'); // 例外による契約:不正値は作成させない } $this->value = $email; // 妥当な値のみ保持:以降は常に正しいメールアドレスであることが保証される } public function getValue(): string // 値の取得:カプセル化されたプリミティブ値へのアクセス { return $this->value; // 内部表現を返却:フレームワーク層での利用時 }
  3. src/ ├── Domain/ # ビジネスルール中核 │ ├── Entity/ # User.php

    - Pure PHPエンティティ │ ├── ValueObject/ # Email.php, UserId.php等 │ └── Repository/ # UserRepositoryInterface.php ├── Application/ # アプリケーション処理 │ └── UseCase/User/ # CreateUserUseCase.php等 └── Infrastructure/ # 外部技術実装 └── Repository/ # InMemoryUserRepository.php
  4. laravel/ ├── src/ │ ├── Infrastructure/Repository/ # LaravelUserRepository.php │ ├──

    Http/Controllers/ # UserController.php │ └── Models/ # EloquentUser.php └── public/ # Webインターフェース symfony/ ├── src/ │ ├── Infrastructure/Repository/ # SymfonyUserRepository.php │ ├── Controller/ # UserController.php │ └── Entity/ # DoctrineUser.php ├── config/ # services.yaml └── public/ # Webインターフェース
  5. // Infrastructure/Repository/LaravelUserRepository.php // Infrastructure層で具体実装 class LaravelUserRepository implements UserRepositoryInterface // Interface契約の実装

    { public function findById(UserId $id): ?User // Domain Entityを返すことでDomain層との契約を守る { $eloquentUser = EloquentUser::find($id->getValue()); // Laravel固有:Eloquent Model 使用 if (!$eloquentUser) { return null; } return new User( // 重要:EloquentからDomain Entityへの変換 new UserId($eloquentUser->id), // ORMの詳細をDomain層から隠蔽 new UserName($eloquentUser->name), // フレームワーク固有データをDomain概念に変換 new Email($eloquentUser->email) // この変換層がフレームワーク移行を可能にする ); } } <?php // Domain/Repository/UserRepositoryInterface.php namespace App\Domain\Repository; // Domain層でInterfaceを定義:依存関係逆転の実現 interface UserRepositoryInterface // データアクセスの契約:フレームワーク非依存 { public function findById(UserId $id): ?User; // Domain Entity を返却:戻り値もDomain概念 public function save(User $user): void; // Domain Entity を受け取り:引数もDomain概念 } // このInterfaceがLaravel/Symfony移行の鍵 ===================================================================================
  6. $user->getName()->getValue(), // 外部システム(Controller等)への戻り値 $user->getEmail()->getValue(), // フレームワーク固有形式への変換は不要 $user->canCreateNewPost() // ビジネスルール結果もそのまま返却 );

    // このコード全体がLaravel固有クラスを一切使用していない点が重要 } } <?php // Application/UseCase/CreateUserUseCase.php namespace App\Application\UseCase; // Application層:アプリケーション固有の処理フロー class CreateUserUseCase // 1つのUseCaseは1つのビジネス機能を表現 { public function __construct( private UserRepositoryInterface $userRepository // Interface に依存:実装ではなく契約に依存 ) {} // DI(依存性注入):フレームワークが具体実装を注入 public function execute(CreateUserRequest $request): UserDto // 入力・出力はDTOで型安全に { // バリデーションは Value Object で実行済み:関心の分離 $user = new User( // Domain Entityの生成:ビジネスルールに従った正しいオブジェクト作成 $this->userRepository->nextId(), // Repository経由でID生成:永続化層の詳細を隠蔽 new UserName($request->name), // Value Object でバリデーション:不正値は例外で弾かれる new Email($request->email) // Value Object でバリデーション:メール形式チェック済み ); $this->userRepository->save($user); // Repository 経由で永続化:ORM詳細は隠蔽 return new UserDto( // DTO で結果を返却:Domain EntityをAPI用データに変換 $user->getId()->getValue(), // Value Objectからプリミティブ値を取得
  7. public function findById(UserId $id): ?User // 戻り値は同じDomain Entity:Laravel版と完全に同じ { $doctrineUser

    = $this->entityManager // Doctrine固有の取得方法 ->find(DoctrineUser::class, $id->getValue()); // Eloquentとは異なるAPI、同じ結果 if (!$doctrineUser) { return null; // 見つからない場合の処理も同じ } return new User( // 重要:全く同じDomain Entity に変換 new UserId($doctrineUser->getId()), // Doctrine→Domain変換:Eloquent版と同じ変換処理 new UserName($doctrineUser->getName()), // ORMの違いを吸収してDomain概念に統一 new Email($doctrineUser->getEmail()) // この変換があるからUseCase層は変更不要 ); // Laravel版のRepositoryと戻り値が完全に同じ:これがフレームワーク移行の鍵 <?php // Infrastructure/Repository/SymfonyUserRepository.php namespace App\Infrastructure\Repository; // Infrastructure層:Symfony/Doctrine実装 class SymfonyUserRepository implements UserRepositoryInterface // 同じInterface契約を実装 { public function __construct( private EntityManagerInterface $entityManager // Symfony固有:Doctrine ORM使用 ) {} // LaravelのEloquentとは異なるORM、しかし同じInterface } }
  8. $this->app->bind( // Laravelコンテナへのバインド UserRepositoryInterface::class, // Interface(契約) LaravelUserRepository::class // Laravel 版実装をバインド:Eloquent使用版

    ); // この設定により、Interface注入時にLaravel実装が自動注入される } // Laravel - AppServiceProvider.php // Laravel固有のDI設定方法 public function register(): void // ServiceProviderでの依存関係定義 {
  9. # Symfony - services.yaml # Symfony固有のDI設定方法 services: # サービスコンテナの定義 App\Domain\Repository\UserRepositoryInterface:

    # Interface(契約)の定義 alias: App\Infrastructure\Repository\SymfonyUserRepository # Symfony実装をエイリアス:Doctrine使用版 ``` # この設定により、Interface注入時にSymfony実装が自動注入される:Laravel版と同じ効果 **結果**: 設定変更だけで移行完了!
  10. eloquent-dependent/ └── src/ ├── Models/ │ └── User.php # Eloquentモデル

    + ビジネスロジック混在 └── Http/Controllers/ └── UserController.php # Model直接操作
  11. phase1-domain-extraction/ └── src/ ├── Domain/Entity/ # User.php (Pure PHP Entity新規作成)

    ├── Http/Controllers/ # UserController.php (まだModel直接操作) └── Models/ └── User.php # toDomainEntity()メソッド追加 phase2-repository-pattern/ └── src/ ├── Domain/ │ ├── Entity/ # User.php │ └── Repository/ # UserRepositoryInterface.php ├── Infrastructure/Repository/ # EloquentUserRepository.php ├── Application/Service/ # UserValidationService.php ├── Http/Controllers/ # Repository経由に変更 └── Models/ # User.php (まだEloquent併存)
  12. // 問題:バリデーションも Model 内に混在 public static function createWithValidation(array $data): self

    // スタティックメソッド:テスト難しい { $validator = Validator::make($data, [ // Laravel Validator 使用:Symfonyでは全く異なるAPI 'email' => 'required|email|unique:users', // Laravel固有バリデーションルール 'name' => 'required|min:2' // この書き方はSymfonyでは使えない ]); if ($validator->fails()) { // Laravel固有のエラーハンドリング throw new ValidationException($validator); // Laravel固有例外:Symfonyでは対応不可 } <?php // App/Models/User.php - Eloquent Model use Illuminate\Database\Eloquent\Model; // Laravel固有:Eloquentへの直接依存 class User extends Model // Model継承:Laravelフレームワークと密結合 { protected $fillable = ['name', 'email']; // Laravel固有:Mass Assignment設定 // リレーション定義:Eloquent固有の機能 public function posts() // メソッド名がリレーションを表現:Eloquentの約束 { return $this->hasMany(Post::class); // Eloquent リレーション:Symfony/Doctrineでは使えない } // Laravel→Symfony移行時にこのメソッドは全書き直し必要 // 問題:ビジネスロジックが Model に混在 public function canCreateNewPost(): bool // ドメイン知識がORM層に漏出 { return $this->posts()->count() < 5; // Eloquent Query Builder 使用:フレームワーク固有処理 } // このビジネスルールがEloquentに依存:移行時の大きな障壁
  13. 'errors' => $e->validator->errors() // Validatorオブジェクトへの直接アクセス:Symfonyでは使えない ], 422); // Laravel固有HTTPステータスコード返却 }

    // この全体が密結合:Symfony移行時にリファクタ必要 } } <?php // App/Http/Controllers/UserController.php use App\Models\User; // Eloquent Modelへの直接依存:コントローラーがORMを直接操作 class UserController extends Controller // Laravel固有Controller継承 { public function store(Request $request): JsonResponse // Laravel固有Request/Response使用 { try { // 例外ハンドリングでビジネスロジックを制御 // 問題:Model のスタティックメソッドを直接呼び出し $user = User::createWithValidation($request->all()); // ControllerがModelの内部実装を知っている return response()->json([ // Laravel固有のJSONレスポンス生成 'id' => $user->id, // 問題:Model の属性に直接アクセス 'name' => $user->name, // カプセル化されていないデータアクセス 'email' => $user->email, // ビジネスルールが無い直接アクセス 'can_create_post' => $user->canCreateNewPost() // Model のメソッド呼び出し:Controllerがビジネスロジックを直接利用 ], 201); } catch (ValidationException $e) { // Laravel固有例外のキャッチ return response()->json([ // エラーレスポンスもLaravel固有方式
  14. { return count($this->posts) < 5; // Pure PHP でビジネスロジック:Eloquent非依存 }

    // これでLaravel→Symfony移行時にビジネスロジックが保持される } <?php // Domain/Entity/User.php - 新規作成 namespace App\Domain\Entity; // Domain層への移行:Eloquentからの脱却 class User // Pure PHPクラス:フレームワーク非依存 { private int $id; // プリミティブ型使用:シンプルな移行例 private string $name; // EloquentのattributeではなくPHPプロパティ private string $email; // 正規化されたデータ表現 private array $posts; // リレーションではなくシンプルな配列 public function __construct(int $id, string $name, string $email, array $posts = []) // コンストラクタでの状態初期化 { $this->id = $id; // バリデーションは別途実装:関心の分離 $this->name = $name; // Eloquentの$fillableやmass assignmentではない $this->email = $email; // 直接的なプロパティ設定 $this->posts = $posts; // 簡潔な初期化 } public function canCreateNewPost(): bool // ビジネスルールの抽出成功
  15. } return new User( // 重要:Eloquent→Domain Entity 変換 $eloquentUser->id, //

    ORM固有データをドメインオブジェクトに $eloquentUser->name, // この変換層がフレームワーク依存を隠蔽 $eloquentUser->email, // UseCase層はDomain Entityしか知らない $eloquentUser->posts->toArray() // Eloquent Collectionを普通の配列に変換 ); // この設計でUseCase層がフレームワーク非依存に } } <?php // Domain/Repository/UserRepositoryInterface.php namespace App\Domain\Repository; // Repository Pattern導入:データアクセスの抽象化 interface UserRepositoryInterface // データアクセスの契約定義 { public function findById(int $id): ?User; // Domain Entityを返却:フレームワーク非依存 public function save(User $user): void; // Domain Entityを受け取り:純粋なビジネスオブジェクト } // このInterfaceがEloquentからDoctrineへの移行を可能にする // Infrastructure/Repository/EloquentUserRepository.php // Eloquent実装版Repository class EloquentUserRepository implements UserRepositoryInterface // Interfaceの実装:Eloquent版 { public function findById(int $id): ?User // Domain Entity返却の契約守る { $eloquentUser = EloquentUser::with('posts')->find($id); // まだEloquent 使用:しかし局所化済み if (!$eloquentUser) { return null; // 結果なしの場合の処理
  16. $user->getId(), // 正規化されたデータ取得 $user->getName(), // カプセル化されたアクセサ $user->getEmail(), // フレームワーク非依存データ $user->canCreateNewPost()

    // ビジネスルール結果も含めて返却 ); // このUseCaseがフレームワーク非依存なのでSymfony移行が可能 } } <?php // Application/UseCase/CreateUserUseCase.php namespace App\Application\UseCase; // UseCase層の導入:アプリケーション処理の分離 class CreateUserUseCase // ビジネス処理の結節点:Controllerからの分離成功 { public function __construct( private UserRepositoryInterface $userRepository, // Repository Interfaceに依存:実装非依存 private ValidatorInterface $validator // バリデーション分離:Laravel Validatorからの脱却 ) {} // DIでInterface注入:コンクリートクラス非依存 public function execute(CreateUserRequest $request): UserDto // ビジネス処理のカプセル化 { // バリデーション実行:Laravel固有の方法からの脱却 $this->validator->validate($request); // Interface経由でバリデーション $user = new User( // Domain Entityでビジネスオブジェクト生成 0, // Repository で ID 生成:永続化詳細の隠蔽 $request->name, // シンプルなデータ渡し $request->email // フレームワーク固有処理なし ); $this->userRepository->save($user); // Repository経由で永続化:ORM詳細隠蔽 return new UserDto( // DTOで結果返却:ドメインオブジェクトからAPI用データ変換
  17. $doctrineUser->getName(), // メソッド名は異なるが結果は同じ $doctrineUser->getEmail(), // ORMの違いをRepository層で吸収 $doctrineUser->getPosts()->toArray() // Collection→配列変換も同じ );

    // この結果、UseCase層は何も変更せずに動作:移行成功 } } <?php // Infrastructure/Repository/DoctrineUserRepository.php namespace App\Infrastructure\Repository; // Infrastructure層:Symfony移行後の実装 class DoctrineUserRepository implements UserRepositoryInterface // 同じInterface契約:移行成功の証拠 { public function __construct( private EntityManagerInterface $entityManager // Doctrine 使用:Symfony固有ORM ) {} // Eloquentとは全く異なるORM、しかし同じ目的 public function findById(int $id): ?User // Interface契約遵守:同じDomain Entity返却 { $doctrineUser = $this->entityManager // Doctrine固有のデータ取得方法 ->getRepository(DoctrineUser::class) // Eloquent::find()とは異なるAPI ->find($id); // しかし同じ結果を得る if (!$doctrineUser) { return null; // エラーハンドリングも同じ } return new User( // 最重要:全く同じDomain Entity生成 $doctrineUser->getId(), // Doctrineエンティティ→Domain Entity変換